Coverage for backend/django/Economics/costing/cost_curves/registry.py: 100%

40 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-06-23 21:51 +0000

1""" 

2Source-controlled costing registry for built-in Economics support. 

3 

4To add a SCENZ equipment family, add one category label, one or more 

5``CostDriverRule`` entries for Ahuora object types, and reviewed curve templates 

6in ``cost_curve_catalog.py``. Rules should name exact property keys/unit types 

7and a manual fallback. The frontend reads the same backend payload and category 

8options, so supported-unit behavior should not be duplicated elsewhere. 

9""" 

10 

11from __future__ import annotations 

12 

13from dataclasses import dataclass 

14 

15 

16@dataclass(frozen=True) 

17class PreferredProperty: 

18 key: str 

19 unit_type: str 

20 canonical_unit: str | None = None 

21 curve_input_variable: str | None = None 

22 recommended_equipment_categories: tuple[str, ...] = () 

23 

24 

25@dataclass(frozen=True) 

26class ManualPropertySpec: 

27 key: str 

28 display_name: str 

29 unit_type: str 

30 canonical_unit: str 

31 tags: tuple[str, ...] 

32 

33 

34@dataclass(frozen=True) 

35class CostDriverRule: 

36 equipment_category: str 

37 compatible_object_types: tuple[str, ...] 

38 preferred_properties: tuple[PreferredProperty, ...] 

39 preferred_input_stream_properties: tuple[PreferredProperty, ...] 

40 manual_property: ManualPropertySpec 

41 curve_input_variable: str 

42 canonical_unit: str 

43 unresolved_reason_code: str 

44 warning_text: str 

45 manual_only: bool = False 

46 allowed_equipment_categories: tuple[str, ...] = () 

47 requires_equipment_category_selection: bool = False 

48 alternate_manual_properties: tuple[ManualPropertySpec, ...] = () 

49 

50 

51SCENZ_CATEGORY_LABELS = { 

52 "boiler": "Boiler", 

53 "compressor_blower": "Compressor / blower", 

54 "cooling_tower": "Cooling tower", 

55 "evaporator": "Evaporator", 

56 "heat_exchanger": "Heat exchanger", 

57 "liquid_storage_tank": "Liquid storage tank", 

58 "membrane_equipment": "Membrane equipment", 

59 "mixer": "Mixer", 

60 "pump": "Pump", 

61 "vessel": "Vessel", 

62} 

63 

64 

65MANUAL_VOLUME = ManualPropertySpec( 

66 key="economics.manual_driver_volume", 

67 display_name="Manual Cost Driver Volume", 

68 unit_type="volume", 

69 canonical_unit="m^3", 

70 tags=("driver:volume", "source:manual"), 

71) 

72MANUAL_AREA = ManualPropertySpec( 

73 key="economics.manual_driver_area", 

74 display_name="Manual Cost Driver Area", 

75 unit_type="area", 

76 canonical_unit="m^2", 

77 tags=("driver:area", "source:manual"), 

78) 

79MANUAL_VOLUMETRIC_FLOW = ManualPropertySpec( 

80 key="economics.manual_driver_volumetric_flow", 

81 display_name="Manual Cost Driver Volumetric Flow", 

82 unit_type="volumetricFlow", 

83 canonical_unit="m^3/h", 

84 tags=("driver:volumetric_flow", "source:manual"), 

85) 

86MANUAL_POWER = ManualPropertySpec( 

87 key="economics.manual_driver_power", 

88 display_name="Manual Cost Driver Power", 

89 unit_type="heatflow", 

90 canonical_unit="kW", 

91 tags=("driver:power", "source:manual"), 

92) 

93MANUAL_THERMAL_DUTY = ManualPropertySpec( 

94 key="economics.manual_driver_thermal_duty", 

95 display_name="Manual Cost Driver Thermal Duty", 

96 unit_type="heatflow", 

97 canonical_unit="kW", 

98 tags=("driver:thermal_duty", "source:manual"), 

99) 

100MANUAL_EVAPORATION_RATE = ManualPropertySpec( 

101 key="economics.manual_driver_evaporation_rate", 

102 display_name="Manual Cost Driver Evaporation Rate", 

103 unit_type="massflow", 

104 canonical_unit="tonne/hr", 

105 tags=("driver:evaporation_rate", "source:manual"), 

106) 

107 

108 

