Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Change Log
===================

Unreleased
-----------------------

Adds:
- Add new internal _derivatives and _error_components properties for internal use without deprecation warning

3.2.4 2026-January-9
-----------------------

Expand Down
12 changes: 6 additions & 6 deletions doc/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -322,15 +322,15 @@ 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())
>>> 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())
>>> print((x**y)._error_components())
{1.0+/-0.2: nan}

Automatic correlations
Expand Down Expand Up @@ -384,7 +384,7 @@ when the variables are **tagged**:
>>> sum_value = u+2*v
>>> sum_value
21.0+/-0.223606797749979
>>> for (var, error) in sum_value.error_components().items():
>>> for (var, error) in sum_value._error_components().items():
... print("{}: {}".format(var.tag, error))
...
v variable: 0.2
Expand All @@ -405,7 +405,7 @@ the total uncertainty of :data:`result` can simply be obtained as:
>>> result = x**y / z
>>> syst_error = math.sqrt(sum( # Error from *all* systematic errors
... error**2
... for (var, error) in result.error_components().items()
... for (var, error) in result._error_components().items()
... if var.tag == "systematic"))
>>> print(format(syst_error, ".3f"))
577.984
Expand Down Expand Up @@ -649,9 +649,9 @@ variables:
>>> u = ufloat(1, 0.1)
>>> v = ufloat(10, 0.1)
>>> sum_value = u+2*v
>>> sum_value.derivatives[u]
>>> sum_value._derivatives[u]
1.0
>>> sum_value.derivatives[v]
>>> sum_value._derivatives[v]
2.0

