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

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

11 

12 

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) 

25 

26 

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

50 

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

52 """ 

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

54 

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) 

60 

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 

72 

73 from_unit = pint_registry(from_unit) 

74 to_unit = pint_registry(to_unit) 

75 

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) 

80 

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)