Skip to content

Commit

Permalink
add registry option for autoconvert_to_preferred (#1803)
Browse files Browse the repository at this point in the history
Documented `to_preferred` and created added an autoautoconvert_to_preferred registry option.

Closes #1787
  • Loading branch information
andrewgsavage authored Jun 14, 2023
1 parent 3fb63af commit e60fe39
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 6 deletions.
3 changes: 2 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Pint Changelog
0.23 (unreleased)
-----------------

- Nothing changed yet.
- Documented to_preferred and created added an autoautoconvert_to_preferred registry option.
(PR #1803)


0.22 (2023-05-25)
Expand Down
32 changes: 32 additions & 0 deletions docs/getting/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,38 @@ If you want pint to automatically perform dimensional reduction when producing
new quantities, the ``UnitRegistry`` class accepts a parameter ``auto_reduce_dimensions``.
Dimensional reduction can be slow, so auto-reducing is disabled by default.

The methods ``to_preferred()`` and ``ito_preferred()`` provide more control over dimensional
reduction by specifying a list of units to combine to get the required dimensionality.

.. doctest::

>>> preferred_units = [
... ureg.ft, # distance L
... ureg.slug, # mass M
... ureg.s, # duration T
... ureg.rankine, # temperature Θ
... ureg.lbf, # force L M T^-2
... ureg.W, # power L^2 M T^-3
... ]
>>> power = ((1 * ureg.lbf) * (1 * ureg.m / ureg.s)).to_preferred(preferred_units)
>>> print(power)
4.4482216152605005 watt

The list of preferred units can also be specified in the unit registry to prevent having to pass it to every call to ``to_preferred()``.

.. doctest::

>>> ureg.default_preferred_units = preferred_units
>>> power = ((1 * ureg.lbf) * (1 * ureg.m / ureg.s)).to_preferred()
>>> print(power)
4.4482216152605005 watt

The ``UnitRegistry`` class accepts a parameter ``autoconvert_to_preferred``. If set to ``True``, pint will automatically convert to
preferred units when producing new quantities. This is disabled by default.

Note when there are multiple good combinations of units to reduce to, to_preferred is not guaranteed to be repeatable.
For example, ``(1 * ureg.lbf * ureg.m).to_preferred(preferred_units)`` may return ``W s`` or ``ft lbf``.

String parsing
--------------

Expand Down
40 changes: 35 additions & 5 deletions pint/facets/plain/qto.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def to_compact(


def to_preferred(
quantity: PlainQuantity, preferred_units: list[UnitLike]
quantity: PlainQuantity, preferred_units: Optional[list[UnitLike]] = None
) -> PlainQuantity:
"""Return Quantity converted to a unit composed of the preferred units.
Expand All @@ -180,8 +180,38 @@ def to_preferred(
<Quantity(4.44822162, 'second * watt')>
"""

units = _get_preferred(quantity, preferred_units)
return quantity.to(units)


def ito_preferred(
quantity: PlainQuantity, preferred_units: Optional[list[UnitLike]] = None
) -> PlainQuantity:
"""Return Quantity converted to a unit composed of the preferred units.
Examples
--------
>>> import pint
>>> ureg = pint.UnitRegistry()
>>> (1*ureg.acre).to_preferred([ureg.meters])
<Quantity(4046.87261, 'meter ** 2')>
>>> (1*(ureg.force_pound*ureg.m)).to_preferred([ureg.W])
<Quantity(4.44822162, 'second * watt')>
"""

units = _get_preferred(quantity, preferred_units)
return quantity.ito(units)


def _get_preferred(
quantity: PlainQuantity, preferred_units: Optional[list[UnitLike]] = None
) -> PlainQuantity:
if preferred_units is None:
preferred_units = quantity._REGISTRY.default_preferred_units

if not quantity.dimensionality:
return quantity
return quantity._units.copy()

# The optimizer isn't perfect, and will sometimes miss obvious solutions.
# This sub-algorithm is less powerful, but always finds the very simple solutions.
Expand Down Expand Up @@ -211,7 +241,7 @@ def find_simple():

simple = find_simple()
if simple is not None:
return quantity.to(simple)
return simple

# For each dimension (e.g. T(ime), L(ength), M(ass)), assign a default base unit from
# the collection of base units
Expand Down Expand Up @@ -380,8 +410,8 @@ def find_simple():
min_key = sorted(sorting_keys)[0]
result_unit = sorting_keys[min_key]

return quantity.to(result_unit)
return result_unit

# for whatever reason, a solution wasn't found
# return the original quantity
return quantity
return quantity._units.copy()
14 changes: 14 additions & 0 deletions pint/facets/plain/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@
def reduce_dimensions(f):
def wrapped(self, *args, **kwargs):
result = f(self, *args, **kwargs)
try:
if result._REGISTRY.autoconvert_to_preferred:
result = result.to_preferred()
except AttributeError:
pass

try:
if result._REGISTRY.auto_reduce_dimensions:
return result.to_reduced_units()
Expand All @@ -78,6 +84,12 @@ def wrapped(self, *args, **kwargs):
def ireduce_dimensions(f):
def wrapped(self, *args, **kwargs):
result = f(self, *args, **kwargs)
try:
if result._REGISTRY.autoconvert_to_preferred:
result.ito_preferred()
except AttributeError:
pass

try:
if result._REGISTRY.auto_reduce_dimensions:
result.ito_reduced_units()
Expand Down Expand Up @@ -487,6 +499,7 @@ def ito(
**ctx_kwargs :
Values for the Context/s
"""

other = to_units_container(other, self._REGISTRY)

self._magnitude = self._convert_magnitude(other, *contexts, **ctx_kwargs)
Expand Down Expand Up @@ -561,6 +574,7 @@ def to_base_units(self) -> PlainQuantity[MagnitudeT]:
# They are implemented elsewhere to keep Quantity class clean.
to_compact = qto.to_compact
to_preferred = qto.to_preferred
ito_preferred = qto.ito_preferred
to_reduced_units = qto.to_reduced_units
ito_reduced_units = qto.ito_reduced_units

Expand Down
6 changes: 6 additions & 0 deletions pint/facets/plain/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ class GenericPlainRegistry(Generic[QuantityT, UnitT], metaclass=RegistryMeta):
action to take in case a unit is redefined: 'warn', 'raise', 'ignore'
auto_reduce_dimensions :
If True, reduce dimensionality on appropriate operations.
autoconvert_to_preferred :
If True, converts preferred units on appropriate operations.
preprocessors :
list of callables which are iteratively ran on any input expression or unit
string
Expand Down Expand Up @@ -204,6 +206,7 @@ def __init__(
force_ndarray_like: bool = False,
on_redefinition: str = "warn",
auto_reduce_dimensions: bool = False,
autoconvert_to_preferred: bool = False,
preprocessors: Optional[list[PreprocessorType]] = None,
fmt_locale: Optional[str] = None,
non_int_type: NON_INT_TYPE = float,
Expand Down Expand Up @@ -248,6 +251,9 @@ def __init__(
#: Determines if dimensionality should be reduced on appropriate operations.
self.auto_reduce_dimensions = auto_reduce_dimensions

#: Determines if units will be converted to preffered on appropriate operations.
self.autoconvert_to_preferred = autoconvert_to_preferred

#: Default locale identifier string, used when calling format_babel without explicit locale.
self.set_fmt_locale(fmt_locale)

Expand Down
4 changes: 4 additions & 0 deletions pint/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ class UnitRegistry(GenericUnitRegistry[Quantity, Unit]):
'warn', 'raise', 'ignore'
auto_reduce_dimensions :
If True, reduce dimensionality on appropriate operations.
autoconvert_to_preferred :
If True, converts preferred units on appropriate operations.
preprocessors :
list of callables which are iteratively ran on any input expression
or unit string
Expand All @@ -117,6 +119,7 @@ def __init__(
on_redefinition: str = "warn",
system=None,
auto_reduce_dimensions=False,
autoconvert_to_preferred=False,
preprocessors=None,
fmt_locale=None,
non_int_type=float,
Expand All @@ -132,6 +135,7 @@ def __init__(
autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit,
system=system,
auto_reduce_dimensions=auto_reduce_dimensions,
autoconvert_to_preferred=autoconvert_to_preferred,
preprocessors=preprocessors,
fmt_locale=fmt_locale,
non_int_type=non_int_type,
Expand Down
30 changes: 30 additions & 0 deletions pint/testsuite/test_quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,36 @@ def test_to_preferred(self):
result = Q_("1 volt").to_preferred(preferred_units)
assert result.units == ureg.volts

@helpers.requires_mip
def test_to_preferred_registry(self):
ureg = UnitRegistry()
Q_ = ureg.Quantity
ureg.preferred_units = [
ureg.m, # distance L
ureg.kg, # mass M
ureg.s, # duration T
ureg.N, # force L M T^-2
ureg.Pa, # pressure M L^−1 T^−2
ureg.W, # power L^2 M T^-3
]
pressure = (Q_(1, "N") * Q_("1 m**-2")).to_preferred()
assert pressure.units == ureg.Pa

@helpers.requires_mip
def test_autoconvert_to_preferred(self):
ureg = UnitRegistry()
Q_ = ureg.Quantity
ureg.preferred_units = [
ureg.m, # distance L
ureg.kg, # mass M
ureg.s, # duration T
ureg.N, # force L M T^-2
ureg.Pa, # pressure M L^−1 T^−2
ureg.W, # power L^2 M T^-3
]
pressure = Q_(1, "N") * Q_("1 m**-2")
assert pressure.units == ureg.Pa

@helpers.requires_numpy
def test_convert_numpy(self):
# Conversions with single units take a different codepath than
Expand Down

0 comments on commit e60fe39

Please sign in to comment.