Coverage for backend/django/idaes_factory/unit_conversion/unit_conversion.py: 77%
38 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
1# units are provided by `pint` library
2# see https://github.com/hgrecco/pint/
4from typing import cast
5from pint import UnitRegistry, Unit, Quantity
7pint_registry = UnitRegistry()
9# might be good to define extra units in a shared location
10pint_registry.define("dollar = [currency]")
11pint_registry.define("megadollar = 1e6 * dollar")
14def get_unit(unit: str | None) -> Unit | None:
15 """
16 Get the pint unit object
17 @unit: str unit type
18 @return: unit object
19 """
20 if unit is None or unit == "":
21 return None
22 pint_unit = getattr(pint_registry, unit, None)
23 if pint_unit is None: 23 ↛ 24line 23 didn't jump to line 24 because the condition on line 23 was never true
24 raise AttributeError(f'Unit `{unit}` not found.')
25 return cast(Unit, pint_unit)
28def convert_value(
29 value: float,
30 from_unit: str | None = None,
31 to_unit: str | None = None,
32 ) -> float:
33 """
34 convert value from one unit to another
35 @value: float value in original units
36 @from_unit: str unit
37 @to_unit: str unit
38 @return: float value converted to new unit
39 """
40 p_from_unit = get_unit(from_unit)
41 p_to_unit = get_unit(to_unit)
42 if p_from_unit is None or p_to_unit is None or p_from_unit == p_to_unit:
43 # no conversion needed
44 return value
45 try:
46 from_quantity = pint_registry.Quantity(value, p_from_unit)
47 to_quantity = from_quantity.to(p_to_unit)
48 return cast(float, to_quantity.magnitude)
49 except Exception as e:
50 raise ValueError(f"Could not perform unit conversion from {from_unit} to {to_unit}: {str(e)}")
52def is_offset_unit(quantity:Quantity ) -> bool:
53 """
54 Check if a unit is an offset unit (e.g. °C, °F)
56 These units are compatible but do not have the same zero point.
57 """
58 unit = quantity.units
59 # TODO: Support things like degC/min etc.
60 return (unit == pint_registry.degC or unit == pint_registry.degF or unit == pint_registry.degR or unit == pint_registry.K)
62def can_convert(
63 from_unit: str | None = None,
64 to_unit: str | None = None,
65 ) -> bool:
66 """
67 Check if pint can convert from one unit to another
68 """
69 if from_unit is None: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true
70 from_unit = pint_registry.dimensionless
71 if to_unit is None: 71 ↛ 72line 71 didn't jump to line 72 because the condition on line 71 was never true
72 to_unit = pint_registry.dimensionless
74 from_unit = pint_registry(from_unit)
75 to_unit = pint_registry(to_unit)
77 # We can convert if:
78 # - they are both pint-compatible
79 # - they are either both offset units or both relative units
80 return is_offset_unit(from_unit) == is_offset_unit(to_unit) and from_unit.check(to_unit)
82def subtract_fraction(value: float, unit: str):
83 converted_value = convert_value(value, unit, "dimensionless")
84 calculated_value = 1 - converted_value
85 return convert_value(calculated_value, "dimensionless", unit)