Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement hash for AffineScalarFunc #219

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
39 changes: 38 additions & 1 deletion tests/test_uncertainties.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
85 changes: 76 additions & 9 deletions uncertainties/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,16 @@
"""
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

Check warning on line 1513 in uncertainties/core.py

View check run for this annotation

Codecov / codecov/patch

uncertainties/core.py#L1511-L1513

Added lines #L1511 - L1513 were not covered by tests

def expanded(self):
"""
Return True if and only if the linear combination is expanded.
Expand Down Expand Up @@ -1756,6 +1766,20 @@

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

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):
Expand Down Expand Up @@ -2422,6 +2446,7 @@
"""
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
Expand Down Expand Up @@ -2729,7 +2754,7 @@
# 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:
Expand All @@ -2739,7 +2764,7 @@
# 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

Expand All @@ -2766,6 +2791,27 @@

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'

Check warning on line 2811 in uncertainties/core.py

View check run for this annotation

Codecov / codecov/patch

uncertainties/core.py#L2811

Added line #L2811 was not covered by tests
' .std_dev = ...')
self.std_dev = value

Check warning on line 2813 in uncertainties/core.py

View check run for this annotation

Codecov / codecov/patch

uncertainties/core.py#L2813

Added line #L2813 was not covered by tests

# The following method is overridden so that we can represent the tag:
def __repr__(self):

Expand All @@ -2776,13 +2822,6 @@
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.
Expand Down Expand Up @@ -2817,6 +2856,34 @@

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.})


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

Expand Down
Loading