Repository: PlasmaPy/PlasmaPy
Pull request #942 (2020-10-22 20:57:36)

Update documentation for particles and utils subpackages
This PR includes minor updates for the documentation for `plasmapy.particles`.  Examples include updating docstrings for pre-defined `Particle` objects (like `proton`, etc.) and fixing links.

codecov[bot]: # [Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/942?src=pr&el=h1) Report
> Merging [#942](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/942?src=pr&el=desc) into [master](https://codecov.io/gh/PlasmaPy/PlasmaPy/commit/6f277181fccb0c26cc623942892f7b18daae4a66?el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/942/graphs/tree.svg?width=650&height=150&src=pr&token=LzZ5mnXVad)](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/942?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master     #942   +/-   ##
=======================================
  Coverage   96.19%   96.19%           
=======================================
  Files          60       60           
  Lines        5413     5413           
=======================================
  Hits         5207     5207           
  Misses        206      206           
```


| [Impacted Files](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/942?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [plasmapy/particles/\_\_init\_\_.py](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/942/diff?src=pr&el=tree#diff-cGxhc21hcHkvcGFydGljbGVzL19faW5pdF9fLnB5) | `100.00% <ø> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/942?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/942?src=pr&el=footer). Last update [6f27718...bba6cc0](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/942?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/changelog/942.doc.rst b/changelog/942.doc.rst
new file mode 100644
index 00000000..9c45d872
--- /dev/null
+++ b/changelog/942.doc.rst
@@ -0,0 +1,1 @@
+Updated documentation for the ``particles`` and ``utils`` subpackages.
diff --git a/changelog/959.bugfix.rst b/changelog/959.bugfix.rst
new file mode 100644
index 00000000..4dd56169
--- /dev/null
+++ b/changelog/959.bugfix.rst
@@ -0,0 +1,2 @@
+Bugfix :meth:`plasmapy.analysis.fit_functions.Linear.root_solve` to handle the case
+where the slope is zero and no finite roots exist.
diff --git a/docs/particles/decorators.rst b/docs/particles/decorators.rst
index 8b96776f..8f4e806f 100644
--- a/docs/particles/decorators.rst
+++ b/docs/particles/decorators.rst
@@ -1,25 +1,29 @@
-.. _atomic-decorators:
+.. _particles-decorators:
 
 Decorators
 **********
 
-Passing `~plasmapy.particles.Particle` Instances to Functions and Methods
-=========================================================================
+.. _particles-decorators-particle-input:
+
+Passing `~plasmapy.particles.Particle` objects to functions and methods
+=======================================================================
 
 When calculating plasma parameters, we very frequently need to access
 the properties of the particles that make up that plasma. The
-`~plasmapy.particles.particle_input` decorator allows functions and
-methods to easily the properties of a particle.
+`~plasmapy.particles.decorators.particle_input` decorator allows functions
+and methods to easily access properties of different particles.
 
 The `~plasmapy.particles.particle_input` decorator takes valid
 representations of particles given in arguments to functions and passes
-through the corresponding instance of the `~plasmapy.particles.Particle`
-class.  The arguments must be annotated with `~plasmapy.particles.Particle`
+through the corresponding `~plasmapy.particles.Particle` object.  The
+arguments must be annotated with `~plasmapy.particles.Particle`
 so that the decorator knows to create the `~plasmapy.particles.Particle`
-instance.  The function can then access particle properties by using
+object.  The decorated function can then access particle properties by using
 `~plasmapy.particles.Particle` attributes.  This decorator will raise an
-`~plasmapy.particles.exceptions.InvalidParticleError` if the input does not correspond
-to a valid particle.
+`~plasmapy.particles.exceptions.InvalidParticleError` if the input does
+not correspond to a valid particle.
+
+Here is an example of a decorated function.
 
 .. code-block:: python
 
@@ -29,6 +33,15 @@ to a valid particle.
   def particle_mass(particle: Particle):
       return particle.mass
 
+This function can now accept either `~plasmapy.particles.Particle` objects
+or valid representations of particles.
+
+>>> particle_mass('p+')  # string input
+<Quantity 1.67262192e-27 kg>
+>>> proton = Particle("proton")
+>>> particle_mass(proton)  # Particle object input
+<Quantity 1.67262192e-27 kg>
+
 If only one positional or keyword argument is annotated with
 `~plasmapy.particles.Particle`, then the keywords ``mass_numb`` and ``Z``
 may be used when the decorated function is called.
@@ -76,9 +89,9 @@ associated with an element, isotope, or ion; respectively.
   def number_of_bound_electrons(ion: Particle):
       return ion.atomic_number - ion.integer_charge
 
-The keywords ``require``, ``any_of``, and ``exclude`` to the decorator
-allow further customization of the particle categories allowed as
-inputs.  These keywords are used as in
+The keywords ``require``, ``any_of``, and ``exclude`` to the
+decorator allow further customization of the particle categories
+allowed as inputs.  These keywords are used as in
 `~plasmapy.particles.Particle.is_category`.
 
 .. code-block:: python
diff --git a/docs/particles/functional.rst b/docs/particles/functional.rst
index e57c1a26..901787ea 100644
--- a/docs/particles/functional.rst
+++ b/docs/particles/functional.rst
@@ -1,4 +1,4 @@
-.. _atomic-functions:
+.. _particles-functions:
 
 Functions
 *********
@@ -6,9 +6,9 @@ Functions
 In addition to the `~plasmapy.particles.Particle` class, the
 `~plasmapy.particles` subpackage has a functional interface.
 
-.. _atomic-func-symbols:
+.. _particles-func-symbols:
 
-Symbols and Names
+Symbols and names
 =================
 
 Several functions in `~plasmapy.particles` return string representations
@@ -34,9 +34,9 @@ The full symbol of the particle can be found using
 >>> particle_symbol('electron')
 'e-'
 
-.. _atomic-func-properties:
+.. _particles-func-properties:
 
-Particle Properties
+Particle properties
 ===================
 
 The `~plasmapy.particles.atomic_number` and `~plasmapy.particles.mass_number`
@@ -76,7 +76,7 @@ The mass of a particle may be accessed through the
 >>> particle_mass('deuteron')
 <Quantity 3.34358372e-27 kg>
 
-.. _atomic-func-isotopes:
+.. _particles-isotopes:
 
 Isotopes
 ========
@@ -107,7 +107,7 @@ All stable isotopes of an element may be found with
 >>> stable_isotopes('Pb')
 ['Pb-204', 'Pb-206', 'Pb-207', 'Pb-208']
 
-.. _atomic-func-stability:
+.. _particles-func-stability:
 
 Stability
 =========
@@ -127,7 +127,7 @@ half-life as a `~astropy.units.Quantity` in units of seconds, if known.
 <Quantity 881.5 s>
 
 For stable particles (or particles that have not been discovered to be
-unstable), `~plasmapy.particles.half_life` returns infinity seconds.
+unstable), `~plasmapy.particles.half_life` returns `~numpy.inf` seconds.
 
 >>> half_life('p+')
 <Quantity inf s>
