Coverage for backend/django/Economics/costing/operating/resource_basis.py: 85%

94 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-07-22 05:22 +0000

1"""Shared operating-resource classification helpers.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass 

6from decimal import Decimal, InvalidOperation 

7from typing import Any 

8 

9from core.auxiliary.models.PropertyInfo import PropertyInfo 

10from flowsheetInternals.unitops.models.SimulationObject import SimulationObject 

11from Economics.costing.models import OperatingCostLine 

12from Economics.shared.choices import OperatingLineCategory, OutletStreamDisposition 

13from Economics.shared.unit_conversion import convert_quantity 

14from Economics.costing.operating.stream_properties import ( 

15 UNIT_COOLING_DUTY_SOURCE_KIND, 

16 UNIT_HEATING_DUTY_SOURCE_KIND, 

17 UNIT_POWER_WORK_SOURCE_KIND, 

18 unit_energy_source_kind, 

19) 

20 

21 

22RESOURCE_CATEGORY_ANNUAL_COOLING_DUTY = "annual_cooling_duty" 

23RESOURCE_CATEGORY_ANNUAL_HEATING_DUTY = "annual_heating_duty" 

24RESOURCE_CATEGORY_ANNUAL_WORK = "annual_work" 

25RESOURCE_CATEGORY_PRODUCT_OUTPUT = "product_output" 

26RESOURCE_CATEGORY_RAW_MATERIAL_CONSUMPTION = "raw_material_consumption" 

27RESOURCE_CATEGORY_WASTE_DISPOSAL = "waste_disposal" 

28 

29 

30@dataclass(frozen=True) 

31class AnnualResourceQuantity: 

32 """Annual quantity snapshot used by high-level resource metrics.""" 

33 

34 quantity: Decimal 

35 unit: str 

36 

37 

38def operating_resource_source_kind( 

39 *, 

40 line: OperatingCostLine, 

41 source_object: SimulationObject | None = None, 

42 property_info: PropertyInfo | None = None, 

43) -> str: 

44 """Classify the process source behind an operating line.""" 

45 if property_info is None and line.source_property_info_id: 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true

46 property_info = line.source_property_info 

47 if source_object is None and property_info is not None: 47 ↛ 48line 47 didn't jump to line 48 because the condition on line 47 was never true

48 source_object = property_info.set.simulationObject 

49 if line.category == OperatingLineCategory.FEEDSTOCK and property_info is not None: 

50 return "starting_input_stream" 

51 if ( 

52 line.category in (OperatingLineCategory.OUTPUT_REVENUE, OperatingLineCategory.DISPOSAL) 

53 and property_info is not None 

54 ): 

55 return "terminal_output_stream" 

56 if line.category != OperatingLineCategory.ENERGY or source_object is None or property_info is None: 

57 return "" 

58 return unit_energy_source_kind(unit=source_object, property_info=property_info) or "" 

59 

60 

61def operating_resource_breakdown_category( 

62 *, 

63 line: OperatingCostLine, 

64 source_kind: str | None = None, 

65) -> str: 

66 """Return the stable resource breakdown bucket for an operating line.""" 

67 if line.category == OperatingLineCategory.FEEDSTOCK: 

68 return RESOURCE_CATEGORY_RAW_MATERIAL_CONSUMPTION 

69 if ( 

70 line.category == OperatingLineCategory.OUTPUT_REVENUE 

71 or line.outlet_stream_disposition == OutletStreamDisposition.SOLD 

72 ): 

73 return RESOURCE_CATEGORY_PRODUCT_OUTPUT 

74 if ( 

75 line.category == OperatingLineCategory.DISPOSAL 

76 or line.outlet_stream_disposition == OutletStreamDisposition.DISPOSED 

77 ): 

78 return RESOURCE_CATEGORY_WASTE_DISPOSAL 

79 if line.category != OperatingLineCategory.ENERGY: 

80 return "" 

81 source_kind = source_kind if source_kind is not None else operating_resource_source_kind(line=line) 

82 if source_kind == UNIT_COOLING_DUTY_SOURCE_KIND: 

83 return RESOURCE_CATEGORY_ANNUAL_COOLING_DUTY 

84 if source_kind == UNIT_HEATING_DUTY_SOURCE_KIND: 

85 return RESOURCE_CATEGORY_ANNUAL_HEATING_DUTY 

86 if source_kind == UNIT_POWER_WORK_SOURCE_KIND: 

87 return RESOURCE_CATEGORY_ANNUAL_WORK 

88 return "" 

89 

90 

91def steam_fuel_energy_from_default_rate( 

92 *, 

93 annual_basis_quantity: Decimal | None, 

94 annual_basis_unit: str, 

95 source_default_rate: Any, 

96 target_unit: str, 

97) -> Decimal | None: 

98 """Derive annual steam fuel energy from persisted basis and default-rate assumptions.""" 

99 useful_steam_energy = _steam_output_energy_gj_per_year( 

100 annual_basis_quantity=annual_basis_quantity, 

101 annual_basis_unit=annual_basis_unit, 

102 source_default_rate=source_default_rate, 

103 ) 

104 efficiency_percent = _steam_boiler_efficiency_percent(source_default_rate) 

105 if useful_steam_energy is None or efficiency_percent is None or efficiency_percent <= 0: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true

106 return None 

107 fuel_gj_per_year = useful_steam_energy / (efficiency_percent / Decimal("100")) 

108 if target_unit == "GJ/year": 108 ↛ 110line 108 didn't jump to line 110 because the condition on line 108 was always true

109 return fuel_gj_per_year 

110 return convert_quantity( 

111 value=fuel_gj_per_year, 

112 source_unit="GJ/year", 

113 target_unit=target_unit, 

114 ) 

115 

116 

117def annual_resource_quantity( 

118 *, 

119 annual_basis_quantity: Decimal | None, 

120 annual_basis_unit: str, 

121) -> AnnualResourceQuantity | None: 

122 """Return a persisted annual quantity without changing its magnitude.""" 

123 if annual_basis_quantity is None or not annual_basis_unit: 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true

124 return None 

125 return AnnualResourceQuantity( 

126 quantity=annual_basis_quantity, 

127 unit=_annual_unit(annual_basis_unit), 

128 ) 

129 

130 

131def converted_annual_resource_quantity( 

132 *, 

133 annual_basis_quantity: Decimal | None, 

134 annual_basis_unit: str, 

135 target_unit: str, 

136) -> Decimal | None: 

137 """Convert a persisted annual resource quantity into the requested unit.""" 

138 if annual_basis_quantity is None or not annual_basis_unit: 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true

139 return None 

140 return convert_quantity( 

141 value=annual_basis_quantity, 

142 source_unit=_annual_unit(annual_basis_unit), 

143 target_unit=target_unit, 

144 ) 

145 

146 

147def _steam_output_energy_gj_per_year( 

148 *, 

149 annual_basis_quantity: Decimal | None, 

150 annual_basis_unit: str, 

151 source_default_rate: Any, 

152) -> Decimal | None: 

153 annual_steam_tonnes = converted_annual_resource_quantity( 

154 annual_basis_quantity=annual_basis_quantity, 

155 annual_basis_unit=annual_basis_unit, 

156 target_unit="t/year", 

157 ) 

158 if annual_steam_tonnes is not None: 158 ↛ 161line 158 didn't jump to line 161 because the condition on line 158 was always true

159 steam_energy = _steam_energy_gj_per_t(source_default_rate) 

160 return annual_steam_tonnes * steam_energy if steam_energy is not None else None 

161 return converted_annual_resource_quantity( 

162 annual_basis_quantity=annual_basis_quantity, 

163 annual_basis_unit=annual_basis_unit, 

164 target_unit="GJ/year", 

165 ) 

166 

167 

168def _steam_energy_gj_per_t(source_default_rate: Any) -> Decimal | None: 

169 metadata = _steam_default_metadata(source_default_rate) 

170 return _decimal_or_none(metadata.get("steam_energy_gj_per_t")) 

171 

172 

173def _steam_boiler_efficiency_percent(source_default_rate: Any) -> Decimal | None: 

174 metadata = _steam_default_metadata(source_default_rate) 

175 return _decimal_or_none(metadata.get("default_boiler_efficiency_percent")) 

176 

177 

178def _steam_default_metadata(source_default_rate: Any) -> dict: 

179 return ( 

180 source_default_rate.metadata 

181 if source_default_rate is not None and isinstance(source_default_rate.metadata, dict) 

182 else {} 

183 ) 

184 

185 

186def _annual_unit(unit: str) -> str: 

187 current = unit.strip() 

188 if not current: 188 ↛ 189line 188 didn't jump to line 189 because the condition on line 188 was never true

189 return "" 

190 if "/" in current and current.rsplit("/", 1)[1].strip() in {"y", "yr", "year", "years"}: 

191 return current 

192 return f"{current}/year" 

193 

194 

195def _decimal_or_none(value: object) -> Decimal | None: 

196 if value in (None, ""): 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true

197 return None 

198 try: 

199 return Decimal(str(value)) 

200 except (ArithmeticError, InvalidOperation, ValueError): 

201 return None