109COST_DRIVER_RULES: tuple[CostDriverRule, ...] = ( 

110 CostDriverRule( 

111 equipment_category="liquid_storage_tank", 

112 compatible_object_types=("Tank",), 

113 preferred_properties=(PreferredProperty(key="control_volume.volume", unit_type="volume"),), 

114 preferred_input_stream_properties=(), 

115 manual_property=MANUAL_VOLUME, 

116 curve_input_variable="V", 

117 canonical_unit="m^3", 

118 unresolved_reason_code="registry_did_not_match", 

119 warning_text=( 

120 "SCENZ liquid storage tank curves require tank volume in m^3. No tank volume was matched; " 

121 "enter a manual volume or choose a supported tank model with a volume property." 

122 ), 

123 ), 

124 CostDriverRule( 

125 equipment_category="heat_exchanger", 

126 compatible_object_types=("heatExchanger", "heat_exchanger_1d", "plate_heat_exchanger"), 

127 preferred_properties=(PreferredProperty(key="area", unit_type="area"),), 

128 preferred_input_stream_properties=(), 

129 manual_property=MANUAL_AREA, 

130 curve_input_variable="A", 

131 canonical_unit="m^2", 

132 unresolved_reason_code="registry_did_not_match", 

133 warning_text=( 

134 "SCENZ heat-exchanger curves require heat-transfer area in m^2. No area property was matched; " 

135 "enter a manual area or choose a supported heat-exchanger model with an area property." 

136 ), 

137 ), 

138 CostDriverRule( 

139 equipment_category="heat_exchanger", 

140 compatible_object_types=("heat_exchanger_ntu", "heat_exchanger_lc"), 

141 preferred_properties=(), 

142 preferred_input_stream_properties=(), 

143 manual_property=MANUAL_AREA, 

144 curve_input_variable="A", 

145 canonical_unit="m^2", 

146 unresolved_reason_code="registry_did_not_match", 

147 warning_text=( 

148 "SCENZ heat-exchanger curves require heat-transfer area in m^2. This heat-exchanger model " 

149 "does not expose a v1 area cost driver, so enter the design area manually." 

150 ), 

151 ), 

152 CostDriverRule( 

153 equipment_category="pump", 

154 compatible_object_types=("pump",), 

155 preferred_properties=(), 

156 preferred_input_stream_properties=(PreferredProperty(key="flow_vol", unit_type="volumetricFlow"),), 

157 manual_property=MANUAL_VOLUMETRIC_FLOW, 

158 curve_input_variable="v", 

159 canonical_unit="m^3/h", 

160 unresolved_reason_code="manual_override_required", 

161 warning_text=( 

162 "SCENZ pump curves require volumetric flow in m^3/h. The current pump unit does not expose " 

163 "a pump-level volumetric-flow cost driver, so enter the design flow manually." 

164 ), 

165 ), 

166 CostDriverRule( 

167 equipment_category="compressor_blower", 

168 compatible_object_types=("compressor",), 

169 preferred_properties=(PreferredProperty(key="work_mechanical", unit_type="heatflow"),), 

170 preferred_input_stream_properties=(), 

171 manual_property=MANUAL_POWER, 

172 curve_input_variable="wf", 

173 canonical_unit="kW", 

174 unresolved_reason_code="registry_did_not_match", 

175 warning_text=( 

176 "SCENZ compressor/blower curves require shaft or fluid work in kW. No compressor power " 

177 "property was matched; enter a manual power or choose a compressor model with mechanical work." 

178 ), 

179 ), 

180 CostDriverRule( 

181 equipment_category="", 

182 compatible_object_types=("heater",), 

183 preferred_properties=( 

184 PreferredProperty( 

185 key="heat_duty", 

186 unit_type="heatflow", 

187 recommended_equipment_categories=("boiler",), 

188 ), 

189 ), 

190 preferred_input_stream_properties=(), 

191 manual_property=MANUAL_THERMAL_DUTY, 

192 curve_input_variable="Q", 

193 canonical_unit="kW", 

194 unresolved_reason_code="equipment_category_required", 

195 warning_text=( 

196 "Heater capital costing requires a physical equipment category such as boiler or heat exchanger " 

197 "before a SCENZ curve can be selected." 

198 ), 

199 allowed_equipment_categories=("boiler", "heat_exchanger"), 

200 requires_equipment_category_selection=True, 

201 ), 

202 CostDriverRule( 

203 equipment_category="", 

204 compatible_object_types=("cooler",), 

205 preferred_properties=( 

206 PreferredProperty( 

207 key="heat_duty_inverted", 

208 unit_type="heatflow", 

209 recommended_equipment_categories=("cooling_tower",), 

210 ), 

211 PreferredProperty( 

212 key="heatRemoved", 

213 unit_type="heatflow", 

214 recommended_equipment_categories=("cooling_tower",), 

215 ), 

216 PreferredProperty( 

217 key="heat_duty", 

218 unit_type="heatflow", 

219 recommended_equipment_categories=("cooling_tower",), 

220 ), 

221 ), 

222 preferred_input_stream_properties=(), 

223 manual_property=MANUAL_THERMAL_DUTY, 

224 curve_input_variable="q", 

225 canonical_unit="kW", 

226 unresolved_reason_code="equipment_category_required", 

227 warning_text=( 

228 "Cooler capital costing requires a physical equipment category such as cooling tower or heat " 

229 "exchanger before a SCENZ curve can be selected." 

230 ), 

231 allowed_equipment_categories=("cooling_tower", "heat_exchanger"), 

232 requires_equipment_category_selection=True, 

233 ), 

234 CostDriverRule( 

235 equipment_category="mixer", 

236 compatible_object_types=("mixer",), 

237 preferred_properties=(), 

238 preferred_input_stream_properties=(), 

239 manual_property=MANUAL_POWER, 

240 curve_input_variable="P", 

241 canonical_unit="kW", 

242 unresolved_reason_code="manual_override_required", 

243 warning_text=( 

244 "SCENZ mixer curves require power consumption in kW. The current mixer unit does not expose " 

245 "a mixer power cost driver, so enter the design power manually." 

246 ), 

247 manual_only=True, 

248 ), 

249 CostDriverRule( 

250 equipment_category="membrane_equipment", 

251 compatible_object_types=("reverse_osmosis_0d",), 

252 preferred_properties=(PreferredProperty(key="area", unit_type="area"),), 

253 preferred_input_stream_properties=(), 

254 manual_property=MANUAL_AREA, 

255 curve_input_variable="A", 

256 canonical_unit="m^2", 

257 unresolved_reason_code="registry_did_not_match", 

258 warning_text=( 

259 "Reverse-osmosis costing prefers membrane area in m^2. No membrane area was matched; enter " 

260 "a manual membrane area or select a compatible flow driver for flow-based membrane curves." 

261 ), 

262 ), 

263 CostDriverRule( 

264 equipment_category="evaporator", 

265 compatible_object_types=("crystallizer",), 

266 preferred_properties=( 

267 PreferredProperty( 

268 key="vapor_phase_flow", 

269 unit_type="massflow", 

270 canonical_unit="tonne/hr", 

271 curve_input_variable="mw", 

272 ), 

273 PreferredProperty(key="heat_duty", unit_type="heatflow", canonical_unit="kW", curve_input_variable="Q"), 

274 ), 

275 preferred_input_stream_properties=(), 

276 manual_property=MANUAL_THERMAL_DUTY, 

277 curve_input_variable="Q", 

278 canonical_unit="kW", 

279 unresolved_reason_code="registry_did_not_match", 

280 warning_text=( 

281 "Crystallizer costing maps to evaporator-like equipment when service matches. No heat-duty " 

282 "driver was matched; enter a manual thermal duty or select evaporation-rate property manually." 

283 ), 

284 alternate_manual_properties=(MANUAL_EVAPORATION_RATE,), 

285 ), 

286 CostDriverRule( 

287 equipment_category="vessel", 

288 compatible_object_types=("phaseSeparator", "compoundSeparator"), 

289 preferred_properties=(), 

290 preferred_input_stream_properties=(), 

291 manual_property=MANUAL_VOLUME, 

292 curve_input_variable="V", 

293 canonical_unit="m^3", 

294 unresolved_reason_code="manual_override_required", 

295 warning_text=( 

296 "Separator costing is treated as vessel-like in v1 and requires manual vessel volume in m^3." 

297 ), 

298 manual_only=True, 

299 ), 

300) 

301 

302 

303SUPPORTED_UNIT_COST_OBJECT_TYPES = frozenset( 

304 object_type 

305 for rule in COST_DRIVER_RULES 

306 for object_type in rule.compatible_object_types 

307)