Repository: PlasmaPy/PlasmaPy
Pull request #959 (2020-12-15 23:03:30)

Patch root_solve() of fit function Linear()
This patch updates the `root_solve()` method of `plasmapy.analysis.fit_function.Linear` to handle the case where the slope is zero.  If this scenario occurs, then there are not finite roots and `(np.nan, np.nan)` is returned.

rocco8773: @namurphy This is ready to merge if you have no other comments.

Patch:
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/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)
 


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])