From e60fe39e830ffd1da691304752970afd00ce63d2 Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Wed, 14 Jun 2023 20:42:57 +0100 Subject: [PATCH] add registry option for autoconvert_to_preferred (#1803) Documented `to_preferred` and created added an autoautoconvert_to_preferred registry option. Closes #1787 --- CHANGES | 3 ++- docs/getting/tutorial.rst | 32 ++++++++++++++++++++++++++ pint/facets/plain/qto.py | 40 ++++++++++++++++++++++++++++----- pint/facets/plain/quantity.py | 14 ++++++++++++ pint/facets/plain/registry.py | 6 +++++ pint/registry.py | 4 ++++ pint/testsuite/test_quantity.py | 30 +++++++++++++++++++++++++ 7 files changed, 123 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index 6d34f7476..597000896 100644 --- a/CHANGES +++ b/CHANGES @@ -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) diff --git a/docs/getting/tutorial.rst b/docs/getting/tutorial.rst index 853aa2722..28041339d 100644 --- a/docs/getting/tutorial.rst +++ b/docs/getting/tutorial.rst @@ -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 -------------- diff --git a/pint/facets/plain/qto.py b/pint/facets/plain/qto.py index 0508e9ac3..9cd8a780a 100644 --- a/pint/facets/plain/qto.py +++ b/pint/facets/plain/qto.py @@ -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. @@ -180,8 +180,38 @@ def to_preferred( """ + 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]) + + >>> (1*(ureg.force_pound*ureg.m)).to_preferred([ureg.W]) + + """ + + 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. @@ -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 @@ -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() diff --git a/pint/facets/plain/quantity.py b/pint/facets/plain/quantity.py index 5841a9a99..3c34d3c07 100644 --- a/pint/facets/plain/quantity.py +++ b/pint/facets/plain/quantity.py @@ -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() @@ -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() @@ -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) @@ -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 diff --git a/pint/facets/plain/registry.py b/pint/facets/plain/registry.py index a6d7a13c7..b602ffa29 100644 --- a/pint/facets/plain/registry.py +++ b/pint/facets/plain/registry.py @@ -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 @@ -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, @@ -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) diff --git a/pint/registry.py b/pint/registry.py index e978e3698..b822057ba 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -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 @@ -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, @@ -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, diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 45b163d76..1843b69ca 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -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