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

Construct fraction and eliminate common terms when calculating conversion factor to increase accuracy #2108

Open
wants to merge 7 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
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Pint Changelog
- Fix return type of `PlainQuantity.to` (#2088)
- Update constants to CODATA 2022 recommended values. (#2049)
- Fixed issue with `.to_compact` and Magnitudes with uncertainties / Quantities with units (PR #2069, issue #2044)
- Fixed issue in unit conversion which led to loss of precision when using `decimal`. (#2107)
- Add conductivity dimension. (#2112)


Expand Down
65 changes: 59 additions & 6 deletions pint/facets/plain/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,10 +898,49 @@ def _get_root_units(
pass

accumulators: dict[str | None, int] = defaultdict(int)
accumulators[None] = 1
self._get_root_units_recurse(input_units, 1, accumulators)
fraction: dict[str, dict[Decimal | float, Decimal | int]] = dict(
numerator=dict(), denominator=dict()
)
self._get_root_units_recurse(input_units, 1, accumulators, fraction)

# Identify if terms appear in both numerator and denominator
def terms_are_unique(fraction):
for n_factor, n_exp in fraction["numerator"].items():
if n_factor in fraction["denominator"]:
return False
return True

# Cancel out terms where factor matches
while not terms_are_unique(fraction):
for n_factor, n_exponent in fraction["numerator"].items():
if n_factor in fraction["denominator"]:
if n_exponent >= fraction["denominator"][n_factor]:
fraction["numerator"][n_factor] -= fraction["denominator"][
n_factor
]
del fraction["denominator"][n_factor]
continue
for d_factor, d_exponent in fraction["denominator"].items():
if d_factor in fraction["numerator"]:
if d_exponent >= fraction["numerator"][d_factor]:
fraction["denominator"][d_factor] -= fraction["numerator"][
d_factor
]
del fraction["numerator"][d_factor]
continue

factor = 1
for n_factor, n_exponent in fraction["numerator"].copy().items():
if n_exponent == 0:
del fraction["numerator"][n_factor]
else:
factor *= n_factor**n_exponent
for d_factor, d_exponent in fraction["denominator"].copy().items():
if d_exponent == 0:
del fraction["denominator"][d_factor]
else:
factor *= d_factor**-d_exponent

factor = accumulators[None]
units = self.UnitsContainer(
{k: v for k, v in accumulators.items() if k is not None and v != 0}
)
Expand Down Expand Up @@ -948,7 +987,11 @@ def get_base_units(
# TODO: accumulators breaks typing list[int, dict[str, int]]
# So we have changed the behavior here
def _get_root_units_recurse(
self, ref: UnitsContainer, exp: Scalar, accumulators: dict[str | None, int]
self,
ref: UnitsContainer,
exp: Scalar,
accumulators: dict[str | None, int],
fraction: dict[str, dict[Decimal | float, Decimal | int]],
) -> None:
"""

Expand All @@ -962,9 +1005,19 @@ def _get_root_units_recurse(
if reg.is_base:
accumulators[key] += exp2
else:
accumulators[None] *= reg.converter.scale**exp2
# Build numerator and denominator
if exp2 < 0:
fraction["denominator"][reg.converter.scale] = (
fraction["denominator"].get(reg.converter.scale, 0) - exp2
)
else:
fraction["numerator"][reg.converter.scale] = (
fraction["numerator"].get(reg.converter.scale, 0) + exp2
)
if reg.reference is not None:
self._get_root_units_recurse(reg.reference, exp2, accumulators)
self._get_root_units_recurse(
reg.reference, exp2, accumulators, fraction
)

def get_compatible_units(self, input_units: QuantityOrUnitLike) -> frozenset[UnitT]:
""" """
Expand Down
20 changes: 20 additions & 0 deletions pint/testsuite/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -1349,3 +1349,23 @@ def test_issue2044():
q = (ufloat(10_000, 0.01) * ureg.m).to_compact()
assert_almost_equal(q.m.n, 10.0)
assert q.u == "kilometer"


def test_issue2107():
# Use decimal
ureg = UnitRegistry(non_int_type=decimal.Decimal)
# 2 L/h is equal to 48 L/day
flow = decimal.Decimal("2") * ureg.L / ureg.h
assert flow.to(ureg.L / ureg.day).magnitude == 48.0
# 1 inch is equal to 1000 thou
distance = ureg.Quantity(decimal.Decimal("1.0"), ureg.inch)
assert distance.to(ureg.thou).magnitude == 1000.0

# Perform the same conversions without decimal
ureg = UnitRegistry()
# 2 L/h is equal to 48 L/day
flow = 2 * ureg.L / ureg.h
assert flow.to(ureg.L / ureg.day).magnitude == 48.0
# 1 inch is equal to 1000 thou
distance = ureg.Quantity(1, ureg.inch)
assert distance.to(ureg.thou).magnitude == 1000.0
Loading