@@ -136,8 +136,8 @@ If the particle's half-life is not known to sufficient precision, then
 `~plasmapy.particles.half_life` returns a `str` with the estimated value
 while issuing a `~plasmapy.particles.exceptions.MissingParticleDataWarning`.
 
-Additional Properties
-=====================
+Reduced mass
+============
 
 The `~plasmapy.particles.reduced_mass` function is useful in cases of
 two-body collisions.
diff --git a/docs/particles/nuclear.rst b/docs/particles/nuclear.rst
index 089a5546..0a3036bd 100644
--- a/docs/particles/nuclear.rst
+++ b/docs/particles/nuclear.rst
@@ -1,11 +1,15 @@
+.. _particles-nuclear-reactions:
+
 Nuclear Reactions
 *****************
 
-Binding Energy
+.. _particles-nuclear-binding-energy:
+
+Binding energy
 ==============
 
 The binding energy of a nuclide may be accessed either as an
-attribute of a `~plasmapy.particles.Particle` instance, or by using the
+attribute of a `~plasmapy.particles.Particle` object, or by using the
 `~plasmapy.particles.nuclear_binding_energy` function.
 
 >>> from plasmapy.particles import Particle, nuclear_binding_energy
@@ -15,8 +19,10 @@ attribute of a `~plasmapy.particles.Particle` instance, or by using the
 >>> nuclear_binding_energy('D').to('GeV')
 <Quantity 0.00222457 GeV>
 
-Nuclear Reactions
-=================
+.. _particles-nuclear-reaction-energy:
+
+Nuclear reaction energy
+=======================
 
 The energy released from a nuclear reaction may be found using the
 `~plasmapy.particles.nuclear_reaction_energy` function.  The input may be
diff --git a/docs/particles/particle_class.rst b/docs/particles/particle_class.rst
index 6a49f70e..fd437343 100644
--- a/docs/particles/particle_class.rst
+++ b/docs/particles/particle_class.rst
@@ -1,17 +1,18 @@
 .. _particle-class:
 
-Particle Class
-**************
+`~plasmapy.particles.Particle` Class
+************************************
 
 The `~plasmapy.particles.Particle` class provides an object-oriented
-interface for particle information.
+interface to access and represent particle information.
 
-Creating a Particle Instance
-============================
+.. _particle-class-instantiation:
 
