Repository: lmfit/uncertainties
Pull request #294: Refactor power derivative functions
Author: login: jagerber48
Created: 2025-03-14T22:10:11Z | Merged: 2025-04-17T12:54:06Z

- [x] Closes #293
- [x] Executed `pre-commit run --all-files` with no errors
- [x] The change is fully covered by automated unit tests
- [x] Documented in docs/ as appropriate
- [x] Added an entry to the CHANGES file

`uncertainties` allows calculating `x**y` where one or both of `x` and `y` are `UFloat` objects. For this we need to calculate the `x` and `y` derivatives of `x**y`. This PR refactors the functions that calculate those derivatives to more explicitly and clearly handle non-typical cases. Moving those functions to the module level also allows these functions to be directly tested.

Linked issue #293: Clean up power derivative calculations
`uncertainties` calculates the `x` and `y` derivatives of `pow(x, y) = x**y` using the `pow_deriv_0` and `pow_deriv_1` local functions defined in `get_ops_with_reflection` in `ops.py`.

https://github.com/lmfit/uncertainties/blob/19865cc0e87558d1d105c9d674ebace501e41395/uncertainties/ops.py#L131-L143

My issues with `pow_deriv_0`:

- It returns complex results for `x<0` with non-integer `y` even though `uncertainties` only supports real values. I would prefer it to return `nan` or raise a `ValueError` (weak preference for returning `nan`).
- For `x=0`, `y<0` and `y` an integer it raises a `ZeroDivisionError`, but for `x=0`, `y<0` and `y` a non-integer it returns `nan`. I think for `x=0` and `y<0` this function should always return `nan` or should always raise an exception (weak preference for returning `nan`).

My issues with `pow_deriv_1`:

- It raises a `ValueError` for all `x<0`. This is different than `pow_deriv_0` which returns complex results for this case. It should have the same behavior as `pow_deriv_0`, either return `nan` (my weak preference) or raise a `ValueError`.
- For `x=0` and `y<=0` it always raises `ValueError`. This is ok but again it's not consistent with the `pow_deriv_0` behavior. The two should be consistent, my preference would be to return `nan` or raise `ValueError`.

Other issues with both:
 
- But one big issue with both of these is that when the `ValueError` is raised, it is only raised as a consequence of the derivative calculation and is raised by built-in functions. It would be preferable to explicitly slice up the domain and handle all cases explicitly rather than relying on implicit behavior of the functions involved.
- They are both defined locally in the scope of another function `get_ops_with_reflection` in `ops.py`. They should be defined top-level in the module. This will allow direct testing to help clarify these edge cases.

Diff:
diff --git a/CHANGES.rst b/CHANGES.rst
index cee272e2..529ac3c2 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -12,6 +12,8 @@ Changes
    time. Now such a function can be imported but if the user attempts to
    execute it, a `NotImplementedError` is raised indicating that the
    function can't be used because `numpy` couldn't be imported.
+- Refactored the implementation for the calculation of the derivatives of the power
+   function and improves the corresponding testing.
 
 Adds:
 
diff --git a/doc/user_guide.rst b/doc/user_guide.rst
index 9226bd9a..8fee187a 100644
--- a/doc/user_guide.rst
+++ b/doc/user_guide.rst
@@ -271,6 +271,64 @@ To check whether the uncertainty is NaN or Inf, use one of :func:`math.isnan`,
 .. index:: correlations; detailed example
 
 
