From 4b885f5bc0d7422d234272090ff15bc8c7e0f3ab Mon Sep 17 00:00:00 2001 From: Varchas Gopalaswamy Date: Mon, 5 Jun 2023 14:14:49 -0400 Subject: [PATCH 1/7] rename the first positional arg in _trapz to match numpy --- pint/facets/numpy/numpy_func.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index f9f64f329..b3a549e89 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -741,23 +741,23 @@ def _base_unit_if_needed(a): @implements("trapz", "function") -def _trapz(a, x=None, dx=1.0, **kwargs): - a = _base_unit_if_needed(a) - units = a.units +def _trapz(y, x=None, dx=1.0, **kwargs): + y = _base_unit_if_needed(y) + units = y.units if x is not None: if hasattr(x, "units"): x = _base_unit_if_needed(x) units *= x.units x = x._magnitude - ret = np.trapz(a._magnitude, x, **kwargs) + ret = np.trapz(y._magnitude, x, **kwargs) else: if hasattr(dx, "units"): dx = _base_unit_if_needed(dx) units *= dx.units dx = dx._magnitude - ret = np.trapz(a._magnitude, dx=dx, **kwargs) + ret = np.trapz(y._magnitude, dx=dx, **kwargs) - return a.units._REGISTRY.Quantity(ret, units) + return y.units._REGISTRY.Quantity(ret, units) def implement_mul_func(func): From 5bad30a334484c97c01d64b37154d31a435f96d0 Mon Sep 17 00:00:00 2001 From: Varchas Gopalaswamy Date: Mon, 5 Jun 2023 18:19:47 -0400 Subject: [PATCH 2/7] added facet for auto uncertainties --- pint/compat.py | 9 +++++ pint/facets/__init__.py | 3 ++ pint/facets/uncertainties/__init__.py | 57 +++++++++++++++++++++++++++ pint/registry.py | 3 ++ pyproject.toml | 1 + 5 files changed, 73 insertions(+) create mode 100644 pint/facets/uncertainties/__init__.py diff --git a/pint/compat.py b/pint/compat.py index 6be906f4d..89a681544 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -155,6 +155,15 @@ def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): ufloat = None HAS_UNCERTAINTIES = False + +try: + from auto_uncertainties import Uncertainty + + HAS_AUTOUNCERTAINTIES = True +except ImportError: + Uncertainty = None + HAS_AUTOUNCERTAINTIES = False + try: from babel import Locale from babel import units as babel_units diff --git a/pint/facets/__init__.py b/pint/facets/__init__.py index 4fd1597a6..8d5c8e2ed 100644 --- a/pint/facets/__init__.py +++ b/pint/facets/__init__.py @@ -83,6 +83,7 @@ class that belongs to a registry that has NumpyRegistry as one of its bases. from .numpy import NumpyRegistry, GenericNumpyRegistry from .plain import PlainRegistry, GenericPlainRegistry, QuantityT, UnitT, MagnitudeT from .system import SystemRegistry, GenericSystemRegistry +from .uncertainties import UncertaintyRegistry, GenericUncertaintyRegistry __all__ = [ "ContextRegistry", @@ -106,4 +107,6 @@ class that belongs to a registry that has NumpyRegistry as one of its bases. "QuantityT", "UnitT", "MagnitudeT", + "UncertaintyRegistry", + "GenericUncertaintyRegistry", ] diff --git a/pint/facets/uncertainties/__init__.py b/pint/facets/uncertainties/__init__.py new file mode 100644 index 000000000..7905c2a8d --- /dev/null +++ b/pint/facets/uncertainties/__init__.py @@ -0,0 +1,57 @@ +""" + pint.facets.Uncertainty + ~~~~~~~~~~~~~~~~ + + Adds pint the capability to interoperate with Uncertainty + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + + +from __future__ import annotations + +from typing import Generic, Any + +from ...compat import Uncertainty, TypeAlias +from ..plain import ( + GenericPlainRegistry, + PlainQuantity, + QuantityT, + UnitT, + PlainUnit, + MagnitudeT, +) + + +class UncertaintyQuantity(Generic[MagnitudeT], PlainQuantity[MagnitudeT]): + @property + def value(self): + if isinstance(self._magnitude, Uncertainty): + return self._magnitude.value * self.units + else: + return self._magnitude * self.units + + @property + def error(self): + if isinstance(self._magnitude, Uncertainty): + return self._magnitude.error * self.units + else: + return (0 * self._magnitude) * self.units + + +class UncertaintyUnit(PlainUnit): + pass + + +class GenericUncertaintyRegistry( + Generic[QuantityT, UnitT], GenericPlainRegistry[QuantityT, UnitT] +): + pass + + +class UncertaintyRegistry( + GenericUncertaintyRegistry[UncertaintyQuantity[Any], UncertaintyUnit] +): + Quantity: TypeAlias = UncertaintyQuantity[Any] + Unit: TypeAlias = UncertaintyUnit diff --git a/pint/registry.py b/pint/registry.py index e978e3698..a5ca4726a 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -30,6 +30,7 @@ class Quantity( facets.SystemRegistry.Quantity, facets.ContextRegistry.Quantity, + facets.UncertaintyRegistry.Quantity, facets.DaskRegistry.Quantity, facets.NumpyRegistry.Quantity, facets.MeasurementRegistry.Quantity, @@ -43,6 +44,7 @@ class Quantity( class Unit( facets.SystemRegistry.Unit, facets.ContextRegistry.Unit, + facets.UncertaintyRegistry.Unit, facets.DaskRegistry.Unit, facets.NumpyRegistry.Unit, facets.MeasurementRegistry.Unit, @@ -57,6 +59,7 @@ class GenericUnitRegistry( Generic[facets.QuantityT, facets.UnitT], facets.GenericSystemRegistry[facets.QuantityT, facets.UnitT], facets.GenericContextRegistry[facets.QuantityT, facets.UnitT], + facets.GenericUncertaintyRegistry[facets.QuantityT, facets.UnitT], facets.GenericDaskRegistry[facets.QuantityT, facets.UnitT], facets.GenericNumpyRegistry[facets.QuantityT, facets.UnitT], facets.GenericMeasurementRegistry[facets.QuantityT, facets.UnitT], diff --git a/pyproject.toml b/pyproject.toml index 6094bd06d..d9ba57513 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ pandas = ["pint-pandas >= 0.3"] xarray = ["xarray"] dask = ["dask"] mip = ["mip >= 1.13"] +auto_uncertainties = ["auto-uncertainties >= 0.4.0"] [project.urls] Homepage = "https://github.com/hgrecco/pint" From 61c5fd04fcd7408b3251267a77df8a54ee4ccbaa Mon Sep 17 00:00:00 2001 From: Varchas Gopalaswamy Date: Mon, 5 Jun 2023 18:58:27 -0400 Subject: [PATCH 3/7] fixed isinstance comparison --- pint/facets/uncertainties/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pint/facets/uncertainties/__init__.py b/pint/facets/uncertainties/__init__.py index 7905c2a8d..5af86e982 100644 --- a/pint/facets/uncertainties/__init__.py +++ b/pint/facets/uncertainties/__init__.py @@ -13,7 +13,7 @@ from typing import Generic, Any -from ...compat import Uncertainty, TypeAlias +from ...compat import TypeAlias, HAS_AUTOUNCERTAINTIES from ..plain import ( GenericPlainRegistry, PlainQuantity, @@ -23,6 +23,11 @@ MagnitudeT, ) +if HAS_AUTOUNCERTAINTIES: + from auto_uncertainties import Uncertainty +else: + Uncertainty = None + class UncertaintyQuantity(Generic[MagnitudeT], PlainQuantity[MagnitudeT]): @property From e49b89435c1103337d91695062fadc6b502802b5 Mon Sep 17 00:00:00 2001 From: Varchas Gopalaswamy Date: Sat, 10 Jun 2023 14:24:29 -0400 Subject: [PATCH 4/7] added parsing, tests and a way to add errors directly --- pint/compat.py | 9 ++-- pint/facets/uncertainties/__init__.py | 17 ++++--- pint/pint_eval.py | 15 +++++- pint/testsuite/helpers.py | 5 ++ pint/testsuite/test_pint_eval.py | 6 +++ pint/testsuite/test_uncertainty.py | 68 +++++++++++++++++++++++++++ pint/util.py | 4 ++ 7 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 pint/testsuite/test_uncertainty.py diff --git a/pint/compat.py b/pint/compat.py index 89a681544..2bc1f14d3 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -158,12 +158,15 @@ def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): try: from auto_uncertainties import Uncertainty - - HAS_AUTOUNCERTAINTIES = True except ImportError: - Uncertainty = None HAS_AUTOUNCERTAINTIES = False + class Uncertainty(object): + ... + +else: + HAS_AUTOUNCERTAINTIES = True + try: from babel import Locale from babel import units as babel_units diff --git a/pint/facets/uncertainties/__init__.py b/pint/facets/uncertainties/__init__.py index 5af86e982..c8c1968ce 100644 --- a/pint/facets/uncertainties/__init__.py +++ b/pint/facets/uncertainties/__init__.py @@ -13,7 +13,7 @@ from typing import Generic, Any -from ...compat import TypeAlias, HAS_AUTOUNCERTAINTIES +from ...compat import TypeAlias, Uncertainty from ..plain import ( GenericPlainRegistry, PlainQuantity, @@ -23,11 +23,6 @@ MagnitudeT, ) -if HAS_AUTOUNCERTAINTIES: - from auto_uncertainties import Uncertainty -else: - Uncertainty = None - class UncertaintyQuantity(Generic[MagnitudeT], PlainQuantity[MagnitudeT]): @property @@ -44,6 +39,16 @@ def error(self): else: return (0 * self._magnitude) * self.units + def plus_minus(self, err): + from auto_uncertainties import nominal_values, std_devs + + my_value = nominal_values(self._magnitude) * self.units + my_err = std_devs(self._magnitude) * self.units + + new_err = (my_err**2 + err**2) ** 0.5 + + return Uncertainty.from_quantities(my_value, new_err) + class UncertaintyUnit(PlainUnit): pass diff --git a/pint/pint_eval.py b/pint/pint_eval.py index a2952ecda..c0090aeeb 100644 --- a/pint/pint_eval.py +++ b/pint/pint_eval.py @@ -17,8 +17,13 @@ from .errors import DefinitionSyntaxError +try: + from auto_uncertainties import Uncertainty +except ImportError: + Uncertainty = None # For controlling order of operations _OP_PRIORITY = { + "±": 4, "**": 3, "^": 3, "unary": 2, @@ -32,6 +37,12 @@ } +def _uncertainty(left: float, right: float) -> Uncertainty: + if Uncertainty is None: + raise ImportError("auto_uncertainties is required for uncertainty calculations") + return Uncertainty.from_quantities(left, right) + + def _power(left: Any, right: Any) -> Any: from . import Quantity from .compat import is_duck_array @@ -60,6 +71,7 @@ def _power(left: Any, right: Any) -> Any: _UNARY_OPERATOR_MAP: dict[str, UnaryOpT] = {"+": lambda x: x, "-": lambda x: x * -1} _BINARY_OPERATOR_MAP: dict[str, BinaryOpT] = { + "±": _uncertainty, "**": _power, "*": operator.mul, "": operator.mul, # operator for implicit ops @@ -202,7 +214,7 @@ def _build_eval_tree( token_type = current_token.type token_text = current_token.string - if token_type == tokenlib.OP: + if token_type == tokenlib.OP or token_text == "±": if token_text == ")": if prev_op == "": raise DefinitionSyntaxError( @@ -229,6 +241,7 @@ def _build_eval_tree( else: # get first token result = right + # This means it's an operator in op_priority elif token_text in op_priority: if result: # equal-priority operators are grouped in a left-to-right order, diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 191f4c3f5..137476911 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -15,6 +15,7 @@ HAS_NUMPY, HAS_NUMPY_ARRAY_FUNCTION, HAS_UNCERTAINTIES, + HAS_AUTOUNCERTAINTIES, NUMPY_VER, ) @@ -128,6 +129,10 @@ def requires_numpy_at_least(version): requires_not_babel = pytest.mark.skipif( HAS_BABEL, reason="Requires Babel not to be installed" ) +requires_autouncertainties = pytest.mark.skipif( + not HAS_AUTOUNCERTAINTIES, reason="Requires Auto-Uncertainties" +) + requires_uncertainties = pytest.mark.skipif( not HAS_UNCERTAINTIES, reason="Requires Uncertainties" ) diff --git a/pint/testsuite/test_pint_eval.py b/pint/testsuite/test_pint_eval.py index b5b94f0d9..7f1c7cbd4 100644 --- a/pint/testsuite/test_pint_eval.py +++ b/pint/testsuite/test_pint_eval.py @@ -138,6 +138,12 @@ def test_build_eval_tree(self, input_text, parsed): ("3 kg + 5", "((3 * kg) + 5)"), ("(5 % 2) m", "((5 % 2) * m)"), # mod operator ("(5 // 2) m", "((5 // 2) * m)"), # floordiv operator + # Uncertainties + ("3 +/- 5", "(3 ± 5)"), + ("3 ± 5", "(3 ± 5)"), + ("3 +- 5", "(3 ± 5)"), + ("2 * (3 +- 5)", "(2 * (3 ± 5))"), + ("(3 +- 5)^2", "((3 ± 5) ** 2)"), ), ) def test_preprocessed_eval_tree(self, input_text, parsed): diff --git a/pint/testsuite/test_uncertainty.py b/pint/testsuite/test_uncertainty.py new file mode 100644 index 000000000..de7da701a --- /dev/null +++ b/pint/testsuite/test_uncertainty.py @@ -0,0 +1,68 @@ +import pytest + +from pint import DimensionalityError +from pint.testsuite import QuantityTestCase, helpers + +from pint.compat import Uncertainty + + +# TODO: do not subclass from QuantityTestCase +@helpers.requires_autouncertainties() +class TestQuantity(QuantityTestCase): + def test_simple(self): + Q = self.ureg.Quantity + Q(Uncertainty(4.0, 0.1), "s") + + def test_build(self): + Q = self.ureg.Quantity + v, u, w = self.Q_(4.0, "s"), self.Q_(0.1, "s"), self.Q_(0.1, "days") + Q(Uncertainty(v.magnitude, u.magnitude), "s") + ( + Q(Uncertainty(v.magnitude, u.magnitude), "s"), + Q(Uncertainty.from_quantities(v, u)), + v.plus_minus(u), + v.plus_minus(w), + ) + + def test_raise_build(self): + v, u = self.Q_(1.0, "s"), self.Q_(0.1, "s") + o = self.Q_(0.1, "m") + + with pytest.raises(DimensionalityError): + Uncertainty.from_quantities(v, u._magnitude) + with pytest.raises(DimensionalityError): + Uncertainty.from_quantities(v._magnitude, u) + with pytest.raises(DimensionalityError): + Uncertainty.from_quantities(v, o) + with pytest.raises(DimensionalityError): + v.plus_minus(o) + with pytest.raises(DimensionalityError): + v.plus_minus(u._magnitude) + + def test_propagate_linear(self): + v = [0, 1, 2, 3, 4] + e = [1, 2, 3, 4, 5] + + x_without_units = [Uncertainty(vi, ei) for vi, ei in zip(v, e)] + x_with_units = [self.Q_(u, "s") for u in x_without_units] + + for x_nou, x_u in zip(x_without_units, x_with_units): + for y_nou, y_u in zip(x_without_units, x_with_units): + z_nou = x_nou + y_nou + z_u = x_u + y_u + assert z_nou.value == z_u.value.m + assert z_nou.error == z_u.error.m + + def test_propagate_product(self): + v = [1, 2, 3, 4] + e = [1, 2, 3, 4, 5] + + x_without_units = [Uncertainty(vi, ei) for vi, ei in zip(v, e)] + x_with_units = [self.Q_(u, "s") for u in x_without_units] + + for x_nou, x_u in zip(x_without_units, x_with_units): + for y_nou, y_u in zip(x_without_units, x_with_units): + z_nou = x_nou * y_nou + z_u = x_u * y_u + assert z_nou.value == z_u.value.m + assert z_nou.error == z_u.error.m diff --git a/pint/util.py b/pint/util.py index 09aed5f93..402295720 100644 --- a/pint/util.py +++ b/pint/util.py @@ -928,6 +928,10 @@ def string_preprocessor(input_string: str) -> str: # Handle caret exponentiation input_string = input_string.replace("^", "**") + + # Handle uncertainties + input_string = input_string.replace("+/-", "±") + input_string = input_string.replace("+-", "±") return input_string From c3743203209111129a7ec95b6fe93067100cb85d Mon Sep 17 00:00:00 2001 From: Varchas Gopalaswamy Date: Sat, 10 Jun 2023 14:40:55 -0400 Subject: [PATCH 5/7] amended setup to point to git for now --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d9ba57513..3bb280851 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ pandas = ["pint-pandas >= 0.3"] xarray = ["xarray"] dask = ["dask"] mip = ["mip >= 1.13"] -auto_uncertainties = ["auto-uncertainties >= 0.4.0"] +auto_uncertainties = ["auto-uncertainties @ git+https://github.com/varchasgopalaswamy/AutoUncertainties.git"] [project.urls] Homepage = "https://github.com/hgrecco/pint" From e4c7492bfbc3ee45284b3dc5348e775a0ca84fe1 Mon Sep 17 00:00:00 2001 From: Varchas Gopalaswamy Date: Sat, 26 Aug 2023 18:18:28 -0400 Subject: [PATCH 6/7] added uncertainties to typing --- pint/_typing.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pint/_typing.py b/pint/_typing.py index 7a67efc45..00aa342b9 100644 --- a/pint/_typing.py +++ b/pint/_typing.py @@ -14,7 +14,7 @@ HAS_NUMPY = False if TYPE_CHECKING: - from .compat import HAS_NUMPY + from .compat import HAS_NUMPY, HAS_AUTOUNCERTAINTIES if HAS_NUMPY: from .compat import np @@ -25,8 +25,12 @@ Scalar: TypeAlias = Union[float, int, Decimal, Fraction] Array: TypeAlias = Never +if HAS_AUTOUNCERTAINTIES: + from auto_uncertainties import Uncertainty +else: + Uncertainty: TypeAlias = Never # TODO: Change when Python 3.10 becomes minimal version. -Magnitude = Union[Scalar, Array] +Magnitude = Union[Scalar, Array, Uncertainty] UnitLike = Union[str, dict[str, Scalar], "UnitsContainer", "Unit"] From e006eb6acc14e1cd385743ea35c19b78539f8586 Mon Sep 17 00:00:00 2001 From: Varchas Gopalaswamy Date: Sat, 26 Aug 2023 19:27:44 -0400 Subject: [PATCH 7/7] fix to type_check guard --- pint/_typing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pint/_typing.py b/pint/_typing.py index 00aa342b9..12b109789 100644 --- a/pint/_typing.py +++ b/pint/_typing.py @@ -13,6 +13,8 @@ HAS_NUMPY = False +HAS_AUTOUNCERTAINTIES = False + if TYPE_CHECKING: from .compat import HAS_NUMPY, HAS_AUTOUNCERTAINTIES