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
« 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.
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"""
11from __future__ import annotations
13from dataclasses import dataclass
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, ...] = ()
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, ...]
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, ...] = ()
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}
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)
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)
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)