These values are obtained with a :ref:`fast differentiation algorithm
Expand Down
8 changes: 4 additions & 4 deletions tests/test_power.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,14 @@ def test_pow_deriv_0(x, y, x_deriv_expected, y_deriv_expected):
)
def test_power_derivatives(first_ufloat, second_ufloat, first_der, second_der):
result = pow(first_ufloat, second_ufloat)
first_der_result = result.derivatives[first_ufloat]
second_der_result = result.derivatives[second_ufloat]
first_der_result = result._derivatives[first_ufloat]
second_der_result = result._derivatives[second_ufloat]
assert nan_close(first_der_result, first_der)
assert nan_close(second_der_result, second_der)

result = umath_pow(first_ufloat, second_ufloat)
first_der_result = result.derivatives[first_ufloat]
second_der_result = result.derivatives[second_ufloat]
first_der_result = result._derivatives[first_ufloat]
second_der_result = result._derivatives[second_ufloat]
assert nan_close(first_der_result, first_der)
assert nan_close(second_der_result, second_der)

Expand Down
6 changes: 3 additions & 3 deletions tests/test_umath.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_umath_function_derivatives(func_name, ufloat_tuples):
result = func(*ufloat_arg_list)

for arg_num, arg in enumerate(ufloat_arg_list):
ufloat_deriv_value = result.derivatives[arg]
ufloat_deriv_value = result._derivatives[arg]
numerical_deriv_func = partial_derivative(func, arg_num)
numerical_deriv_value = numerical_deriv_func(*float_arg_list)
assert math.isclose(
Expand Down Expand Up @@ -278,8 +278,8 @@ def test_hypot():
# Derivatives that cannot be calculated simply return NaN, with no
# exception being raised, normally:
result = umath_core.hypot(x, y)
assert isnan(result.derivatives[x])
assert isnan(result.derivatives[y])
assert isnan(result._derivatives[x])
assert isnan(result._derivatives[y])


@pytest.mark.parametrize("function_name", umath_core.deprecated_functions)
Expand Down
30 changes: 15 additions & 15 deletions tests/test_uncertainties.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def test_ufloat_method_derivativs(func_name, ufloat_tuples):
result = bound_func(*ufloat_arg_list[1:])

for arg_num, arg in enumerate(ufloat_arg_list):
ufloat_deriv_value = result.derivatives[arg]
ufloat_deriv_value = result._derivatives[arg]
numerical_deriv_func = partial_derivative(unbound_func, arg_num)
numerical_deriv_value = numerical_deriv_func(*float_arg_list)
assert math.isclose(
Expand Down Expand Up @@ -200,21 +200,21 @@ def test_copy():
y = copy.copy(x)
assert x != y
assert not (x == y)
assert y in y.derivatives.keys() # y must not copy the dependence on x
assert y in y._derivatives.keys() # y must not copy the dependence on x

z = copy.deepcopy(x)
assert x != z

# Copy tests on expressions:
t = x + 2 * z
# t depends on x:
assert x in t.derivatives
assert x in t._derivatives

# The relationship between the copy of an expression and the
# original variables should be preserved:
t_copy = copy.copy(t)
# Shallow copy: the variables on which t depends are not copied:
assert x in t_copy.derivatives
assert x in t_copy._derivatives
assert uncert_core.covariance_matrix([t, z]) == uncert_core.covariance_matrix(
[t_copy, z]
)
Expand All @@ -223,7 +223,7 @@ def test_copy():
# variables should be broken, since the deep copy created new,
# independent variables:
t_deepcopy = copy.deepcopy(t)
assert x not in t_deepcopy.derivatives
assert x not in t_deepcopy._derivatives
assert uncert_core.covariance_matrix([t, z]) != uncert_core.covariance_matrix(
[t_deepcopy, z]
)
Expand All @@ -238,7 +238,7 @@ def test_copy():

gc.collect()

assert y in list(y.derivatives.keys())
assert y in list(y._derivatives.keys())


## Classes for the pickling tests (put at the module level, so that
Expand Down Expand Up @@ -504,17 +504,17 @@ def test_basic_access_to_data():
# Details on the sources of error:
a = ufloat(-1, 0.001)
y = 2 * x + 3 * x + 2 + a
error_sources = y.error_components()
error_sources = y._error_components()
assert len(error_sources) == 2 # 'a' and 'x'
assert error_sources[x] == 0.05
assert error_sources[a] == 0.001

# Derivative values should be available:
assert y.derivatives[x] == 5
assert y._derivatives[x] == 5

# Modification of the standard deviation of variables:
x.std_dev = 1
assert y.error_components()[x] == 5 # New error contribution!
assert y._error_components()[x] == 5 # New error contribution!

# Calculated values with uncertainties should not have a settable
# standard deviation:
Expand Down Expand Up @@ -975,8 +975,8 @@ def f(x, y, *args, **kwargs):
# to try to confuse the code:

assert (
f_wrapped2(x, y, z, t=t).derivatives[y]
== f_auto_unc(x, y, z, t=t).derivatives[y]
f_wrapped2(x, y, z, t=t)._derivatives[y]
== f_auto_unc(x, y, z, t=t)._derivatives[y]
)

# Derivatives supplied through the keyword-parameter dictionary of
Expand All @@ -992,12 +992,12 @@ def f(x, y, *args, **kwargs):
# The derivatives should be exactly the same, because they are
# obtained with the exact same analytic formula:
assert (
f_wrapped3(x, y, z, t=t).derivatives[z]
== f_auto_unc(x, y, z, t=t).derivatives[z]
f_wrapped3(x, y, z, t=t)._derivatives[z]
== f_auto_unc(x, y, z, t=t)._derivatives[z]
)
assert (
f_wrapped3(x, y, z, t=t).derivatives[t]
== f_auto_unc(x, y, z, t=t).derivatives[t]
f_wrapped3(x, y, z, t=t)._derivatives[t]
== f_auto_unc(x, y, z, t=t)._derivatives[t]
)

########################################
Expand Down
8 changes: 4 additions & 4 deletions tests/test_unumpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ def derivatives_close(x, y):
"""

# x and y must depend on the same variables:
if set(x.derivatives) != set(y.derivatives):
if set(x._derivatives) != set(y._derivatives):
return False # Not the same variables

return all(
nan_close(x.derivatives[var], y.derivatives[var]) for var in x.derivatives
nan_close(x._derivatives[var], y._derivatives[var]) for var in x._derivatives
)


Expand Down Expand Up @@ -162,8 +162,8 @@ def test_inverse():

# There are correlations if both the next two derivatives are
# not zero:
assert m_inverse[0, 0].derivatives[x]
assert m_inverse[0, 1].derivatives[x]
assert m_inverse[0, 0]._derivatives[x]
assert m_inverse[0, 1]._derivatives[x]

# Correlations between m and m_inverse should create a perfect
# inversion:
Expand Down
37 changes: 24 additions & 13 deletions uncertainties/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,19 @@ def nominal_value(self):

############################################################

@property
def _derivatives(self):
"""
Private version of `derivatives` for internal use.
"""
if not self._linear_part.expanded():
self._linear_part.expand()
# Attempts to get the contribution of a variable that the
# function does not depend on raise a KeyError:
self._linear_part.linear_combo.default_factory = None

return self._linear_part.linear_combo

# Making derivatives a property gives the user a clean syntax,
# which is consistent with derivatives becoming a dictionary.
@property
Expand All @@ -451,19 +464,11 @@ def derivatives(self):
FutureWarning,
stacklevel=2,
)

if not self._linear_part.expanded():
self._linear_part.expand()
# Attempts to get the contribution of a variable that the
# function does not depend on raise a KeyError:
self._linear_part.linear_combo.default_factory = None

return self._linear_part.linear_combo
return self._derivatives

########################################

# Uncertainties handling:

def error_components(self):
"""
Individual components of the standard deviation of the affine
Expand All @@ -485,10 +490,16 @@ def error_components(self):
stacklevel=2,
)

return self._error_components()

def _error_components(self):
"""
Private version of `error_components` for internal use.
"""
# Calculation of the variance:
error_components = {}

for variable, derivative in self.derivatives.items():
for variable, derivative in self._derivatives.items():
# print "TYPE", type(variable), type(derivative)

# Individual standard error due to variable:
Expand Down Expand Up @@ -523,7 +534,7 @@ def std_dev(self):
# std_dev value (in fact, many intermediate AffineScalarFunc do
# not need to have their std_dev calculated: only the final
# AffineScalarFunc returned to the user does).
return float(sqrt(sum(delta**2 for delta in self.error_components().values())))
return float(sqrt(sum(delta**2 for delta in self._error_components().values())))

# Abbreviation (for formulas, etc.):
s = std_dev
Expand Down Expand Up @@ -920,12 +931,12 @@ def covariance_matrix(nums_with_uncert):

covariance_matrix = []
for i1, expr1 in enumerate(nums_with_uncert, 1):
derivatives1 = expr1.derivatives # Optimization
derivatives1 = expr1._derivatives # Optimization
vars1 = set(derivatives1) # !! Python 2.7+: viewkeys() would work
coefs_expr1 = []

for expr2 in nums_with_uncert[:i1]:
derivatives2 = expr2.derivatives # Optimization
derivatives2 = expr2._derivatives # Optimization
coefs_expr1.append(
sum(
(
Expand Down
6 changes: 3 additions & 3 deletions uncertainties/unumpy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def derivative(u, var):
"""
if isinstance(u, uncert_core.AffineScalarFunc):
try:
return u.derivatives[var]
return u._derivatives[var]
except KeyError:
return 0.0
else:
Expand Down Expand Up @@ -195,7 +195,7 @@ def wrapped_func(arr, *args, **kwargs):
# working with a large number of arrays?
#
# !! set() is only needed for Python 2 compatibility:
variables |= set(element.derivatives.keys())
variables |= set(element._derivatives.keys())

# If the matrix has no variables, then the function value can be
# directly returned:
Expand Down Expand Up @@ -415,7 +415,7 @@ def wrapped_func(array_like, *args, **kwargs):
# floats, etc. might be present
if isinstance(element, uncert_core.AffineScalarFunc):
# !!! set() is only needed for Python 2 compatibility:
variables |= set(element.derivatives.keys())
variables |= set(element._derivatives.keys())

array_nominal = nominal_values(array_version)
# Function value, then derivatives at array_nominal (the
Expand Down