-The simplest way to create an instance of the
-`~plasmapy.particles.Particle` class is to pass it a `str` representing a
-particle.
+Creating a `~plasmapy.particles.Particle` object
+================================================
+
+The simplest way to create a `~plasmapy.particles.Particle` object
+is to pass it a `str` representing a particle.
 
 >>> from plasmapy.particles import Particle
 >>> electron = Particle('e-')
@@ -37,20 +38,22 @@ keyword.
 
 >>> proton = Particle(1, mass_numb=1, Z=1)
 
-The most frequently used `~plasmapy.particles.Particle` instances may be
+The most frequently used `~plasmapy.particles.Particle` objects may be
 imported directly from the atomic subpackage.
 
 >>> from plasmapy.particles import proton, electron
 
-The `~plasmapy.particles.Particle` instances that may be imported
+The `~plasmapy.particles.Particle` objects that may be imported
 directly are: ``proton``, ``electron``, ``neutron``, ``positron``,
 ``deuteron``, ``triton``, and ``alpha``.
 
-Accessing Particle Properties
+.. _particle-class-properties:
+
+Accessing particle properties
 =============================
 
 The properties of each particle may be accessed using the attributes of
-the `~plasmapy.particles.Particle` instance.
+the corresponding `~plasmapy.particles.Particle` object.
 
 >>> proton.atomic_number
 1
@@ -131,14 +134,14 @@ particle.  Valid categories include: ``'actinide'``, ``'alkali metal'``,
 ``'post-transition metal'``, ``'proton'``, ``'stable'``,
 ``'transition metal'``, ``'uncharged'``, and ``'unstable'``.
 
-.. _particle-class-conditionals:
+.. _particle-class-conditionals-equality:
 
-Conditionals and Equality Properties
+Conditionals and equality properties
 ====================================
 
 Equality between particles may be tested either between two
-`~plasmapy.particles.Particle` instances, or between a
-`~plasmapy.particles.Particle` instance and a `str`.
+`~plasmapy.particles.Particle` objects, or between a
+`~plasmapy.particles.Particle` object and a `str`.
 
 >>> Particle('H-1') == Particle('protium 1+')
 False
@@ -176,13 +179,13 @@ categories.
 
 .. _particle-class-antiparticles:
 
-Returning Antiparticles
+Returning antiparticles
 =======================
 
 The antiparticle of an elementary particle or antiparticle may be found
-by either using Python's unary invert operator (~) or the
+by either using Python's unary invert operator (``~``) or the
 `~plasmapy.particles.Particle.antiparticle` attribute of a
-`~plasmapy.particles.Particle` instance.
+`~plasmapy.particles.Particle` object.
 
 >>> ~electron
 Particle("e+")
diff --git a/docs/utils/index.rst b/docs/utils/index.rst
index 9b9922a9..8cace26e 100644
--- a/docs/utils/index.rst
+++ b/docs/utils/index.rst
@@ -11,18 +11,18 @@ Core package utilities (`plasmapy.utils`)
 Introduction
 ============
 
-`PlasmaPy.utils` is where we store functionality that helps us write
-(what we try to think of as) clean, readable and informative code. This
-means:
-
- * the many kinds of warnings and exceptions you may (hopefully not!)
-   encounter while working with PlasmaPy, such as
-   `plasmapy.utils.RelativityWarning` or `plasmapy.utils.PhysicsError`.
- * decorators we use for reusable physical quantity computation and
-   checking, such as `plasmapy.utils.decorators.validate_quantities` and
-   `plasmapy.utils.check_relativistic`.
- * Some helper utilities for importing and testing packages such as
-   `plasmapy.utils.call_string`.
+The `~plasmapy.utils` subpackage contains functionality that is needed
+across multiple subpackages or does not fit nicely in any other subpackage.
+Functionality contained in `~plasmapy.utils` includes:
+
+ * Warnings and exceptions used in PlasmaPy, such as
+   `~plasmapy.utils.exceptions.RelativityWarning` or
+   `~plasmapy.utils.exceptions.PhysicsError`.
+ * Decorators we use for reusable physical `~astropy.units.Quantity`
+   computation and checking, such as
+   `~plasmapy.utils.decorators.validate_quantities`
+   and `~plasmapy.utils.decorators.check_relativistic`.
+ * Helper utilities for importing and testing packages.
 
 Reference/API
 =============
diff --git a/plasmapy/analysis/fit_functions.py b/plasmapy/analysis/fit_functions.py
index 89fc7718..56bec2a4 100644
--- a/plasmapy/analysis/fit_functions.py
+++ b/plasmapy/analysis/fit_functions.py
@@ -17,6 +17,7 @@
 from scipy.optimize import curve_fit, fsolve
 from scipy.stats import linregress
 from typing import Tuple, Union
