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

Added a facet for array uncertainty propagation #1797

Open
wants to merge 13 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
10 changes: 8 additions & 2 deletions pint/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]

Expand Down
12 changes: 12 additions & 0 deletions pint/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,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
Expand Down
4 changes: 4 additions & 0 deletions pint/facets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -103,4 +105,6 @@ class that belongs to a registry that has NumpyRegistry as one of its bases.
"QuantityT",
"UnitT",
"MagnitudeT",
"UncertaintyRegistry",
"GenericUncertaintyRegistry",
]
67 changes: 67 additions & 0 deletions pint/facets/uncertainties/__init__.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 16 additions & 2 deletions pint/pint_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 == "<none>":
raise DefinitionSyntaxError(
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions pint/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
class Quantity(
facets.SystemRegistry.Quantity,
facets.ContextRegistry.Quantity,
facets.UncertaintyRegistry.Quantity,
facets.DaskRegistry.Quantity,
facets.NumpyRegistry.Quantity,
facets.MeasurementRegistry.Quantity,
Expand All @@ -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,
Expand All @@ -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],
Expand Down
5 changes: 5 additions & 0 deletions pint/testsuite/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
HAS_NUMPY,
HAS_NUMPY_ARRAY_FUNCTION,
HAS_UNCERTAINTIES,
HAS_AUTOUNCERTAINTIES,
NUMPY_VER,
)

Expand Down Expand Up @@ -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"
)
Expand Down
6 changes: 6 additions & 0 deletions pint/testsuite/test_pint_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
68 changes: 68 additions & 0 deletions pint/testsuite/test_uncertainty.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions pint/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading