Coverage for backend/idaes_factory/unit_conversion/unit_conversion.py: 72%
37 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +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]")
13def get_unit(unit: str | None) -> Unit | None:
14 """
15 Get the pint unit object
16 @unit: str unit type
17 @return: unit object
18 """
19 if unit is None or unit == "": 19 ↛ 20line 19 didn't jump to line 20 because the condition on line 19 was never true
20 return None
21 pint_unit = getattr(pint_registry, unit, None)
22 if pint_unit is None: 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true
23 raise AttributeError(f'Unit `{unit}` not found.')
24 return cast(Unit, pint_unit)
27def convert_value(
28 value: float,
29 from_unit: str | None = None,
30 to_unit: str | None = None,
31 ) -> float:
32 """
33 convert value from one unit to another
34 @value: float value in original units
35 @from_unit: str unit
36 @to_unit: str unit
37 @return: float value converted to new unit
38 """
39 p_from_unit = get_unit(from_unit)
40 p_to_unit = get_unit(to_unit)
41 if p_from_unit is None or p_to_unit is None or p_from_unit == p_to_unit:
42 # no conversion needed
43 return value
44 try:
45 from_quantity = pint_registry.Quantity(value, p_from_unit)
46 to_quantity = from_quantity.to(p_to_unit)
47 return cast(float, to_quantity.magnitude)
48 except Exception as e:
49 raise ValueError(f"Could not perform unit conversion from {from_unit} to {to_unit}: {str(e)}")
51def is_offset_unit(quantity:Quantity ) -> bool:
52 """
53 Check if a unit is an offset unit (e.g. °C, °F)
55 These units are compatible but do not have the same zero point.
56 """
57 unit = quantity.units
58 # TODO: Support things like degC/min etc.
59 return (unit == pint_registry.degC or unit == pint_registry.degF or unit == pint_registry.degR or unit == pint_registry.K)
61def can_convert(
62 from_unit: str | None = None,
63 to_unit: str | None = None,
64 ) -> bool:
65 """
66 Check if pint can convert from one unit to another
67 """
68 if from_unit is None: 68 ↛ 69line 68 didn't jump to line 69 because the condition on line 68 was never true
69 from_unit = pint_registry.dimensionless
70 if to_unit is None: 70 ↛ 71line 70 didn't jump to line 71 because the condition on line 70 was never true
71 to_unit = pint_registry.dimensionless
73 from_unit = pint_registry(from_unit)
74 to_unit = pint_registry(to_unit)
76 # We can convert if:
77 # - they are both pint-compatible
78 # - they are either both offset units or both relative units
79 return is_offset_unit(from_unit) == is_offset_unit(to_unit) and from_unit.check(to_unit)
81def subtract_fraction(value: float, unit: str):
82 converted_value = convert_value(value, unit, "dimensionless")
83 calculated_value = 1 - converted_value
84 return convert_value(calculated_value, "dimensionless", unit)