+from warnings import warn
 
 from plasmapy.utils.decorators import modify_docstring
 
@@ -590,10 +591,20 @@ def root_solve(self, *args, **kwargs):
             :attr:`parameters` and :attr:`parameters_err`.
         """
         m, b = self.params
+
+        if m == 0.0:
+            warn(
+                f"Slope of Linear fit function is zero so no finite root exists. ",
+                RuntimeWarning,
+            )
+            return _RootResults(np.nan, np.nan)
+
         root = -b / m
 
         m_err, b_err = self.param_errors
-        err = np.abs(root) * np.sqrt((m_err / m) ** 2 + (b_err / b) ** 2)
+        m_term = (root * m_err / m) ** 2
+        b_term = (b_err / m) ** 2
+        err = np.sqrt(m_term + b_term)
 
         return _RootResults(root, err)
 
diff --git a/plasmapy/particles/__init__.py b/plasmapy/particles/__init__.py
index 467f8512..2873f3e9 100644
--- a/plasmapy/particles/__init__.py
+++ b/plasmapy/particles/__init__.py
@@ -45,28 +45,29 @@
     particle_symbol,
 )
 
-# Create instances of the most commonly used particles
-
-#: PlasmaPy particle object for a proton
 proton = Particle("p+")
+"""A `Particle` instance representing a proton."""
 
-#: PlasmaPy particle object for an electron
 electron = Particle("e-")
+"""A `Particle` instance representing an electron."""
 
-#: PlasmaPy particle object for a neutron
 neutron = Particle("n")
+"""A `Particle` instance representing a neutron."""
 
-#: PlasmaPy particle object for a positron
 positron = Particle("e+")
+"""A `Particle` instance representing a positron."""
 
-#: PlasmaPy particle object for a deuteron
 deuteron = Particle("D 1+")
+"""A `Particle` instance representing a positively charged deuterium ion."""
 
-#: PlasmaPy particle object for a triton
 triton = Particle("T 1+")
+"""A `Particle` instance representing a positively charged tritium ion."""
 
-#: PlasmaPy particle object for an alpha particle
 alpha = Particle("He-4 2+")
+"""
+A `Particle` instance representing an alpha particle (doubly charged
+helium-4).
+"""
 
 # auto populate __all__
 for obj_name in list(globals()):


Test patch:
diff --git a/plasmapy/analysis/tests/test_fit_functions.py b/plasmapy/analysis/tests/test_fit_functions.py
index 6c4919b0..9436890b 100644
--- a/plasmapy/analysis/tests/test_fit_functions.py
+++ b/plasmapy/analysis/tests/test_fit_functions.py
@@ -577,12 +577,33 @@ def func_err(self, x, params, param_errors, x_err=None):
 
         return err
 
-    def test_root_solve(self):
-        ff_obj = self.ff_class(params=(1, 1), param_errors=(0, 0))
-        assert ff_obj.root_solve() == (-1, 0)
-
-        ff_obj.params = (5.0, 1.3)
-        ff_obj.param_errors = (0.1, 0.1)
-        root, err = ff_obj.root_solve()
-        assert root == -1.3 / 5.0
-        assert err == np.abs(root) * np.sqrt((0.1 / 5.0) ** 2 + (0.1 / 1.3) ** 2)
+    @pytest.mark.parametrize(
+        "params, param_errors, root, root_err, conditional",
+        [
+            ((1, 1), (0, 0), -1, 0, does_not_raise()),
+            (
+                (5.0, 1.3),
+                (0.1, 0.1),
+                -1.3 / 5.0,
+                np.abs(-1.3 / 5.0) * np.sqrt((0.1 / 5.0) ** 2 + (0.1 / 1.3) ** 2),
+                does_not_raise(),
+            ),
+            ((0.3, 0.0), (0.1, 0.1), 0.0, np.abs(0.1 / 0.3), does_not_raise()),
+            ((0.0, 1.0), (0.1, 0.1), np.nan, np.nan, pytest.warns(RuntimeWarning)),
+        ],
+    )
+    def test_root_solve(self, params, param_errors, root, root_err, conditional):
+        with conditional:
+            ff_obj = self.ff_class(params=params, param_errors=param_errors)
+            results = ff_obj.root_solve()
+
+            if np.all(np.isnan([root, root_err])):
+                assert np.all(np.isnan(results))
+            elif np.isnan(root):
+                assert np.isnan(results[0])
+                assert np.isclose(results[1], root_err)
+            elif np.isnan(root_err):
+                assert np.isclose(results[0], root)
+                assert np.isnan(results[1])
+            else:
+                assert np.allclose(results, [root, root_err])