+Power Function Behavior
+=======================
+
+The value of one :class:`UFloat` raised to the power of another can be calculated in two
+ways:
+
+>>> from uncertainties import umath
+>>>
+>>> x = ufloat(4.5, 0.2)
+>>> y = ufloat(3.4, 0.4)
+>>> print(x**y)
+(1.7+/-1.0)e+02
+>>> print(umath.pow(x, y))
+(1.7+/-1.0)e+02
+
+The function ``x**y`` is defined for all ``x != 0`` and for ``x == 0`` as long as
+``y > 0``.
+There is not a unique definition for ``0**0``, however python takes the convention for
+:class:`float` that ``0**0 == 1``.
+If the power operation is performed on an ``(x, y)`` pair for which ``x**y`` is
+undefined then an exception will be raised:
+
+>>> x = ufloat(0, 0.2)
+>>> y = ufloat(-3.4, 0.4)
+>>> print(x**y)
+Traceback (most recent call last):
+ ...
+ZeroDivisionError: 0.0 cannot be raised to a negative power
+
+On the domain where it is defined, ``x**y`` is always real for ``x >= 0``.
+For ``x < 0`` it is real for all integer values of ``y``.
+If ``x<0`` and ``y`` is not an integer then ``x**y`` has a non-zero imaginary component.
+The :mod:`uncertainties` module does not handle complex values:
+
+>>> x = ufloat(-4.5, 0.2)
+>>> y = ufloat(-3.4, 0.4)
+>>> print(x**y)
+Traceback (most recent call last):
+ ...
+ValueError: The uncertainties module does not handle complex results
+
+The ``x`` derivative is real anywhere ``x**y`` is real except along ``x==0`` for
+non-integer ``y``.
+At these points the ``x`` derivative would be complex so a NaN value is used:
+
+>>> x = ufloat(0, 0.2)
+>>> y=1.5
+>>> print((x**y).error_components())
+{0.0+/-0.2: nan}
+
+The ``y`` derivative is real anywhere ``x**y`` is real as long as ``x>=0``.
+For ``x < 0`` the ``y`` derivative is always complex valued so a NaN value is used:
+
+>>> x = -2
+>>> y = ufloat(1, 0.2)
+>>> print((x**y).error_components())
+{1.0+/-0.2: nan}
+
 Automatic correlations
 ======================
 
diff --git a/tests/test_power.py b/tests/test_power.py
index f89f38e2..82d00e96 100644
--- a/tests/test_power.py
+++ b/tests/test_power.py
@@ -3,11 +3,40 @@
 import pytest
 
 from uncertainties import ufloat
+from uncertainties.ops import pow_deriv_0, pow_deriv_1
 from uncertainties.umath_core import pow as umath_pow
 
 from helpers import nan_close
 
 
+pow_deriv_cases = [
+    (0.5, 2, 1.0, -0.17328679513998632),
+    (0.5, 1.5, 1.0606601717798214, -0.2450645358671368),
+    (0.5, 0, 0.0, -0.6931471805599453),
+    (0.5, -1.5, -8.485281374238571, -1.9605162869370945),
+    (0.5, -2, -16.0, -2.772588722239781),
+    (0, 2, 0, 0),
+    (0, 1.5, float("nan"), 0),
+    (0, 0, 0, float("nan")),
+    (0, -0.5, float("nan"), float("nan")),
+    (0, -2, float("nan"), float("nan")),
+    (-0.5, 2, -1.0, float("nan")),
+    (-0.5, 1.5, float("nan"), float("nan")),
+    (-0.5, 0, -0.0, float("nan")),
+    (-0.5, -1.5, float("nan"), float("nan")),
+    (-0.5, -2, 16.0, float("nan")),
+]
+
+
+@pytest.mark.parametrize("x, y, x_deriv_expected, y_deriv_expected", pow_deriv_cases)
+def test_pow_deriv_0(x, y, x_deriv_expected, y_deriv_expected):
+    x_deriv_actual = pow_deriv_0(x, y)
+    assert nan_close(x_deriv_actual, x_deriv_expected)
+
+    y_deriv_actual = pow_deriv_1(x, y)
+    assert nan_close(y_deriv_actual, y_deriv_expected)
+
+
 zero = ufloat(0, 0.1)
 zero2 = ufloat(0, 0.1)
 one = ufloat(1, 0.1)
