Coverage for backend/django/idaes_factory/unit_conversion/unit_conversion.py: 85%
51 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-23 21:51 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-23 21:51 +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")
13# Fixed FX assumptions used by the unit registry. The rates are anchored to NZD
14# so existing generic `dollar` units remain equivalent to the app's default
15# project currency.
16FIXED_FX_RATES_PER_NZD = {
17 "NZD": 1.0,
18 "USD": 0.5849305057151759,
19 "AUD": 0.8203239576440228,
20 "EUR": 0.5039856740506515,
21 "GBP": 0.4352380389574088,
22}
24pint_registry.define("NZD = dollar")
25for currency_code, units_per_nzd in FIXED_FX_RATES_PER_NZD.items():
26 if currency_code == "NZD":
27 continue
28 nzd_per_unit = 1 / units_per_nzd
29 pint_registry.define(f"{currency_code} = {nzd_per_unit} * NZD")
32def get_unit(unit: str | None) -> Unit | None:
33 """
34 Get the pint unit object
35 @unit: str unit type
36 @return: unit object
37 """
38 if unit is None or unit == "":
39 return None
40 pint_unit = getattr(pint_registry, unit, None)
41 if pint_unit is None: 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true
42 raise AttributeError(f'Unit `{unit}` not found.')
43 return cast(Unit, pint_unit)
46def convert_value(
47 value: float,
48 from_unit: str | None = None,
49 to_unit: str | None = None,
50 ) -> float:
51 """
52 convert value from one unit to another
53 @value: float value in original units
54 @from_unit: str unit
55 @to_unit: str unit
56 @return: float value converted to new unit
57 """
58 if from_unit in (None, "") and to_unit in (None, ""): 58 ↛ 59line 58 didn't jump to line 59 because the condition on line 58 was never true
59 return value
60 if from_unit == to_unit:
61 return value
63 p_from_unit = get_unit(from_unit)
64 p_to_unit = get_unit(to_unit)
65 if p_from_unit is None or p_to_unit is None or p_from_unit == p_to_unit:
66 # no conversion needed
67 return value
68 try:
69 from_quantity = pint_registry.Quantity(value, p_from_unit)
70 to_quantity = from_quantity.to(p_to_unit)
71 return cast(float, to_quantity.magnitude)
72 except Exception as e:
73 raise ValueError(f"Could not perform unit conversion from {from_unit} to {to_unit}: {str(e)}")
75def is_offset_unit(quantity:Quantity ) -> bool:
76 """
77 Check if a unit is an offset unit (e.g. °C, °F)
79 These units are compatible but do not have the same zero point.
80 """
81 unit = quantity.units
82 # TODO: Support things like degC/min etc.
83 return (unit == pint_registry.degC or unit == pint_registry.degF or unit == pint_registry.degR or unit == pint_registry.K)
85def can_convert(
86 from_unit: str | None = None,
87 to_unit: str | None = None,
88 ) -> bool:
89 """
90 Check if pint can convert from one unit to another
91 """
92 if from_unit in (None, ""): 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 from_unit = "dimensionless"
94 if to_unit in (None, ""):
95 to_unit = "dimensionless"
96 if from_unit == to_unit:
97 return True
99 from_unit = pint_registry(from_unit)
100 to_unit = pint_registry(to_unit)
102 # We can convert if:
103 # - they are both pint-compatible
104 # - they are either both offset units or both relative units
105 return is_offset_unit(from_unit) == is_offset_unit(to_unit) and from_unit.check(to_unit)
107def subtract_fraction(value: float, unit: str):
108 converted_value = convert_value(value, unit, "dimensionless")
109 calculated_value = 1 - converted_value
110 return convert_value(calculated_value, "dimensionless", unit)