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

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 

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) 

26 

27 

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

51 

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

53 """ 

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

55 

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) 

61 

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 

73 

74 from_unit = pint_registry(from_unit) 

75 to_unit = pint_registry(to_unit) 

76 

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) 

81 

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)