diff --git a/pint/_typing.py b/pint/_typing.py index 241459ef1..96bb63ccb 100644 --- a/pint/_typing.py +++ b/pint/_typing.py @@ -14,8 +14,10 @@ HAS_NUMPY = False +HAS_AUTOUNCERTAINTIES = False + if TYPE_CHECKING: - from .compat import HAS_NUMPY + from .compat import HAS_NUMPY, HAS_AUTOUNCERTAINTIES if HAS_NUMPY: from .compat import np @@ -26,8 +28,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"] diff --git a/pint/compat.py b/pint/compat.py index 4b4cbab92..a564abfff 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -171,6 +171,18 @@ def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): return value + +try: + from auto_uncertainties import Uncertainty +except ImportError: + 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/__init__.py b/pint/facets/__init__.py index 12729289c..74e56c5b2 100644 --- a/pint/facets/__init__.py +++ b/pint/facets/__init__.py @@ -77,9 +77,11 @@ class that belongs to a registry that has NumpyRegistry as one of its bases. GenericNonMultiplicativeRegistry, NonMultiplicativeRegistry, ) + from .numpy import GenericNumpyRegistry, NumpyRegistry from .plain import GenericPlainRegistry, MagnitudeT, PlainRegistry, QuantityT, UnitT from .system import GenericSystemRegistry, SystemRegistry +from .uncertainties import UncertaintyRegistry, GenericUncertaintyRegistry __all__ = [ "ContextRegistry", @@ -103,4 +105,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..c8c1968ce --- /dev/null +++ b/pint/facets/uncertainties/__init__.py @@ -0,0 +1,67 @@ +""" + 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 TypeAlias, Uncertainty +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 + + 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 + + +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/pint_eval.py b/pint/pint_eval.py index c2ddb29cd..a2008df9b 100644 --- a/pint/pint_eval.py +++ b/pint/pint_eval.py @@ -26,8 +26,13 @@ from .errors import DefinitionSyntaxError +try: + from auto_uncertainties import Uncertainty +except ImportError: + Uncertainty = None # For controlling order of operations _OP_PRIORITY = { + "±": 4, "+/-": 4, "**": 3, "^": 3, @@ -42,12 +47,19 @@ } + +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 _ufloat(left, right): if HAS_UNCERTAINTIES: return ufloat(left, right) raise TypeError("Could not import support for uncertainties") + def _power(left: Any, right: Any) -> Any: from . import Quantity from .compat import is_duck_array @@ -295,7 +307,8 @@ def _finalize_e(nominal_value, std_dev, toklist, possible_e): _UNARY_OPERATOR_MAP: dict[str, UnaryOpT] = {"+": lambda x: x, "-": lambda x: x * -1} _BINARY_OPERATOR_MAP: dict[str, BinaryOpT] = { - "+/-": _ufloat, + "±": _uncertainty, + "+/-": _uncertainty, "**": _power, "*": operator.mul, "": operator.mul, # operator for implicit ops @@ -438,7 +451,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( @@ -465,6 +478,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/registry.py b/pint/registry.py index ceb9b62d1..8e13cbba5 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -28,6 +28,7 @@ class Quantity( facets.SystemRegistry.Quantity, facets.ContextRegistry.Quantity, + facets.UncertaintyRegistry.Quantity, facets.DaskRegistry.Quantity, facets.NumpyRegistry.Quantity, facets.MeasurementRegistry.Quantity, @@ -40,6 +41,7 @@ class Quantity( class Unit( facets.SystemRegistry.Unit, facets.ContextRegistry.Unit, + facets.UncertaintyRegistry.Unit, facets.DaskRegistry.Unit, facets.NumpyRegistry.Unit, facets.MeasurementRegistry.Unit, @@ -53,6 +55,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/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index d317e0755..72a5d4592 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -17,6 +17,7 @@ HAS_NUMPY, HAS_NUMPY_ARRAY_FUNCTION, HAS_UNCERTAINTIES, + HAS_AUTOUNCERTAINTIES, NUMPY_VER, ) @@ -151,6 +152,10 @@ def requires_babel(tested_locales=[]): 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 3cee7d758..73dc35baa 100644 --- a/pint/testsuite/test_pint_eval.py +++ b/pint/testsuite/test_pint_eval.py @@ -142,6 +142,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 c7a7ec10c..375afa300 100644 --- a/pint/util.py +++ b/pint/util.py @@ -938,6 +938,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 diff --git a/pyproject.toml b/pyproject.toml index 9f29f8f92..60e8bdf24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ pandas = ["pint-pandas >= 0.3"] xarray = ["xarray"] dask = ["dask"] mip = ["mip >= 1.13"] +auto_uncertainties = ["auto-uncertainties @ git+https://github.com/varchasgopalaswamy/AutoUncertainties.git"] [project.urls] Homepage = "https://github.com/hgrecco/pint"