diff --git a/tests/test_uncertainties.py b/tests/test_uncertainties.py index 46078747..b38721a6 100644 --- a/tests/test_uncertainties.py +++ b/tests/test_uncertainties.py @@ -5,7 +5,7 @@ from math import isnan import uncertainties.core as uncert_core -from uncertainties.core import ufloat, AffineScalarFunc, ufloat_fromstr +from uncertainties.core import ufloat, AffineScalarFunc, ufloat_fromstr, LinearCombination from uncertainties import umath from helpers import (power_special_cases, power_all_cases, power_wrt_ref,numbers_close, ufloats_close, compare_derivatives, uarrays_close) @@ -1935,3 +1935,40 @@ def test_correlated_values_correlation_mat(): assert uarrays_close( numpy.array(cov_mat), numpy.array(uncert_core.covariance_matrix([x2, y2, z2]))) + + def test_hash(): + ''' + Tests the invariance that if x==y, then hash(x)==hash(y) + ''' + + a = ufloat(1.23, 2.34) + b = ufloat(1.23, 2.34) + + # nominal values and std_dev terms are equal, but... + assert a.n==b.n and a.s==b.s + # ...x and y are independent variables, therefore not equal as uncertain numbers + assert a != b + assert hash(a) != hash(b) + + # order of calculation should be irrelevant + assert a + b == b + a + assert hash(a + b) == hash(b + a) + + # the equation (2x+x)/3 is equal to the variable x, so... + assert ((2*a+a)/3)==a + # ...hash of the equation and the variable should be equal + assert hash((2*a+a)/3)==hash(a) + + c = ufloat(1.23, 2.34) + + # the values of the linear combination entries matter + x = AffineScalarFunc(1, LinearCombination({a:1, b:2, c:1})) + y = AffineScalarFunc(1, LinearCombination({a:1, b:2, c:2})) + assert x != y + assert hash(x) != hash(y) + + # the order of linear combination values matter and should not lead to the same hash + x = AffineScalarFunc(1, LinearCombination({a:1, b:2})) + y = AffineScalarFunc(1, LinearCombination({a:2, b:1})) + assert x != y + assert hash(x) != hash(y) diff --git a/uncertainties/core.py b/uncertainties/core.py index a5870dfe..58b58558 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -1502,6 +1502,16 @@ def __bool__(self): """ return bool(self.linear_combo) + def copy(self): + """Shallow copy of the LinearCombination object. + + Returns: + LinearCombination: Copy of the object. + """ + cpy = LinearCombination.__new__(LinearCombination) + cpy.linear_combo = self.linear_combo.copy() + return cpy + def expanded(self): """ Return True if and only if the linear combination is expanded. @@ -1756,6 +1766,20 @@ def __bool__(self): ######################################## + def __hash__(self): + """ + Calculates the hash for any AffineScalarFunc object. + The hash is calculated from the nominal_value, and the derivatives. + + Returns: + int: The hash of this object + """ + + # derivatives which are zero must be filtered out, because the variable is insensitive to errors in those correlations. + # the derivatives must be sorted, because the hash depends on the order, but the equality of variables does not. + derivatives = sorted([(id(key), value) for key, value in self.derivatives.items() if value != 0]) + return hash((self._nominal_value, tuple(derivatives))) + # Uncertainties handling: def error_components(self): @@ -2422,6 +2446,7 @@ def __setstate__(self, data_dict): """ Hook for the pickle module. """ + for (name, value) in data_dict.items(): # Contrary to the default __setstate__(), this does not # necessarily save to the instance dictionary (because the @@ -2729,7 +2754,7 @@ def __init__(self, value, std_dev, tag=None): # differentiable functions: for instance, Variable(3, 0.1)/2 # has a nominal value of 3/2 = 1, but a "shifted" value # of 3.1/2 = 1.55. - value = float(value) + self._nominal_value = float(value) # If the variable changes by dx, then the value of the affine # function that gives its value changes by 1*dx: @@ -2739,7 +2764,7 @@ def __init__(self, value, std_dev, tag=None): # takes much more memory. Thus, this implementation chooses # more cycles and a smaller memory footprint instead of no # cycles and a larger memory footprint. - super(Variable, self).__init__(value, LinearCombination({self: 1.})) + super(Variable, self).__init__(self._nominal_value, LinearCombination({self: 1.})) self.std_dev = std_dev # Assignment through a Python property @@ -2766,6 +2791,27 @@ def std_dev(self, std_dev): self._std_dev = float(std_dev) + def __hash__(self): + """ + Calculates the hash for any `Variable` object. + The implementation is the same as for `AffineScalarFunc`. + But this method sets the `_linear_part` manually. + It is set to a single entry with a self reference as key and 1.0 as value. + + Returns: + int: The hash of this object + """ + + # The manual implementation of the _linear_part is necessary, because pickle would not work otherwise. + # That is because of the self reference inside the _linear_part. + return hash((self._nominal_value, ((id(self), 1.),))) + + # Support for legacy method: + def set_std_dev(self, value): # Obsolete + deprecation('instead of set_std_dev(), please use' + ' .std_dev = ...') + self.std_dev = value + # The following method is overridden so that we can represent the tag: def __repr__(self): @@ -2776,13 +2822,6 @@ def __repr__(self): else: return "< %s = %s >" % (self.tag, num_repr) - def __hash__(self): - # All Variable objects are by definition independent - # variables, so they never compare equal; therefore, their - # id() are allowed to differ - # (http://docs.python.org/reference/datamodel.html#object.__hash__): - return id(self) - def __copy__(self): """ Hook for the standard copy module. @@ -2817,6 +2856,34 @@ def __deepcopy__(self, memo): return self.__copy__() + def __getstate__(self): + """ + Hook for the pickle module. + + Same as for the AffineScalarFunction but remove the linear part, + since it only contains a self reference. + This would lead to problems when unpickling the linear part. + """ + + LINEAR_PART_NAME = "_linear_part" + state = super().__getstate__() + + if LINEAR_PART_NAME in state: + del state[LINEAR_PART_NAME] + + return state + + def __setstate__(self, state): + """ + Hook for the pickle module. + + Same as for AffineScalarFunction, but manually set the linear part. + This one is removed when pickling Variable objects. + """ + + super().__setstate__(state) + self._linear_part = LinearCombination({self: 1.}) + ###############################################################################