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

1# units are provided by `pint` library 

2# see https://github.com/hgrecco/pint/ 

3 

4from typing import cast 

5from pint import UnitRegistry, Unit, Quantity 

6 

7pint_registry = UnitRegistry() 

8 

9# might be good to define extra units in a shared location 

10pint_registry.define("dollar = [currency]") 

11pint_registry.define("megadollar = 1e6 * dollar") 

12 

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} 

23 

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") 

30 

31 

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) 

44 

45 

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 

62 

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)}") 

74 

75def is_offset_unit(quantity:Quantity ) -> bool: 

76 """ 

77 Check if a unit is an offset unit (e.g. °C, °F) 

78 

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) 

84 

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 

98 

99 from_unit = pint_registry(from_unit) 

100 to_unit = pint_registry(to_unit) 

101 

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) 

106 

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)