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

Nonreducing #1993

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
5 changes: 4 additions & 1 deletion pint/delegates/formatter/_compound_unit_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)

from ...compat import TypeAlias, babel_parse
from ...util import UnitsContainer
from ...util import NonReducingUnitsContainer, UnitsContainer

T = TypeVar("T")
U = TypeVar("U")
Expand Down Expand Up @@ -246,6 +246,7 @@ def prepare_compount_unit(
locale: Locale | str | None = None,
as_ratio: bool = True,
registry: UnitRegistry | None = None,
empty_numerator_fmt="1",
) -> tuple[Iterable[tuple[str, T]], Iterable[tuple[str, T]]]:
"""Format compound unit into unit container given
an spec and locale.
Expand All @@ -257,6 +258,8 @@ def prepare_compount_unit(

if isinstance(unit, UnitsContainer):
out = unit.items()
elif hasattr(unit, "_units") and isinstance(unit._units, NonReducingUnitsContainer):
out = unit._units.non_reduced_d_items
elif hasattr(unit, "_units"):
out = unit._units.items()
else:
Expand Down
5 changes: 4 additions & 1 deletion pint/delegates/formatter/_format_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ def formatter(
power_fmt: str = "{} ** {}",
parentheses_fmt: str = "({0})",
exp_call: FORMATTER = "{:n}".format,
empty_numerator_fmt="1",
) -> str:
"""Format a list of (name, exponent) pairs.

