diff --git a/CHANGES.rst b/CHANGES.rst index c4870485..2cd407b3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 ----------------------- diff --git a/doc/user_guide.rst b/doc/user_guide.rst index 0c555a23..ef839a2c 100644 --- a/doc/user_guide.rst +++ b/doc/user_guide.rst @@ -322,7 +322,7 @@ 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``. @@ -330,7 +330,7 @@ For ``x < 0`` the ``y`` derivative is always complex valued so a NaN value is us >>> x = -2 >>> y = ufloat(1, 0.2) ->>> print((x**y).error_components()) +>>> print((x**y)._error_components()) {1.0+/-0.2: nan} Automatic correlations @@ -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 @@ -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 @@ -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 diff --git a/tests/test_power.py b/tests/test_power.py index 82d00e96..0ae9d599 100644 --- a/tests/test_power.py +++ b/tests/test_power.py @@ -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) diff --git a/tests/test_umath.py b/tests/test_umath.py index 95c423b2..1ba6cf29 100644 --- a/tests/test_umath.py +++ b/tests/test_umath.py @@ -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( @@ -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) diff --git a/tests/test_uncertainties.py b/tests/test_uncertainties.py index a4cc3276..4997a2e5 100644 --- a/tests/test_uncertainties.py +++ b/tests/test_uncertainties.py @@ -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( @@ -200,7 +200,7 @@ 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 @@ -208,13 +208,13 @@ def test_copy(): # 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] ) @@ -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] ) @@ -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 @@ -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: @@ -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 @@ -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] ) ######################################## diff --git a/tests/test_unumpy.py b/tests/test_unumpy.py index 701137ff..118c7d72 100644 --- a/tests/test_unumpy.py +++ b/tests/test_unumpy.py @@ -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 ) @@ -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: diff --git a/uncertainties/core.py b/uncertainties/core.py index 1c7ff51e..5a141edf 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -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 @@ -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 @@ -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: @@ -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 @@ -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( ( diff --git a/uncertainties/unumpy/core.py b/uncertainties/unumpy/core.py index 714f1729..cb6f897e 100644 --- a/uncertainties/unumpy/core.py +++ b/uncertainties/unumpy/core.py @@ -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: @@ -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: @@ -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