From d90c541cef90fbee2c77f997c874fd4e8b95aa9f Mon Sep 17 00:00:00 2001 From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> Date: Wed, 5 Jul 2023 19:44:35 -0400 Subject: [PATCH 01/18] Update core.py Add __hash__ so it works with Pandas, Pint, and Pint-Pandas Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> --- uncertainties/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/uncertainties/core.py b/uncertainties/core.py index 8ed7d433..a3b224ef 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -1849,6 +1849,10 @@ def std_dev(self): # Abbreviation (for formulas, etc.): s = std_dev + def __hash__(self): + # Placeholder until we figure out how to really make these hashable + return id(self) + def __repr__(self): # Not putting spaces around "+/-" helps with arrays of # Variable, as each value with an uncertainty is a From 5d429fe50332f23d5f237e83f614c9c80159d969 Mon Sep 17 00:00:00 2001 From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> Date: Fri, 7 Jul 2023 08:58:41 -0400 Subject: [PATCH 02/18] Implement hash invariant Tweak hash function so that when x==y, hash(x)==hash(y) Test case: ``` import uncertainties from uncertainties import ufloat u = ufloat(1.23, 2.34) v = ufloat(1.23, 2.34) print(f"u{u} == v{v}: {u==v}") print(f"hash(u){hash(u)} == hash(v){hash(v)}: {hash(u)==hash(v)}") print(f"u{u} == (u+u)/2{(u+u)/2}: {u==(u+u)/2}") print(f"hash(u){hash(u)} == hash((u+u)/2){hash((u+u)/2)}: {hash(u)==hash((u+u)/2)}") ``` Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> --- uncertainties/core.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/uncertainties/core.py b/uncertainties/core.py index a3b224ef..cbd9ae97 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -1850,8 +1850,13 @@ def std_dev(self): s = std_dev def __hash__(self): - # Placeholder until we figure out how to really make these hashable - return id(self) + if not self._linear_part.expanded(): + self.format('') + combo = tuple(iter(self._linear_part.linear_combo.items())) + if len(combo) > 1 or combo[0][1] != 1.0: + return hash(combo) + # The unique value that comes from a unique variable (which it also hashes to) + return id(combo[0][0]) def __repr__(self): # Not putting spaces around "+/-" helps with arrays of @@ -2825,7 +2830,16 @@ def __hash__(self): # 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) + + # Also, since the _linear_part of a variable is based on self, we can use + # that as a hash (uniqueness of self), which allows us to also + # preserve the invariance that x == y implies hash(x) == hash(y) + if hasattr(self, '_linear_part'): + if self in iter(self._linear_part.linear_combo.keys()): + return id(tuple(iter(self._linear_part.linear_combo.keys()))[0]) + return hash(self._linear_part) + else: + return id(self) def __copy__(self): """ From 40154ce56582cdcacb2693b9fda530246d4359b1 Mon Sep 17 00:00:00 2001 From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> Date: Fri, 7 Jul 2023 22:30:09 -0400 Subject: [PATCH 03/18] Fix pickling (broken by last commit) Ensure that we have a linear_combo attribute before trying to look up its keys. Also re-indent so our conditional test is not too long, and add test case to test_uncertainties. Now test_uncertainties passes all 31 tests original tests plus the new one, and test_umath passes all 9 tests. Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> --- uncertainties/core.py | 11 +++++++---- uncertainties/test_uncertainties.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/uncertainties/core.py b/uncertainties/core.py index cbd9ae97..e26fef5d 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -2835,11 +2835,14 @@ def __hash__(self): # that as a hash (uniqueness of self), which allows us to also # preserve the invariance that x == y implies hash(x) == hash(y) if hasattr(self, '_linear_part'): - if self in iter(self._linear_part.linear_combo.keys()): - return id(tuple(iter(self._linear_part.linear_combo.keys()))[0]) - return hash(self._linear_part) + if ( + hasattr(self._linear_part, 'linear_combo') + and self in iter(self._linear_part.linear_combo.keys()) + ): + return id(tuple(iter(self._linear_part.linear_combo.keys()))[0]) + return hash(self._linear_part) else: - return id(self) + return id(self) def __copy__(self): """ diff --git a/uncertainties/test_uncertainties.py b/uncertainties/test_uncertainties.py index c5ed4ccc..01ba1fdf 100644 --- a/uncertainties/test_uncertainties.py +++ b/uncertainties/test_uncertainties.py @@ -2404,3 +2404,21 @@ def test_correlated_values_correlation_mat(): assert arrays_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) + ''' + + x = ufloat(1.23, 2.34) + y = ufloat(1.23, 2.34) + # nominal values and std_dev terms are equal, but... + assert x.n==y.n and x.s==y.s + # ...x and y are independent variables, therefore not equal as uncertain numbers + assert x != y + assert hash(x) != hash(y) + + # the equation (2x+x)/3 is equal to the variable x, so... + assert ((2*x+x)/3)==x + # ...hash of the equation and the variable should be equal + assert hash((2*x+x)/3)==hash(x) From 634db477c84e3ef28bb69885f66966a83d4bbcce Mon Sep 17 00:00:00 2001 From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> Date: Sat, 8 Jul 2023 15:52:09 -0400 Subject: [PATCH 04/18] Improve efficiency of AffineScalarFunc hash Call `expand` directly if self._linear_part is not yet expanded. Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> --- uncertainties/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uncertainties/core.py b/uncertainties/core.py index e26fef5d..113f3e3a 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -1851,7 +1851,7 @@ def std_dev(self): def __hash__(self): if not self._linear_part.expanded(): - self.format('') + self._linear_part.expand() combo = tuple(iter(self._linear_part.linear_combo.items())) if len(combo) > 1 or combo[0][1] != 1.0: return hash(combo) From 7cca18c49117f358e945672675d6fa1c61a2c6df Mon Sep 17 00:00:00 2001 From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> Date: Tue, 11 Jul 2023 12:37:35 -0400 Subject: [PATCH 05/18] Update appveyor.yml Attempt to work around old version of nose that doesn't work with later versions of Python. Will it work? Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index c9adde86..d3c3f3f4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -47,7 +47,7 @@ install: - conda init cmd.exe - conda info -a # Create a conda virtual environement - - "conda create -n uncty-env numpy nose python=%PYTHON_VERSION%" + - "conda create -n uncty-env numpy nose=1.3.7=py_1006 python=%PYTHON_VERSION%" - activate uncty-env From af3447ccce6cf0523423b81263428afddf52b4b2 Mon Sep 17 00:00:00 2001 From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> Date: Tue, 11 Jul 2023 12:45:20 -0400 Subject: [PATCH 06/18] Revert "Update appveyor.yml" This reverts commit 7cca18c49117f358e945672675d6fa1c61a2c6df. --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index d3c3f3f4..c9adde86 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -47,7 +47,7 @@ install: - conda init cmd.exe - conda info -a # Create a conda virtual environement - - "conda create -n uncty-env numpy nose=1.3.7=py_1006 python=%PYTHON_VERSION%" + - "conda create -n uncty-env numpy nose python=%PYTHON_VERSION%" - activate uncty-env From f3cb61500b8ddcc957a1dcb24c7610c9d43af39f Mon Sep 17 00:00:00 2001 From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> Date: Tue, 11 Jul 2023 12:59:31 -0400 Subject: [PATCH 07/18] Replace nose with pytest Attempt replacing `nose` (which has not been regularly supported for years) with `pytest` for better CI/CD experience. Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> --- INSTALL.txt | 8 +------- appveyor.yml | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/INSTALL.txt b/INSTALL.txt index 1aa398cc..bf3c045e 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -19,10 +19,4 @@ or, if additional access rights are needed (Unix): testing framework. This can be achieved for instance with a command like - nosetests -sv uncertainties/ - -or simply - - nosetests uncertainties/ - -(for a less verbose output). + pytest uncertainties/ diff --git a/appveyor.yml b/appveyor.yml index c9adde86..5833e5aa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -47,7 +47,7 @@ install: - conda init cmd.exe - conda info -a # Create a conda virtual environement - - "conda create -n uncty-env numpy nose python=%PYTHON_VERSION%" + - "conda create -n uncty-env numpy pytest python=%PYTHON_VERSION%" - activate uncty-env diff --git a/setup.py b/setup.py index 1fdce805..f2d7e5bb 100755 --- a/setup.py +++ b/setup.py @@ -339,7 +339,7 @@ install_requires=['future'], - tests_require=['nose', 'numpy'], + tests_require=['pytest', 'numpy'], # Optional dependencies install using: # `easy_install uncertainties[optional]` From 5e40c49496a56e04d0a9d28faa7647c2ad4395af Mon Sep 17 00:00:00 2001 From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> Date: Tue, 11 Jul 2023 13:08:56 -0400 Subject: [PATCH 08/18] Revert "Replace nose with pytest" This reverts commit f3cb61500b8ddcc957a1dcb24c7610c9d43af39f. --- INSTALL.txt | 8 +++++++- appveyor.yml | 2 +- setup.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/INSTALL.txt b/INSTALL.txt index bf3c045e..1aa398cc 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -19,4 +19,10 @@ or, if additional access rights are needed (Unix): testing framework. This can be achieved for instance with a command like - pytest uncertainties/ + nosetests -sv uncertainties/ + +or simply + + nosetests uncertainties/ + +(for a less verbose output). diff --git a/appveyor.yml b/appveyor.yml index 5833e5aa..c9adde86 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -47,7 +47,7 @@ install: - conda init cmd.exe - conda info -a # Create a conda virtual environement - - "conda create -n uncty-env numpy pytest python=%PYTHON_VERSION%" + - "conda create -n uncty-env numpy nose python=%PYTHON_VERSION%" - activate uncty-env diff --git a/setup.py b/setup.py index f2d7e5bb..1fdce805 100755 --- a/setup.py +++ b/setup.py @@ -339,7 +339,7 @@ install_requires=['future'], - tests_require=['pytest', 'numpy'], + tests_require=['nose', 'numpy'], # Optional dependencies install using: # `easy_install uncertainties[optional]` From cd3b7e0fa414c0db485e9f54faf7954982adfdca Mon Sep 17 00:00:00 2001 From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> Date: Tue, 11 Jul 2023 13:12:42 -0400 Subject: [PATCH 09/18] Update appveyor.yml Try running nosetests directly, rather than via setuptools, which trips up nose-1.3.7 for python >= 3.6. See https://github.com/nose-devs/nose/issues/873 Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index c9adde86..d53334aa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -56,4 +56,5 @@ test_script: - "cd C:\\projects\\uncertainties" - activate uncty-env # Activate the virtual environment - python setup.py egg_info - - python setup.py nosetests -sv + # Try to work around nose-1.3.7 not working with modern setuptools (> Python 3.5) + - nosetests -sv uncertainties/ From a2d4bb1a980805a2b3872ce97fabae7356083294 Mon Sep 17 00:00:00 2001 From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> Date: Tue, 11 Jul 2023 13:34:56 -0400 Subject: [PATCH 10/18] Update appveyor.yml Install `future` to satisfy `builtins` for Python 2.7. Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com> --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index d53334aa..c7a697fa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -47,7 +47,7 @@ install: - conda init cmd.exe - conda info -a # Create a conda virtual environement - - "conda create -n uncty-env numpy nose python=%PYTHON_VERSION%" + - "conda create -n uncty-env future numpy nose python=%PYTHON_VERSION%" - activate uncty-env From d74a9d1c9b7a89733b7e401a3e73be4163281458 Mon Sep 17 00:00:00 2001 From: "Nelles, David" Date: Mon, 22 Jan 2024 15:01:39 +0100 Subject: [PATCH 11/18] feat: Added hash function for AffineScalarFunc class --- uncertainties/core.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/uncertainties/core.py b/uncertainties/core.py index 8ed7d433..40dbdc2e 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -1790,6 +1790,17 @@ def __bool__(self): ######################################## + def __hash__(self): + """ + Calculates the hash for any AffineScalarFunc object. + + Returns: + int: The hash of this object + """ + + ids = [id(d) for d in self.derivatives.keys()] + return hash((self._nominal_value, self._linear_part, tuple(ids))) + # Uncertainties handling: def error_components(self): From 3e0b06484afc5e681d5350aa2ecbfe9bb15d5660 Mon Sep 17 00:00:00 2001 From: "Nelles, David" Date: Thu, 25 Jan 2024 13:01:59 +0100 Subject: [PATCH 12/18] fix: Merged hash method from "MichaelTiemannOSC". And created a correct, but simplified method. --- uncertainties/core.py | 50 +++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/uncertainties/core.py b/uncertainties/core.py index 6799c8e5..725854ab 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -1790,16 +1790,18 @@ def __bool__(self): ######################################## - def __hash__(self): + def __hash__(self) -> int: """ 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 """ ids = [id(d) for d in self.derivatives.keys()] - return hash((self._nominal_value, self._linear_part, tuple(ids))) + values_hash = tuple(self.derivatives.values()) + return hash((self._nominal_value, tuple(ids), values_hash)) # Uncertainties handling: @@ -1860,15 +1862,6 @@ def std_dev(self): # Abbreviation (for formulas, etc.): s = std_dev - def __hash__(self): - if not self._linear_part.expanded(): - self._linear_part.expand() - combo = tuple(iter(self._linear_part.linear_combo.items())) - if len(combo) > 1 or combo[0][1] != 1.0: - return hash(combo) - # The unique value that comes from a unique variable (which it also hashes to) - return id(combo[0][0]) - def __repr__(self): # Not putting spaces around "+/-" helps with arrays of # Variable, as each value with an uncertainty is a @@ -2820,6 +2813,22 @@ def std_dev(self, std_dev): self._std_dev = CallableStdDev(std_dev) + def __hash__(self) -> int: + """ + Calculates the hash for any Variable object. + This simply calls the __hash__ method of `AffineScalarFunc` in most cases. + During initialization, the `_linear_part` and `_nominal_value` attributes does not exist. + Because of that, the id returned in case this method is called during initialization. + + Returns: + int: The hash of this object + """ + + if hasattr(self, '_linear_part') and hasattr(self, '_nominal_value'): + return super().__hash__() + else: + return id(self) + # Support for legacy method: def set_std_dev(self, value): # Obsolete deprecation('instead of set_std_dev(), please use' @@ -2836,25 +2845,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__): - - # Also, since the _linear_part of a variable is based on self, we can use - # that as a hash (uniqueness of self), which allows us to also - # preserve the invariance that x == y implies hash(x) == hash(y) - if hasattr(self, '_linear_part'): - if ( - hasattr(self._linear_part, 'linear_combo') - and self in iter(self._linear_part.linear_combo.keys()) - ): - return id(tuple(iter(self._linear_part.linear_combo.keys()))[0]) - return hash(self._linear_part) - else: - return id(self) - def __copy__(self): """ Hook for the standard copy module. From 1e57a2f37c00bf2a1f52957fe1e6f0c633952ed0 Mon Sep 17 00:00:00 2001 From: "Nelles, David" Date: Thu, 25 Jan 2024 13:09:18 +0100 Subject: [PATCH 13/18] refactor: Nose does not like return types. Removed them. --- uncertainties/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uncertainties/core.py b/uncertainties/core.py index 725854ab..d93bcf78 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -1790,7 +1790,7 @@ def __bool__(self): ######################################## - def __hash__(self) -> int: + def __hash__(self): """ Calculates the hash for any AffineScalarFunc object. The hash is calculated from the nominal_value, and the derivatives. @@ -2813,7 +2813,7 @@ def std_dev(self, std_dev): self._std_dev = CallableStdDev(std_dev) - def __hash__(self) -> int: + def __hash__(self): """ Calculates the hash for any Variable object. This simply calls the __hash__ method of `AffineScalarFunc` in most cases. From 4d8d268bd4cf67bacdfde6ee07cbd0081edb1baf Mon Sep 17 00:00:00 2001 From: "Nelles, David" Date: Thu, 4 Apr 2024 12:16:25 +0200 Subject: [PATCH 14/18] fix: hash calculation works now. However, hash equality between Variable and AffineScalarFunction is not always guaranteed. --- uncertainties/core.py | 11 ++++----- uncertainties/test_uncertainties.py | 35 ++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/uncertainties/core.py b/uncertainties/core.py index 6fe1e79a..30ee35f1 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -1765,9 +1765,9 @@ def __hash__(self): int: The hash of this object """ - ids = [id(d) for d in self.derivatives.keys()] - values_hash = tuple(self.derivatives.values()) - return hash((self._nominal_value, tuple(ids), values_hash)) + items = sorted(self.derivatives.items(), key= lambda x: id(x[0])) + ids, values = zip(*map(lambda item: (id(item[0]), item[1]), items)) + return hash((self._nominal_value, tuple(ids), tuple(values))) # Uncertainties handling: @@ -2790,10 +2790,7 @@ def __hash__(self): int: The hash of this object """ - if hasattr(self, '_linear_part') and hasattr(self, '_nominal_value'): - return super().__hash__() - else: - return id(self) + return hash(id(self)) # The following method is overridden so that we can represent the tag: def __repr__(self): diff --git a/uncertainties/test_uncertainties.py b/uncertainties/test_uncertainties.py index 8466beb1..01efba2f 100644 --- a/uncertainties/test_uncertainties.py +++ b/uncertainties/test_uncertainties.py @@ -30,7 +30,7 @@ # Local modules 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 # The following information is useful for making sure that the right @@ -2344,15 +2344,34 @@ def test_hash(): Tests the invariance that if x==y, then hash(x)==hash(y) ''' - x = ufloat(1.23, 2.34) - y = ufloat(1.23, 2.34) + a = ufloat(1.23, 2.34) + b = ufloat(1.23, 2.34) + # nominal values and std_dev terms are equal, but... - assert x.n==y.n and x.s==y.s + assert a.n==b.n and a.s==b.s # ...x and y are independent variables, therefore not equal as uncertain numbers - assert x != y - assert hash(x) != hash(y) + 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*x+x)/3)==x + assert ((2*a+a)/3)==a # ...hash of the equation and the variable should be equal - assert hash((2*x+x)/3)==hash(x) + 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) From 605b5cdb581c7bdbfa431779a1c081f34f6834fd Mon Sep 17 00:00:00 2001 From: "Nelles, David" Date: Thu, 4 Apr 2024 14:39:48 +0200 Subject: [PATCH 15/18] fix: Variabel hash is equal for objects which __eq__ returns True. --- uncertainties/core.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/uncertainties/core.py b/uncertainties/core.py index 30ee35f1..46a9cd8d 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -2742,7 +2742,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: @@ -2752,7 +2752,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 @@ -2789,8 +2789,11 @@ def __hash__(self): Returns: int: The hash of this object """ + # Otherwise, pickles loads does not work + if not hasattr(self, "_nominal_value"): + self._nominal_value = None - return hash(id(self)) + return hash((self._nominal_value, (id(self),), (1.,))) # The following method is overridden so that we can represent the tag: def __repr__(self): From 82285dfc227151b54458a52c4207e8c0e5a699c4 Mon Sep 17 00:00:00 2001 From: "Nelles, David" Date: Fri, 5 Apr 2024 13:04:24 +0200 Subject: [PATCH 16/18] fix: Pickle is able to serialize/deserialize Variables again --- uncertainties/core.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/uncertainties/core.py b/uncertainties/core.py index 46a9cd8d..bcbcf730 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. @@ -1765,9 +1775,8 @@ def __hash__(self): int: The hash of this object """ - items = sorted(self.derivatives.items(), key= lambda x: id(x[0])) - ids, values = zip(*map(lambda item: (id(item[0]), item[1]), items)) - return hash((self._nominal_value, tuple(ids), tuple(values))) + derivatives = sorted([(id(key), value) for key, value in self.derivatives.items()]) + return hash((self._nominal_value, tuple(derivatives))) # Uncertainties handling: @@ -2435,12 +2444,29 @@ def __setstate__(self, data_dict): """ Hook for the pickle module. """ + + LINEAR_PART_NAME = "_linear_part" + try: + linear_part: LinearCombination = data_dict.pop(LINEAR_PART_NAME) + except KeyError: + linear_part = None + for (name, value) in data_dict.items(): # Contrary to the default __setstate__(), this does not # necessarily save to the instance dictionary (because the # instance might contain slots): setattr(self, name, value) + if linear_part is not None: + try: + self_derivative = linear_part.linear_combo.pop(None) + except: + pass + else: + linear_part.linear_combo[self] = self_derivative + + setattr(self, LINEAR_PART_NAME, linear_part) + # Nicer name, for users: isinstance(ufloat(...), UFloat) is # True. Also: isinstance(..., UFloat) is the test for "is this a # number with uncertainties from the uncertainties package?": @@ -2790,10 +2816,8 @@ def __hash__(self): int: The hash of this object """ # Otherwise, pickles loads does not work - if not hasattr(self, "_nominal_value"): - self._nominal_value = None - return hash((self._nominal_value, (id(self),), (1.,))) + return hash((self._nominal_value, ((id(self), 1.),))) # The following method is overridden so that we can represent the tag: def __repr__(self): From 3c64ae307a2a24480c76df6fa8a9996e3516ec00 Mon Sep 17 00:00:00 2001 From: David Nelles Date: Fri, 5 Apr 2024 23:29:04 +0200 Subject: [PATCH 17/18] fix: pickling works now --- uncertainties/core.py | 44 +++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/uncertainties/core.py b/uncertainties/core.py index bcbcf730..5892adc0 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -2445,28 +2445,12 @@ def __setstate__(self, data_dict): Hook for the pickle module. """ - LINEAR_PART_NAME = "_linear_part" - try: - linear_part: LinearCombination = data_dict.pop(LINEAR_PART_NAME) - except KeyError: - linear_part = None - for (name, value) in data_dict.items(): # Contrary to the default __setstate__(), this does not # necessarily save to the instance dictionary (because the # instance might contain slots): setattr(self, name, value) - if linear_part is not None: - try: - self_derivative = linear_part.linear_combo.pop(None) - except: - pass - else: - linear_part.linear_combo[self] = self_derivative - - setattr(self, LINEAR_PART_NAME, linear_part) - # Nicer name, for users: isinstance(ufloat(...), UFloat) is # True. Also: isinstance(..., UFloat) is the test for "is this a # number with uncertainties from the uncertainties package?": @@ -2863,6 +2847,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.}) + ############################################################################### From 185aa0d77b35a1c96b99b9c2455bbff6385c7fbc Mon Sep 17 00:00:00 2001 From: David Nelles Date: Sat, 6 Apr 2024 17:51:22 +0200 Subject: [PATCH 18/18] fix: derivatives with value 0 are filtered out for hash calculation. --- uncertainties/core.py | 15 +++++++++------ uncertainties/test_uncertainties.py | 10 +++++++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/uncertainties/core.py b/uncertainties/core.py index 5892adc0..4cccfa56 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -1775,7 +1775,9 @@ def __hash__(self): int: The hash of this object """ - derivatives = sorted([(id(key), value) for key, value in self.derivatives.items()]) + # 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: @@ -2791,16 +2793,17 @@ def std_dev(self, std_dev): def __hash__(self): """ - Calculates the hash for any Variable object. - This simply calls the __hash__ method of `AffineScalarFunc` in most cases. - During initialization, the `_linear_part` and `_nominal_value` attributes does not exist. - Because of that, the id returned in case this method is called during initialization. + 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 """ - # Otherwise, pickles loads does not work + # 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.),))) # The following method is overridden so that we can represent the tag: diff --git a/uncertainties/test_uncertainties.py b/uncertainties/test_uncertainties.py index 01efba2f..96e3475c 100644 --- a/uncertainties/test_uncertainties.py +++ b/uncertainties/test_uncertainties.py @@ -18,9 +18,9 @@ from builtins import map from builtins import range import copy -import weakref import math from math import isnan, isinf +from uncertainties.umath import cos import random import sys @@ -2375,3 +2375,11 @@ def test_hash(): y = AffineScalarFunc(1, LinearCombination({a:2, b:1})) assert x != y assert hash(x) != hash(y) + + # test for a derivative with value 0 + a = ufloat(0, 0.1) + b = ufloat(1, 0.1) + x = 2 * b + y = 2 * b * cos(a) + x == y + hash(x) == hash(y)