Expand All @@ -185,6 +186,8 @@ def formatter(
the format used for parenthesis. (Default value = "({0})")
exp_call : callable
(Default value = lambda x: f"{x:n}")
empty_numerator_fmt : str
the format used for an empty numerator. (Default value = "1")

Returns
-------
Expand Down Expand Up @@ -220,7 +223,7 @@ def formatter(
return join_u(product_fmt, pos_terms + neg_terms)

# Show as Ratio: positive terms / negative terms
pos_ret = join_u(product_fmt, pos_terms) or "1"
pos_ret = join_u(product_fmt, pos_terms) or empty_numerator_fmt

if not neg_terms:
return pos_ret
Expand Down
17 changes: 15 additions & 2 deletions pint/delegates/formatter/full.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from ..._typing import Magnitude
from ...compat import Unpack, babel_parse
from ...util import iterable
from ...util import NonReducingUnitsContainer, iterable
from ._compound_unit_helpers import BabelKwds, SortFunc, sort_by_unit_name
from ._to_register import REGISTERED_FORMATTERS
from .html import HTMLFormatter
Expand All @@ -40,6 +40,11 @@
from ...registry import UnitRegistry


def _sort_func(items, registry):
# print([i for i in items])
return items


class FullFormatter(BaseFormatter):
"""A formatter that dispatch to other formatters.

Expand Down Expand Up @@ -134,8 +139,16 @@ def format_unit(
) -> str:
uspec = uspec or self.default_format
sort_func = sort_func or self.default_sort_func
empty_numerator_fmt = "1"
if isinstance(unit._units, NonReducingUnitsContainer):
sort_func = _sort_func
empty_numerator_fmt = ""
return self.get_formatter(uspec).format_unit(
unit, uspec, sort_func=sort_func, **babel_kwds
unit,
uspec,
sort_func=sort_func,
empty_numerator_fmt=empty_numerator_fmt,
**babel_kwds,
)

def format_quantity(
Expand Down
5 changes: 5 additions & 0 deletions pint/delegates/formatter/plain.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def format_unit(
unit: PlainUnit | Iterable[tuple[str, Any]],
uspec: str = "",
sort_func: SortFunc | None = None,
empty_numerator_fmt="1",
**babel_kwds: Unpack[BabelKwds],
) -> str:
"""Format a unit (can be compound) into string
Expand Down Expand Up @@ -203,6 +204,7 @@ def format_unit(
unit: PlainUnit | Iterable[tuple[str, Any]],
uspec: str = "",
sort_func: SortFunc | None = None,
empty_numerator_fmt="1",
**babel_kwds: Unpack[BabelKwds],
) -> str:
numerator, denominator = prepare_compount_unit(
Expand All @@ -225,6 +227,7 @@ def format_unit(
division_fmt=division_fmt,
power_fmt="{}**{}",
parentheses_fmt=r"({})",
empty_numerator_fmt=empty_numerator_fmt,
)

def format_quantity(
Expand Down Expand Up @@ -313,6 +316,7 @@ def format_unit(
unit: PlainUnit | Iterable[tuple[str, Any]],
uspec: str = "",
sort_func: SortFunc | None = None,
empty_numerator_fmt="1",
**babel_kwds: Unpack[BabelKwds],
) -> str:
numerator, denominator = prepare_compount_unit(
Expand All @@ -339,6 +343,7 @@ def format_unit(
power_fmt="{}{}",
parentheses_fmt="({})",
exp_call=pretty_fmt_exponent,
empty_numerator_fmt=empty_numerator_fmt,
)

def format_quantity(
Expand Down
2 changes: 2 additions & 0 deletions pint/facets/plain/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ def __new__(cls, value, units=None):
if units is None:
units = inst.UnitsContainer()
else:
if isinstance(units, list):
units = inst._REGISTRY.NonReducingUnitContainer(units)
if isinstance(units, (UnitsContainer, UnitDefinition)):
units = units
elif isinstance(units, str):
Expand Down
23 changes: 20 additions & 3 deletions pint/facets/plain/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@
from ...errors import DimensionalityError, RedefinitionError, UndefinedUnitError
from ...pint_eval import build_eval_tree
from ...util import (
NonReducingUnitsContainer,
ParserHelper,
UnitsContainer,
_is_dim,
create_class_with_registry,
getattr_maybe_raise,
Expand All @@ -72,7 +74,6 @@
string_preprocessor,
to_units_container,
)
from ...util import UnitsContainer as UnitsContainer
from .definitions import (
AliasDefinition,
CommentDefinition,
Expand Down Expand Up @@ -217,6 +218,7 @@ def __init__(
force_ndarray: bool = False,
force_ndarray_like: bool = False,
on_redefinition: str = "warn",
auto_reduce_units: bool = True,
auto_reduce_dimensions: bool = False,
autoconvert_to_preferred: bool = False,
preprocessors: list[PreprocessorType] | None = None,
Expand Down Expand Up @@ -261,6 +263,9 @@ def __init__(
#: Action to take in case a unit is redefined. 'warn', 'raise', 'ignore'
self._on_redefinition = on_redefinition

#: Determines if units should be reduced on appropriate operations.
self.auto_reduce_units = auto_reduce_units

#: Determines if dimensionality should be reduced on appropriate operations.
self.auto_reduce_dimensions = auto_reduce_dimensions

Expand Down Expand Up @@ -1258,7 +1263,6 @@ def _parse_units_as_container(

if as_delta:
cache[input_string] = ret

return ret

def _eval_token(
Expand Down Expand Up @@ -1402,7 +1406,20 @@ def _define_op(s: str):
# TODO: Maybe in the future we need to change it to a more meaningful
# non-colliding name.
def UnitsContainer(self, *args: Any, **kwargs: Any) -> UnitsContainer:
return UnitsContainer(*args, non_int_type=self.non_int_type, **kwargs)
return UnitsContainer(
*args,
non_int_type=self.non_int_type,
auto_reduce_units=self.auto_reduce_units,
**kwargs,
)

def NonReducingUnitsContainer(self, *args: Any, **kwargs: Any) -> UnitsContainer:
return NonReducingUnitsContainer(
*args,
non_int_type=self.non_int_type,
auto_reduce_units=self.auto_reduce_units,
**kwargs,
)

__call__ = parse_expression

Expand Down
2 changes: 2 additions & 0 deletions pint/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def __init__(
autoconvert_offset_to_baseunit: bool = False,
on_redefinition: str = "warn",
system=None,
auto_reduce_units=True,
auto_reduce_dimensions=False,
autoconvert_to_preferred=False,
preprocessors=None,
Expand All @@ -136,6 +137,7 @@ def __init__(
default_as_delta=default_as_delta,
autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit,
system=system,
auto_reduce_units=auto_reduce_units,
auto_reduce_dimensions=auto_reduce_dimensions,
autoconvert_to_preferred=autoconvert_to_preferred,
preprocessors=preprocessors,
Expand Down
30 changes: 30 additions & 0 deletions pint/testsuite/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,36 @@ def test_case_sensitivity(self):
assert ureg.parse_units("j", case_sensitive=False) == UnitsContainer(joule=1)


class TestNonReducing(QuantityTestCase):
def test_init(self):
ureg = self.ureg
NRUC_ = ureg.NonReducingUnitsContainer

strain_unit_container = NRUC_([(ureg.mm, 1), (ureg.mm, -1)])
assert strain_unit_container.non_reduced_units == [
ureg.UnitsContainer({"millimeter": 1}),
ureg.UnitsContainer({"millimeter": -1}),
]
assert strain_unit_container == ureg.dimensionless

def test_ureg_auto_reduce_units(self):
ureg = UnitRegistry(auto_reduce_units=False)
NRUC_ = ureg.NonReducingUnitsContainer

strain_unit = ureg.Unit("mm") / ureg.Unit("mm")
strain_unit == NRUC_([(ureg.mm, 1), (ureg.mm, -1)])
strain_unit == ureg.Unit("dimensionless")

strain_q = ureg.Quantity(1, "mm") / ureg.Quantity(1, "mm")
assert strain_q.units == strain_unit

def test_formatting(self):
ureg = UnitRegistry(auto_reduce_units=False)
strain_unit = ureg.Unit("mm") / ureg.Unit("mm")
assert format(strain_unit, "~D") == "mm / mm"
assert format(strain_unit, "P") == "millimeter/millimeter"


class TestCaseInsensitiveRegistry(QuantityTestCase):
kwargs = dict(case_sensitive=False)

Expand Down
72 changes: 67 additions & 5 deletions pint/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,22 +442,28 @@ class UnitsContainer(Mapping[str, Scalar]):
Numerical type used for non integer values.
"""

__slots__ = ("_d", "_hash", "_one", "_non_int_type")
__slots__ = ("_d", "_hash", "_one", "_non_int_type", "_auto_reduce_units")

_d: udict
_hash: int | None
_one: Scalar
_non_int_type: type
_auto_reduce_units: bool

def __init__(
self, *args: Any, non_int_type: type | None = None, **kwargs: Any
self,
*args: Any,
non_int_type: type | None = None,
auto_reduce_units: bool = True,
**kwargs: Any,
) -> None:
if args and isinstance(args[0], UnitsContainer):
default_non_int_type = args[0]._non_int_type
else:
default_non_int_type = float

self._non_int_type = non_int_type or default_non_int_type
self._auto_reduce_units = auto_reduce_units

if self._non_int_type is float:
self._one = 1
Expand Down Expand Up @@ -620,13 +626,20 @@ def __copy__(self):
out._hash = self._hash
out._non_int_type = self._non_int_type
out._one = self._one
out._auto_reduce_units = self._auto_reduce_units
return out

def __deepcopy__(self, memo):
return self.copy()

def __mul__(self, other: Any):
if not isinstance(other, self.__class__):
if not isinstance(other, UnitsContainer):
err = "Cannot multiply UnitsContainer by {}"
raise TypeError(err.format(type(other)))

if not self._auto_reduce_units:
return NonReducingUnitsContainer([self, other])

new = self.copy()
for key, value in other.items():
new._d[key] += value
Expand All @@ -650,10 +663,13 @@ def __pow__(self, other: Any):
return new

def __truediv__(self, other: Any):
if not isinstance(other, self.__class__):
if not isinstance(other, UnitsContainer):
err = "Cannot divide UnitsContainer by {}"
raise TypeError(err.format(type(other)))

if not self._auto_reduce_units:
return NonReducingUnitsContainer([self, UnitsContainer({}) / other])

new = self.copy()
for key, value in other.items():
new._d[key] -= self._normalize_nonfloat_value(value)
Expand All @@ -664,7 +680,7 @@ def __truediv__(self, other: Any):
return new

def __rtruediv__(self, other: Any):
if not isinstance(other, self.__class__) and other != 1:
if not isinstance(other, UnitsContainer) and other != 1:
err = "Cannot divide {} by UnitsContainer"
raise TypeError(err.format(type(other)))

Expand All @@ -676,6 +692,52 @@ def _normalize_nonfloat_value(self, value: Scalar) -> Scalar:
return value


class NonReducingUnitsContainer(UnitsContainer):
"""The NonReducingUnitsContainer stores UnitsContainers without simplifying common units.
This is useful when it is desired to show a unit in the numerator and denominator, eg mm/mm.
"""

def __init__(
self,
units: list[UnitsContainer] | list[tuple[QuantityOrUnitLike, Scalar]],
non_int_type: type | None = None,
auto_reduce_units: bool = True,
) -> None:
self.non_reduced_units = []
self._non_int_type = non_int_type
self.auto_reduce_units = auto_reduce_units

for u in units:
if isinstance(u, tuple):
u = u[0]._units ** u[1]
if hasattr(u, "_units"):
u = u._units
self.non_reduced_units.append(u)

self.reduced_units = UnitsContainer()
for unit in self.non_reduced_units:
self.reduced_units *= unit

self._d = self.reduced_units._d
self._hash = self.reduced_units._hash

self.non_reduced_d_items = [
(key, value)
for uc in self.non_reduced_units
for key, value in uc._d.items()
]
self.i = 0

def __repr__(self) -> str:
tmp = "[%s]" % ", ".join(
[f"'{key}': {value}" for key, value in self.non_reduced_d_items]
)
return f"<NonReducingUnitsContainer({tmp})>"

def unit_items(self) -> Iterable[tuple[str, Scalar]]:
return [items for _d in self.non_reduced_units for items in _d.items()]


class ParserHelper(UnitsContainer):
"""The ParserHelper stores in place the product of variables and
their respective exponent and implements the corresponding operations.
Expand Down
Loading