diff --git a/uncertainties/ops.py b/uncertainties/ops.py
index d3904599..53e64c85 100644
--- a/uncertainties/ops.py
+++ b/uncertainties/ops.py
@@ -64,6 +64,28 @@ def wrapped_f(*args, **kwargs):
     return wrapped_f
 
 
+def pow_deriv_0(x, y):
+    """
+    The formula below works if x is positive or if y is an integer and x is negative
+    of y is an integer, x is zero and y is greater than or equal to 1.
+    """
+    if x > 0 or (y % 1 == 0 and (x < 0 or y >= 1)):
+        return y * x ** (y - 1)
+    elif x == 0 and y == 0:
+        return 0
+    else:
+        return float("nan")
+
+
+def pow_deriv_1(x, y):
+    if x > 0:
+        return log(x) * x**y
+    elif x == 0 and y > 0:
+        return 0
+    else:
+        return float("nan")
+
+
 def get_ops_with_reflection():
     """
     Return operators with a reflection, along with their partial derivatives.
@@ -119,29 +141,6 @@ def get_ops_with_reflection():
             eval("lambda y, x: %s" % expr) for expr in reversed(derivatives)
         ]
 
-    # The derivatives of pow() are more complicated:
-
-    # The case x**y is constant one the line x = 0 and in y = 0;
-    # the corresponding derivatives must be zero in these
-    # cases. If the function is actually not defined (e.g. 0**-3),
-    # then an exception will be raised when the nominal value is
-    # calculated.  These derivatives are transformed to NaN if an
-    # error happens during their calculation:
-
-    def pow_deriv_0(x, y):
-        if y == 0:
-            return 0.0
-        elif x != 0 or y % 1 == 0:
-            return y * x ** (y - 1)
-        else:
-            return float("nan")
-
-    def pow_deriv_1(x, y):
-        if x == 0 and y > 0:
-            return 0.0
-        else:
-            return log(x) * x**y
-
     ops_with_reflection["pow"] = [pow_deriv_0, pow_deriv_1]
     ops_with_reflection["rpow"] = [
         lambda y, x: pow_deriv_1(x, y),


Reviews:
user: login: newville
body: Looks good to me.  Thanks for adding those tests.
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-05T16:49:04Z
user: login: andrewgsavage
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T18:53:37Z
user: login: jagerber48
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T19:55:03Z
user: login: andrewgsavage
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T13:02:33Z
user: login: jagerber48
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T20:28:33Z
user: login: newville
body: @jagerber48 @andrewgsavage  This looks good to me...  Any objections to merging?

state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-17T02:47:42Z

Review comments:
diff_hunk: @@ -119,29 +141,6 @@ def get_ops_with_reflection():
             eval("lambda y, x: %s" % expr) for expr in reversed(derivatives)
         ]
 
-    # The derivatives of pow() are more complicated:
-
-    # The case x**y is constant one the line x = 0 and in y = 0;
path: uncertainties/ops.py
user: login: andrewgsavage
body: it's a shame to lose this comment. is there somewhere it could be moved to? I would say a docstring but that's not useful for `__pow__`.

maybe it could be added to `umath.pow`'s docstring?
created_at: 2025-04-05T18:53:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,29 +141,6 @@ def get_ops_with_reflection():
             eval("lambda y, x: %s" % expr) for expr in reversed(derivatives)
         ]
 
-    # The derivatives of pow() are more complicated:
-
-    # The case x**y is constant one the line x = 0 and in y = 0;
path: uncertainties/ops.py
user: login: jagerber48
body: Let me take a look. I actually spent a VERY long time puzzling over various special cases and I had some rewrites of this same comment.

Part of the problem is that the old function handled different special cases using different approaches. For example, the old function allowed complex results to be returned but, instead, relied on some other part of the codebase to prevent complex results.

Under this PR the comment is not actually correct anymore but I can come up with replacement text. But I'm curious, do you want a comment for the developer's sake (like the old comment used to be) or for the user's sake? For the latter we can add some description under the `umath.pow` docstring. We can also add documentation to the docs webpage. If we want a comment for the developers sake then I can update the old comments with some notes about the various special cases and how they're handled.

Also happy to do all of the above, though it ends up being a lot of text because there's a few different cases.
created_at: 2025-04-05T19:55:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,29 +141,6 @@ def get_ops_with_reflection():
             eval("lambda y, x: %s" % expr) for expr in reversed(derivatives)
         ]
 
-    # The derivatives of pow() are more complicated:
-
-    # The case x**y is constant one the line x = 0 and in y = 0;
path: uncertainties/ops.py
user: login: andrewgsavage
body: I think it's most useful for users so a docstring or section of the documentation, either works.
created_at: 2025-04-06T13:02:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +271,65 @@ To check whether the uncertainty is NaN or Inf, use one of :func:`math.isnan`,
 .. index:: correlations; detailed example
 
 
+Power Function Behavior
+=======================
+
+The value of one :class:`UFloat` raised to the power of another can be calculated in two
+ways:
+
+>>> from uncertainties import umath
+>>>
+>>> x = ufloat(4.5, 0.2)
+>>> y = ufloat(3.4, 0.4)
+>>> print(x**y)
+(1.7+/-1.0)e+02
+>>> print(umath.pow(x, y))
+(1.7+/-1.0)e+02
+
+The function ``x**y`` is defined for all ``x != 0`` and for ``x == 0`` as long as
+``y > 0``.
+There is not a unique definition for ``0**0``, however python takes the convention for
+:class:`float` that ``0**0 == 1``.
+If the power operation is performed on an ``(x, y)`` pair for which ``x**y`` is
+undefined then an exception will be raised:
+
+>>> x = ufloat(0, 0.2)
+>>> y = ufloat(-3.4, 0.4)
+>>> print(x**y)
+Traceback (most recent call last):
+ ...
+ZeroDivisionError: 0.0 cannot be raised to a negative power
+
+On the domain where it is defined, ``x**y`` is real valued for ``x >= 0`` and for all
path: doc/user_guide.rst
user: login: jagerber48
body: This should be 

> On the domain where it is defined, `x**y` is always real if `x>=0`. If `x<0` then `x**y` is real as long as `y` is an integer.
created_at: 2025-04-11T20:27:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -271,6 +271,65 @@ To check whether the uncertainty is NaN or Inf, use one of :func:`math.isnan`,
 .. index:: correlations; detailed example
 
 
+Power Function Behavior
+=======================
+
+The value of one :class:`UFloat` raised to the power of another can be calculated in two
+ways:
+
+>>> from uncertainties import umath
+>>>
+>>> x = ufloat(4.5, 0.2)
+>>> y = ufloat(3.4, 0.4)
+>>> print(x**y)
+(1.7+/-1.0)e+02
+>>> print(umath.pow(x, y))
+(1.7+/-1.0)e+02
+
+The function ``x**y`` is defined for all ``x != 0`` and for ``x == 0`` as long as
+``y > 0``.
+There is not a unique definition for ``0**0``, however python takes the convention for
+:class:`float` that ``0**0 == 1``.
+If the power operation is performed on an ``(x, y)`` pair for which ``x**y`` is
+undefined then an exception will be raised:
+
+>>> x = ufloat(0, 0.2)
+>>> y = ufloat(-3.4, 0.4)
+>>> print(x**y)
+Traceback (most recent call last):
+ ...
+ZeroDivisionError: 0.0 cannot be raised to a negative power
+
+On the domain where it is defined, ``x**y`` is real valued for ``x >= 0`` and for all
+integer values of ``y``.
+If ``x<0`` and ``y`` is not an integer then ``x**y`` has a non-zero imaginary component.
+The :mod:`uncertainties` module does not handle complex values:
+
+>>> x = ufloat(-4.5, 0.2)
+>>> y = ufloat(-3.4, 0.4)
+>>> print(x**y)
+Traceback (most recent call last):
+ ...
+ValueError: The uncertainties module does not handle complex results
+
+The ``x`` derivative is real anywhere ``x**y`` is real except along ``x==0`` for
+non-integer ``y``.
+At these points the ``x`` derivative would be complex so a NaN value is used:
+
+>>> x = ufloat(0, 0.2)
+>>> y=1.5
+>>> print((x**y).error_components())
+{0.0+/-0.2: nan}
+
+The ``y`` derivative is real anywhere ``x**y`` is real as long as ``x>=0``.
+For ``x < 0`` the ``y`` derivative is always complex valued so a NaN value is used:
+returns a NaN value.
path: doc/user_guide.rst
user: login: jagerber48
body: Typo
created_at: 2025-04-11T20:28:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-14T22:10:57Z
body: ## [Codecov](https://app.codecov.io/gh/lmfit/uncertainties/pull/294?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 86.54%. Comparing base [(`3bd3b9d`)](https://app.codecov.io/gh/lmfit/uncertainties/commit/3bd3b9d38dc9a5ca11214df5f33a7d45c086f00d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) to head [(`f10d77a`)](https://app.codecov.io/gh/lmfit/uncertainties/commit/f10d77a934c622b3c275086df669b4a6083e4060?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit).
> Report is 12 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #294      +/-   ##
==========================================
+ Coverage   86.47%   86.54%   +0.07%     
==========================================
  Files          18       18              
  Lines        1893     1903      +10     
==========================================
+ Hits         1637     1647      +10     
  Misses        256      256              
```

| [Flag](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | Coverage Δ | |
|---|---|---|
| [macos-latest-3.10](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [macos-latest-3.11](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [macos-latest-3.12](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [macos-latest-3.8](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [macos-latest-3.9](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [no-numpy](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `78.71% <100.00%> (+0.11%)` | :arrow_up: |
| [ubuntu-latest-3.10](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [ubuntu-latest-3.11](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [ubuntu-latest-3.12](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [ubuntu-latest-3.8](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [ubuntu-latest-3.9](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [windows-latest-3.10](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [windows-latest-3.11](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [windows-latest-3.12](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [windows-latest-3.8](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |
| [windows-latest-3.9](https://app.codecov.io/gh/lmfit/uncertainties/pull/294/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.58% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/lmfit/uncertainties/pull/294?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: codspeed-hq[bot]
created_at: 2025-03-14T22:14:18Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/lmfit/uncertainties/branches/jagerber48%3Apow_derivs_funcs)

### Merging #294 will **not alter performance**

<sub>Comparing <code>jagerber48:pow_derivs_funcs</code> (f10d77a) with <code>master</code> (3bd3b9d)</sub>



### Summary

`✅ 5` untouched benchmarks  




author_association: NONE
user: login: jagerber48
created_at: 2025-04-10T05:59:56Z
body: @andrewgsavage @newville I've included words and codeblocks in the user guide documentation near the nan/inf discussion. Let me know what you think. There are more cases that could be demonstrated. I also intentionally left out examples with `std_dev=0` because those confuse the story greatly re #283. 

Are more cases needed to show the different situations? Do we want any of this to appear on the docstring for `umath.pow()`?

I'm relatively happy where it is, but open to suggestions.
author_association: CONTRIBUTOR
user: login: andrewgsavage
created_at: 2025-04-11T19:10:19Z
body: seems good
author_association: CONTRIBUTOR
user: login: jagerber48
created_at: 2025-04-12T03:13:59Z
body: Cleaned it up a little more. Think it's ready for approval and merging if others agree.
author_association: CONTRIBUTOR
user: login: jagerber48
created_at: 2025-04-17T02:28:38Z
body: Can someone please approve and merge #302 and then merge this PR? @newville @andrewgsavage? After that we are ready to release version `3.2.3` and begin work on the `4.0.0` release.
author_association: CONTRIBUTOR
user: login: andrewgsavage
created_at: 2025-04-17T11:21:27Z
body: go for it
author_association: CONTRIBUTOR