Coverage for backend/django/Economics/costing/cost_curves/catalog.py: 89%
122 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
1from __future__ import annotations
3from dataclasses import dataclass
4from typing import Iterable
6from Economics.shared.choices import CostBasis, CostCurveEvaluationKind
8from Economics.costing.models import CostCurve
9from Economics.costing.cost_curves.driver_specs import CostCurveDiscreteVariant, CostCurveDriverSpec
10from Economics.costing.cost_curves.registry import SCENZ_CATEGORY_LABELS
13SCENZ_SOURCE_TITLE = "Process capital cost estimation for New Zealand 2004"
14SCENZ_BASIS_DATE = "2004-12-31"
15SCENZ_BASIS_INDEX_NAME = "PMEI"
16SCENZ_BASIS_INDEX_VALUE = "988"
17SCENZ_LICENSE_STATUS = "awaiting-review"
18CUSTOM_EQUIPMENT_CATEGORY = "custom_equipment"
19CUSTOM_EQUIPMENT_CATEGORY_LABEL = "Custom equipment"
22@dataclass(frozen=True)
23class CostCurveTemplate:
24 value: str
25 name: str
26 equipment_category: str
27 equipment_subtype: str
28 cost_basis: str
29 evaluation_kind: str
30 output_unit: str
31 expression_text: str
32 required_driver_specs: tuple[CostCurveDriverSpec, ...]
33 discrete_variants: tuple[CostCurveDiscreteVariant, ...]
34 valid_min: str
35 valid_max: str
36 valid_range_note: str
37 currency: str
38 basis_date: str
39 basis_index_name: str
40 basis_index_value: str
41 source_document_title: str
42 source_page: str
43 source_figure: str
44 source_data_origin: str
45 source_range_precision: str
46 source_license_status: str
47 source_reference: str
48 notes: str
49 applicability_warning: str
50 active: bool = True
53@dataclass(frozen=True)
54class CostCurveEquipmentCategory:
55 value: str
56 label: str
57 subtypes: tuple[str, ...]
58 templates: tuple[CostCurveTemplate, ...]
61def _template(
62 *,
63 value: str,
64 name: str,
65 category: str,
66 subtype: str,
67 cost_basis: str = CostBasis.PURCHASE,
68 variable_symbol: str,
69 input_unit: str,
70 expression_text: str,
71 required_driver_specs: tuple[CostCurveDriverSpec, ...] | None = None,
72 valid_min: str,
73 valid_max: str,
74 valid_range_note: str,
75 source_page: str,
76 source_figure: str,
77 source_data_origin: str = "NZ",
78 notes: str,
79 applicability_warning: str,
80) -> CostCurveTemplate:
81 return CostCurveTemplate(
82 value=value,
83 name=name,
84 equipment_category=category,
85 equipment_subtype=subtype,
86 cost_basis=cost_basis,
87 evaluation_kind=CostCurveEvaluationKind.EXPRESSION,
88 output_unit="NZD",
89 expression_text=expression_text,
90 required_driver_specs=required_driver_specs
91 if required_driver_specs is not None
92 else (
93 CostCurveDriverSpec(
94 key="sizing_value",
95 label=_default_formula_input_label(
96 category=category,
97 variable_symbol=variable_symbol,
98 input_unit=input_unit,
99 ),
100 role="formula_input",
101 variable_symbol=variable_symbol,
102 unit=input_unit,
103 required=True,
104 primary=True,
105 ),
106 ),
107 discrete_variants=(),
108 valid_min=valid_min,
109 valid_max=valid_max,
110 valid_range_note=valid_range_note,
111 currency="NZD",
112 basis_date=SCENZ_BASIS_DATE,
113 basis_index_name=SCENZ_BASIS_INDEX_NAME,
114 basis_index_value=SCENZ_BASIS_INDEX_VALUE,
115 source_document_title=SCENZ_SOURCE_TITLE,
116 source_page=source_page,
117 source_figure=source_figure,
118 source_data_origin=source_data_origin,
119 source_range_precision="visual-inferred",
120 source_license_status=SCENZ_LICENSE_STATUS,
121 source_reference=f"SCENZ 2004 {source_page}, {source_figure}",
122 notes=notes,
123 applicability_warning=applicability_warning,
124)
127def _default_formula_input_label(*, category: str, variable_symbol: str, input_unit: str) -> str:
128 """Return process-facing labels for built-in expression curve variables."""
129 if variable_symbol == "V" and input_unit == "m^3":
130 return "Volume"
131 if variable_symbol == "A" and input_unit == "m^2":
132 return "Heat transfer area"
133 if variable_symbol == "v" and input_unit == "m^3/h":
134 return "Volumetric flow capacity"
135 if variable_symbol == "q" and input_unit == "m^3/s":
136 return "Cooling-water flow capacity"
137 if variable_symbol == "Q" and input_unit == "kW":
138 return "Heating duty capacity"
139 if variable_symbol == "wf" and input_unit == "kW":
140 return "Fluid power capacity"
141 if variable_symbol == "P" and input_unit == "kW":
142 return "Power capacity"
143 if variable_symbol == "mw" and input_unit == "tonne/hr": 143 ↛ 145line 143 didn't jump to line 145 because the condition on line 143 was always true
144 return "Evaporation capacity"
145 if category == "cooling_tower":
146 return "Cooling capacity"
147 return "Sizing value"
150def _formula_input_spec(*, key: str, label: str, symbol: str, unit: str, valid_min: str = "", valid_max: str = "", valid_range_note: str = "") -> CostCurveDriverSpec:
151 return CostCurveDriverSpec(
152 key=key,
153 label=label,
154 role="formula_input",
155 variable_symbol=symbol,
156 unit=unit,
157 required=True,
158 primary=True,
159 valid_min=valid_min,
160 valid_max=valid_max,
161 valid_range_note=valid_range_note,
162 )
165def _selector_spec(
166 *,
167 key: str,
168 label: str,
169 unit: str,
170 soft_maximum_adjustment_percent: str = "25",
171 valid_min: str = "",
172 valid_max: str = "",
173 valid_range_note: str = "",
174 source_options: tuple[str, ...] = ("property", "manual"),
175) -> CostCurveDriverSpec:
176 return CostCurveDriverSpec(
177 key=key,
178 label=label,
179 role="discrete_selector",
180 unit=unit,
181 required=True,
182 primary=False,
183 valid_min=valid_min,
184 valid_max=valid_max,
185 valid_range_note=valid_range_note,
186 soft_maximum_adjustment_percent=soft_maximum_adjustment_percent,
187 source_options=source_options,
188 )
191def _discrete_variant(
192 *,
193 key: str,
194 label: str,
195 selector_values: dict[str, str],
196 expression_text: str,
197 valid_min: str,
198 valid_max: str,
199 valid_range_note: str,
200 source_reference: str,
201 notes: str,
202) -> CostCurveDiscreteVariant:
203 return CostCurveDiscreteVariant(
204 key=key,
205 label=label,
206 selector_values=selector_values,
207 expression_text=expression_text,
208 valid_min=valid_min,
209 valid_max=valid_max,
210 valid_range_note=valid_range_note,
211 source_reference=source_reference,
212 notes=notes,
213 )
216def _discrete_family_template(
217 *,
218 value: str,
219 name: str,
220 category: str,
221 subtype: str,
222 formula_spec: CostCurveDriverSpec,
223 selector_specs: tuple[CostCurveDriverSpec, ...],
224 variants: tuple[CostCurveDiscreteVariant, ...],
225 valid_min: str,
226 valid_max: str,
227 valid_range_note: str,
228 source_page: str,
229 source_figure: str,
230 source_data_origin: str,
231 notes: str,
232 applicability_warning: str,
233) -> CostCurveTemplate:
234 return CostCurveTemplate(
235 value=value,
236 name=name,
237 equipment_category=category,
238 equipment_subtype=subtype,
239 cost_basis=CostBasis.PURCHASE,
240 evaluation_kind=CostCurveEvaluationKind.DISCRETE_FAMILY,
241 output_unit="NZD",
242 expression_text="",
243 required_driver_specs=(formula_spec, *selector_specs),
244 discrete_variants=variants,
245 valid_min=valid_min,
246 valid_max=valid_max,
247 valid_range_note=valid_range_note,
248 currency="NZD",
249 basis_date=SCENZ_BASIS_DATE,
250 basis_index_name=SCENZ_BASIS_INDEX_NAME,
251 basis_index_value=SCENZ_BASIS_INDEX_VALUE,
252 source_document_title=SCENZ_SOURCE_TITLE,
253 source_page=source_page,
254 source_figure=source_figure,
255 source_data_origin=source_data_origin,
256 source_range_precision="visual-inferred",
257 source_license_status=SCENZ_LICENSE_STATUS,
258 source_reference=f"SCENZ 2004 {source_page}, {source_figure}",
259 notes=notes,
260 applicability_warning=applicability_warning,
261 )
264def _vessel_template(
265 *,
266 value: str,
267 orientation: str,
268 diameter: str,
269 axial_key: str,
270 axial_label: str,
271 axial_symbol: str,
272 expression_text: str,
273 source_page: str,
274 source_figure: str,
275) -> CostCurveTemplate:
276 return _template(
277 value=value,
278 name=f"SCENZ vessel - {orientation} D {diameter} m",
279 category="vessel",
280 subtype=f"{orientation} vessel - D {diameter} m",
281 variable_symbol=axial_symbol,
282 input_unit="m",
283 # SCENZ Figures 8.23 and 8.24 publish one regression per fixed vessel
284 # diameter. The chosen subtype identifies that fixed diameter, so the
285 # only runtime driver is the equation variable: length or height.
286 required_driver_specs=(
287 CostCurveDriverSpec(
288 key=axial_key,
289 label=axial_label,
290 role="formula_input",
291 variable_symbol=axial_symbol,
292 unit="m",
293 required=True,
294 primary=True,
295 ),
296 ),
297 expression_text=expression_text,
298 valid_min="1",
299 valid_max="100",
300 valid_range_note=f"Visual-inferred {axial_key} range from {source_figure}; reviewer confirmation required.",
301 source_page=source_page,
302 source_figure=source_figure,
303 source_data_origin="US - Ulrich (2004)",
304 notes=(
305 f"Purchased {orientation.lower()} process-vessel cost for carbon steel construction "
306 "and internal pressure less than 4 bar gauge."
307 ),
308 applicability_warning=(
309 f"Preliminary estimate only; use only for matching {orientation.lower()} "
310 f"carbon-steel process vessels under 4 bar gauge. This {axial_key}-driven "
311 "curve requires vessel dimensions, not vessel volume."
312 ),
313 )
316def _horizontal_vessel_templates() -> tuple[CostCurveTemplate, ...]:
317 """Return the grouped fixed-diameter horizontal process-vessel family."""
318 return (
319 _discrete_family_template(
320 value="scenz_2004_vessel_horizontal",
321 name="SCENZ vessel - Horizontal",
322 category="vessel",
323 subtype="Horizontal vessel",
324 formula_spec=_formula_input_spec(
325 key="length",
326 label="Vessel length",
327 symbol="L",
328 unit="m",
329 valid_min="1",
330 valid_max="100",
331 valid_range_note="Visual-inferred length range from Figure 8.23; reviewer confirmation required.",
332 ),
333 selector_specs=(
334 _selector_spec(
335 key="diameter",
336 label="Vessel diameter",
337 unit="m",
338 soft_maximum_adjustment_percent="20",
339 valid_min="0.3",
340 valid_max="4.0",
341 valid_range_note="Published fixed-diameter curve grid from Figure 8.23.",
342 ),
343 ),
344 variants=tuple(
345 _discrete_variant(
346 key=f"diameter_{diameter.replace('.', '_')}_m",
347 label=f"D {diameter} m",
348 selector_values={"diameter": diameter},
349 expression_text=expression_text,
350 valid_min="1",
351 valid_max="100",
352 valid_range_note=f"Fixed-diameter horizontal vessel curve for D {diameter} m.",
353 source_reference="SCENZ 2004 PDF p.51, doc p.47, Figure 8.23",
354 notes="Purchased horizontal process-vessel cost for carbon steel construction and internal pressure less than 4 bar gauge.",
355 )
356 for diameter, expression_text in (
357 ("0.3", "1.14 * 10 ^ 3 * L ^ 0.701"),
358 ("0.5", "1.76 * 10 ^ 3 * L ^ 0.701"),
359 ("1.0", "3.71 * 10 ^ 3 * L ^ 0.667"),
360 ("1.5", "5.21 * 10 ^ 3 * L ^ 0.680"),
361 ("2.0", "5.92 * 10 ^ 3 * L ^ 0.767"),
362 ("2.5", "6.33 * 10 ^ 3 * L ^ 0.803"),
363 ("3.0", "6.56 * 10 ^ 3 * L ^ 0.887"),
364 ("4.0", "8.87 * 10 ^ 3 * L ^ 0.855"),
365 )
366 ),
367 valid_min="1",
368 valid_max="100",
369 valid_range_note="Grouped fixed-diameter curves from Figure 8.23.",
370 source_page="PDF p.51, doc p.47",
371 source_figure="Figure 8.23",
372 source_data_origin="US - Ulrich (2004)",
373 notes="Grouped purchased horizontal process-vessel curves for carbon steel construction and internal pressure less than 4 bar gauge.",
374 applicability_warning="Preliminary estimate only; use only for matching horizontal carbon-steel process vessels under 4 bar gauge.",
375 ),
376 )
379def _vertical_vessel_templates() -> tuple[CostCurveTemplate, ...]:
380 """Return the grouped fixed-diameter vertical process-vessel family."""
381 return (
382 _discrete_family_template(
383 value="scenz_2004_vessel_vertical",
384 name="SCENZ vessel - Vertical",
385 category="vessel",
386 subtype="Vertical vessel",
387 formula_spec=_formula_input_spec(
388 key="height",
389 label="Vessel height",
390 symbol="h",
391 unit="m",
392 valid_min="1",
393 valid_max="100",
394 valid_range_note="Visual-inferred height range from Figure 8.24; reviewer confirmation required.",
395 ),
396 selector_specs=(
397 _selector_spec(
398 key="diameter",
399 label="Vessel diameter",
400 unit="m",
401 soft_maximum_adjustment_percent="20",
402 valid_min="0.3",
403 valid_max="4.0",
404 valid_range_note="Published fixed-diameter curve grid from Figure 8.24.",
405 ),
406 ),
407 variants=tuple(
408 _discrete_variant(
409 key=f"diameter_{diameter.replace('.', '_')}_m",
410 label=f"D {diameter} m",
411 selector_values={"diameter": diameter},
412 expression_text=expression_text,
413 valid_min="1",
414 valid_max="100",
415 valid_range_note=f"Fixed-diameter vertical vessel curve for D {diameter} m.",
416 source_reference="SCENZ 2004 PDF p.52, doc p.48, Figure 8.24",
417 notes="Purchased vertical process-vessel cost for carbon steel construction and internal pressure less than 4 bar gauge.",
418 )
419 for diameter, expression_text in (
420 ("0.3", "2.64 * 10 ^ 3 * h ^ 0.985"),
421 ("0.5", "3.26 * 10 ^ 3 * h ^ 0.950"),
422 ("1.0", "4.72 * 10 ^ 3 * h ^ 0.895"),
423 ("1.5", "5.79 * 10 ^ 3 * h ^ 0.928"),
424 ("2.0", "8.80 * 10 ^ 3 * h ^ 0.847"),
425 ("2.5", "12.5 * 10 ^ 3 * h ^ 0.784"),
426 ("3.0", "10.9 * 10 ^ 3 * h ^ 0.870"),
427 ("4.0", "17.6 * 10 ^ 3 * h ^ 0.774"),
428 )
429 ),
430 valid_min="1",
431 valid_max="100",
432 valid_range_note="Grouped fixed-diameter curves from Figure 8.24.",
433 source_page="PDF p.52, doc p.48",
434 source_figure="Figure 8.24",
435 source_data_origin="US - Ulrich (2004)",
436 notes="Grouped purchased vertical process-vessel curves for carbon steel construction and internal pressure less than 4 bar gauge.",
437 applicability_warning="Preliminary estimate only; use only for matching vertical carbon-steel process vessels under 4 bar gauge.",
438 ),
439 )
442def _plate_hx_discrete_templates() -> tuple[CostCurveTemplate, ...]:
443 """Return SCENZ plate-HX families where source curves are discretised by service values."""
444 area_spec = _formula_input_spec(
445 key="area",
446 label="Heat transfer area",
447 symbol="A",
448 unit="m^2",
449 valid_min="0.1",
450 valid_max="60",
451 valid_range_note="Visual-inferred plate-HX area ranges from Figures 8.14-8.16.",
452 )
453 volumetric_flow_selector = _selector_spec(
454 key="volumetric_flow",
455 label="Volumetric flow capacity",
456 unit="m^3/h",
457 soft_maximum_adjustment_percent="10",
458 valid_min="3.6",
459 valid_max="12.7",
460 valid_range_note="Volumetric-flow curve grid from Figure 8.14.",
461 source_options=("manual",),
462 )
463 large_volumetric_flow_selector = _selector_spec(
464 key="volumetric_flow",
465 label="Volumetric flow capacity",
466 unit="m^3/h",
467 soft_maximum_adjustment_percent="10",
468 valid_min="39",
469 valid_max="140",
470 valid_range_note="Volumetric-flow curve grid from Figure 8.15.",
471 source_options=("manual",),
472 )
473 return (
474 _discrete_family_template(
475 value="scenz_2004_hx_brazed_small",
476 name="SCENZ HX - Brazed plate small",
477 category="heat_exchanger",
478 subtype="Plate - brazed small",
479 formula_spec=area_spec,
480 selector_specs=(volumetric_flow_selector,),
481 variants=tuple(
482 _discrete_variant(
483 key=f"flow_{flow.replace('.', '_')}_m3h",
484 label=f"{flow} m^3/h",
485 selector_values={"volumetric_flow": flow},
486 expression_text=expression_text,
487 valid_min=valid_min,
488 valid_max=valid_max,
489 valid_range_note=f"Flow qualifier <= {flow} m^3/h. Visual-inferred area range from Figure 8.14.",
490 source_reference="SCENZ 2004 PDF p.42, doc p.38, Figure 8.14",
491 notes="Purchased brazed plate heat exchanger cost. Copper brazing, AISI 316 stainless plates.",
492 )
493 for flow, expression_text, valid_min, valid_max in (
494 ("3.6", "578 * A + 114", "0.1", "0.5"),
495 ("8.1", "412 * A + 274", "0.2", "3"),
496 ("12.7", "272 * A + 247", "0.5", "5"),
497 )
498 ),
499 valid_min="0.1",
500 valid_max="5",
501 valid_range_note="Grouped volumetric-flow curves from Figure 8.14.",
502 source_page="PDF p.42, doc p.38",
503 source_figure="Figure 8.14",
504 source_data_origin="NZ",
505 notes="Grouped purchased brazed plate heat exchanger curves. Copper brazing, AISI 316 stainless plates.",
506 applicability_warning="Preliminary estimate only; use only for matching construction and plotted area/flow range.",
507 ),
508 _discrete_family_template(
509 value="scenz_2004_hx_brazed_large",
510 name="SCENZ HX - Brazed plate large",
511 category="heat_exchanger",
512 subtype="Plate - brazed large",
513 formula_spec=area_spec,
514 selector_specs=(large_volumetric_flow_selector,),
515 variants=tuple(
516 _discrete_variant(
517 key=f"flow_{flow}_m3h",
518 label=f"{flow} m^3/h",
519 selector_values={"volumetric_flow": flow},
520 expression_text=expression_text,
521 valid_min=valid_min,
522 valid_max=valid_max,
523 valid_range_note=f"Flow qualifier <= {flow} m^3/h. Visual-inferred area range from Figure 8.15.",
524 source_reference="SCENZ 2004 PDF p.43, doc p.39, Figure 8.15",
525 notes="Purchased brazed plate heat exchanger cost. Copper brazing, AISI 316 stainless plates.",
526 )
527 for flow, expression_text, valid_min, valid_max in (
528 ("39", "301 * A + 578", "1", "15"),
529 ("102", "162 * A + 2.15 * 10 ^ 3", "5", "30"),
530 ("140", "189 * A + 3.17 * 10 ^ 3", "8", "60"),
531 )
532 ),
533 valid_min="1",
534 valid_max="60",
535 valid_range_note="Grouped volumetric-flow curves from Figure 8.15.",
536 source_page="PDF p.43, doc p.39",
537 source_figure="Figure 8.15",
538 source_data_origin="NZ",
539 notes="Grouped purchased brazed plate heat exchanger curves. Copper brazing, AISI 316 stainless plates.",
540 applicability_warning="Preliminary estimate only; do not use outside plotted area/flow range.",
541 ),
542 )
545SCENZ_COST_CURVE_TEMPLATES: tuple[CostCurveTemplate, ...] = (
546 _template(
547 value="scenz_2004_tank_stainless_steel",
548 name="SCENZ tank - Stainless steel",
549 category="liquid_storage_tank",
550 subtype="Stainless steel",
551 variable_symbol="V",
552 input_unit="m^3",
553 expression_text="2.48 * 10 ^ 3 * V ^ 0.597",
554 valid_min="0.2",
555 valid_max="50",
556 valid_range_note="Visual-inferred range from Figure 8.38; reviewer confirmation required.",
557 source_page="PDF p.66, doc p.62",
558 source_figure="Figure 8.38",
559 notes="Purchased liquid storage tank cost. R2 0.995, df 5.",
560 applicability_warning="Preliminary estimate only; do not use outside plotted volume range or for pressure/process-vessel service.",
561 ),
562 _template(
563 value="scenz_2004_tank_polyethylene",
564 name="SCENZ tank - Polyethylene",
565 category="liquid_storage_tank",
566 subtype="Polyethylene",
567 variable_symbol="V",
568 input_unit="m^3",
569 expression_text="358 * V ^ 0.609",
570 valid_min="0.5",
571 valid_max="30",
572 valid_range_note="Visual-inferred range from Figure 8.38; reviewer confirmation required.",
573 source_page="PDF p.66, doc p.62",
574 source_figure="Figure 8.38",
575 notes="Purchased liquid storage tank cost. R2 0.969, df 8.",
576 applicability_warning="Preliminary estimate only; material/service compatibility is not validated by the curve.",
577 ),
578 _template(
579 value="scenz_2004_tank_frp",
580 name="SCENZ tank - Fibre reinforced plastic",
581 category="liquid_storage_tank",
582 subtype="Fibre reinforced plastic",
583 variable_symbol="V",
584 input_unit="m^3",
585 expression_text="960 * V + 7.00 * 10 ^ 3",
586 valid_min="10",
587 valid_max="50",
588 valid_range_note="Visual-inferred range from Figure 8.38; reviewer confirmation required.",
589 source_page="PDF p.66, doc p.62",
590 source_figure="Figure 8.38",
591 notes="Purchased liquid storage tank cost. R2 0.985, df 2.",
592 applicability_warning="Preliminary estimate only; small data set, df 2. Warn if used outside the short plotted range.",
593 ),
594 _template(
595 value="scenz_2004_tank_timber",
596 name="SCENZ tank - Timber",
597 category="liquid_storage_tank",
598 subtype="Timber",
599 variable_symbol="V",
600 input_unit="m^3",
601 expression_text="555 * V ^ 0.795",
602 valid_min="10",
603 valid_max="7000",
604 valid_range_note="Visual-inferred range from Figure 8.38; reviewer confirmation required.",
605 source_page="PDF p.66, doc p.62",
606 source_figure="Figure 8.38",
607 notes="Purchased liquid storage tank cost. R2 0.993, df 9.",
608 applicability_warning="Preliminary estimate only; timber tank applicability must be user-confirmed for service, site, and construction assumptions.",
609 ),
610 *_plate_hx_discrete_templates(),
611 _template(
612 value="scenz_2004_hx_shell_tube_double_pipe",
613 name="SCENZ HX - Shell and tube double pipe",
614 category="heat_exchanger",
615 subtype="Shell and tube - double pipe",
616 variable_symbol="A",
617 input_unit="m^2",
618 expression_text="2.88 * 10 ^ 3 * A ^ 0.539",
619 valid_min="0.5",
620 valid_max="10",
621 valid_range_note="Visual-inferred range from Figure 8.17; reviewer confirmation required.",
622 source_page="PDF p.45, doc p.41",
623 source_figure="Figure 8.17",
624 source_data_origin="US - Ulrich (2004)",
625 notes="Purchased shell-and-tube exchanger cost. R2 and df not shown in source.",
626 applicability_warning="Preliminary estimate only; source rights and applicability for NZ projects must be reviewed.",
627 ),
628 _template(
629 value="scenz_2004_hx_shell_tube_fixed_u_tube",
630 name="SCENZ HX - Shell and tube fixed/U-tube",
631 category="heat_exchanger",
632 subtype="Shell and tube - fixed tube sheet and U-tube",
633 variable_symbol="A",
634 input_unit="m^2",
635 expression_text="1.53 * 10 ^ 3 * A ^ 0.566",
636 valid_min="3",
637 valid_max="1000",
638 valid_range_note="Visual-inferred range from Figure 8.17; reviewer confirmation required.",
639 source_page="PDF p.45, doc p.41",
640 source_figure="Figure 8.17",
641 source_data_origin="US - Ulrich (2004)",
642 notes="Purchased shell-and-tube exchanger cost. R2 and df not shown in source.",
643 applicability_warning="Preliminary estimate only; material and pressure factors are cited externally and are not included by default.",
644 ),
645 _template(
646 value="scenz_2004_hx_shell_tube_floating_head",
647 name="SCENZ HX - Shell and tube floating head",
648 category="heat_exchanger",
649 subtype="Shell and tube - floating head",
650 variable_symbol="A",
651 input_unit="m^2",
652 expression_text="1.77 * 10 ^ 3 * A ^ 0.578",
653 valid_min="10",
654 valid_max="1000",
655 valid_range_note="Visual-inferred range from Figure 8.17; reviewer confirmation required.",
656 source_page="PDF p.45, doc p.41",
657 source_figure="Figure 8.17",
658 source_data_origin="US - Ulrich (2004)",
659 notes="Purchased shell-and-tube exchanger cost. R2 and df not shown in source.",
660 applicability_warning="Preliminary estimate only; no pressure/material factor is included by default.",
661 ),
662 _template(
663 value="scenz_2004_hx_shell_tube_kettle_reboiler",
664 name="SCENZ HX - Shell and tube kettle reboiler",
665 category="heat_exchanger",
666 subtype="Shell and tube - kettle reboiler",
667 variable_symbol="A",
668 input_unit="m^2",
669 expression_text="3.47 * 10 ^ 3 * A ^ 0.508",
670 valid_min="10",
671 valid_max="100",
672 valid_range_note="Visual-inferred range from Figure 8.17; reviewer confirmation required.",
673 source_page="PDF p.45, doc p.41",
674 source_figure="Figure 8.17",
675 source_data_origin="US - Ulrich (2004)",
676 notes="Purchased shell-and-tube exchanger cost. R2 and df not shown in source.",
677 applicability_warning="Preliminary estimate only; reboiler service match must be user-confirmed.",
678 ),
679 _template(
680 value="scenz_2004_hx_shell_tube_scraped_wall",
681 name="SCENZ HX - Shell and tube scraped wall",
682 category="heat_exchanger",
683 subtype="Shell and tube - scraped wall",
684 variable_symbol="A",
685 input_unit="m^2",
686 expression_text="7.90 * 10 ^ 3 * A ^ 0.964",
687 valid_min="2",
688 valid_max="20",
689 valid_range_note="Visual-inferred range from Figure 8.17; reviewer confirmation required.",
690 source_page="PDF p.45, doc p.41",
691 source_figure="Figure 8.17",
692 source_data_origin="US - Ulrich (2004)",
693 notes="Purchased shell-and-tube exchanger cost. R2 and df not shown in source.",
694 applicability_warning="Preliminary estimate only; scraped-wall applicability is specialized and must not be auto-selected from generic heat exchanger units.",
695 ),
696 *_horizontal_vessel_templates(),
697 *_vertical_vessel_templates(),
698 _template(
699 value="scenz_2004_pump_centrifugal_installed",
700 name="SCENZ pump - Centrifugal installed",
701 category="pump",
702 subtype="Centrifugal - installed",
703 cost_basis=CostBasis.INSTALLED,
704 variable_symbol="v",
705 input_unit="m^3/h",
706 expression_text="111 * v + 3.71 * 10 ^ 3",
707 valid_min="10",
708 valid_max="100",
709 valid_range_note="Visual-inferred range from Figure 8.25; reviewer confirmation required.",
710 source_page="PDF p.53, doc p.49",
711 source_figure="Figure 8.25",
712 notes="Installed centrifugal pump cost. R2 0.996, df 2. Installed cost does not include piping, valves, or instrumentation.",
713 applicability_warning="Preliminary estimate only; installed basis is partial, not full installed plant cost.",
714 ),
715 _template(
716 value="scenz_2004_pump_centrifugal_uninstalled",
717 name="SCENZ pump - Centrifugal uninstalled",
718 category="pump",
719 subtype="Centrifugal - uninstalled",
720 variable_symbol="v",
721 input_unit="m^3/h",
722 expression_text="127 * v + 1.72 * 10 ^ 3",
723 valid_min="1",
724 valid_max="25",
725 valid_range_note="Visual-inferred range from Figure 8.25; reviewer confirmation required.",
726 source_page="PDF p.53, doc p.49",
727 source_figure="Figure 8.25",
728 notes="Purchased centrifugal pump cost. R2 0.994, df 1.",
729 applicability_warning="Preliminary estimate only; small data set, df 1.",
730 ),
731 _template(
732 value="scenz_2004_pump_positive_displacement",
733 name="SCENZ pump - Positive displacement",
734 category="pump",
735 subtype="Positive displacement",
736 cost_basis=CostBasis.PURCHASE,
737 variable_symbol="v",
738 input_unit="m^3/h",
739 expression_text="548 * v + 13.0 * 10 ^ 3",
740 valid_min="6",
741 valid_max="70",
742 valid_range_note="Visual-inferred range from Figure 8.25; purchase-vs-installed basis needs reviewer signoff.",
743 source_page="PDF p.53, doc p.49",
744 source_figure="Figure 8.25",
745 notes="Pump basis must be confirmed before use. R2 0.994, df 1.",
746 applicability_warning="Preliminary estimate only; basis ambiguity must be visible until resolved.",
747 ),
748 _template(
749 value="scenz_2004_pump_progressive_cavity",
750 name="SCENZ pump - Progressive cavity",
751 category="pump",
752 subtype="Progressive cavity",
753 cost_basis=CostBasis.PURCHASE,
754 variable_symbol="v",
755 input_unit="m^3/h",
756 expression_text="4.22 * 10 ^ 3 * v ^ 0.379",
757 valid_min="2",
758 valid_max="7",
759 valid_range_note="Visual-inferred range from Figure 8.25; purchase-vs-installed basis needs reviewer signoff.",
760 source_page="PDF p.53, doc p.49",
761 source_figure="Figure 8.25",
762 notes="Pump basis must be confirmed before use. R2 0.894, df 1.",
763 applicability_warning="Preliminary estimate only; weak regression quality and basis ambiguity should be warning-grade metadata.",
764 ),
765 _template(
766 value="scenz_2004_pump_dosing_piston_stainless",
767 name="SCENZ pump - Dosing piston stainless steel",
768 category="pump",
769 subtype="Dosing - piston stainless steel",
770 variable_symbol="v",
771 input_unit="m^3/h",
772 expression_text="458 * v + 6.79 * 10 ^ 3",
773 valid_min="1",
774 valid_max="50",
775 valid_range_note="Visual-inferred range from Figure 8.26; reviewer confirmation required.",
776 source_page="PDF p.54, doc p.50",
777 source_figure="Figure 8.26",
778 notes="Purchased dosing piston stainless steel pump cost. R2 0.994, df 2.",
779 applicability_warning="Preliminary estimate only; material/pump type must match user selection.",
780 ),
781 _template(
782 value="scenz_2004_pump_dosing_mono_peristaltic",
783 name="SCENZ pump - Dosing mono/peristaltic",
784 category="pump",
785 subtype="Dosing - mono/peristaltic",
786 variable_symbol="v",
787 input_unit="m^3/h",
788 expression_text="615 * v + 3.81 * 10 ^ 3",
789 valid_min="1",
790 valid_max="50",
791 valid_range_note="Visual-inferred range from Figure 8.26; reviewer confirmation required.",
792 source_page="PDF p.54, doc p.50",
793 source_figure="Figure 8.26",
794 notes="Purchased dosing mono/peristaltic pump cost. R2 0.997, df 2.",
795 applicability_warning="Preliminary estimate only; do not auto-select from generic pump unless user confirms dosing/mono-peristaltic service.",
796 ),
797 _template(
798 value="scenz_2004_pump_dosing_diaphragm",
799 name="SCENZ pump - Dosing diaphragm",
800 category="pump",
801 subtype="Dosing - diaphragm",
802 variable_symbol="v",
803 input_unit="m^3/h",
804 expression_text="1.24 * 10 ^ 3 * v ^ 0.603",
805 valid_min="1",
806 valid_max="50",
807 valid_range_note="Visual-inferred range from Figure 8.26; reviewer confirmation required.",
808 source_page="PDF p.54, doc p.50",
809 source_figure="Figure 8.26",
810 notes="Purchased dosing diaphragm pump cost. R2 0.914, df 6.",
811 applicability_warning="Preliminary estimate only; warn about lower R2 than other dosing curves.",
812 ),
813 _template(
814 value="scenz_2004_pump_vacuum_rotary_vane",
815 name="SCENZ pump - Vacuum rotary vane",
816 category="pump",
817 subtype="Vacuum - rotary vane",
818 cost_basis=CostBasis.INSTALLED,
819 variable_symbol="v",
820 input_unit="m^3/h",
821 expression_text="375 * v + 20.4 * 10 ^ 3",
822 valid_min="10",
823 valid_max="50",
824 valid_range_note="Visual-inferred range from Figure 8.27; reviewer confirmation required.",
825 source_page="PDF p.55, doc p.51",
826 source_figure="Figure 8.27",
827 notes="Installed vacuum rotary vane pump cost. R2 0.964, df 1. Installed cost does not include piping, valves, or instrumentation.",
828 applicability_warning="Preliminary estimate only; partial installed basis, small data set.",
829 ),
830 _template(
831 value="scenz_2004_pump_vacuum_liquid_ring",
832 name="SCENZ pump - Vacuum liquid ring",
833 category="pump",
834 subtype="Vacuum - liquid ring",
835 cost_basis=CostBasis.INSTALLED,
836 variable_symbol="v",
837 input_unit="m^3/h",
838 expression_text="1.30 * 10 ^ 3 * v ^ 0.687",
839 valid_min="20",
840 valid_max="100",
841 valid_range_note="Visual-inferred range from Figure 8.27; reviewer confirmation required.",
842 source_page="PDF p.55, doc p.51",
843 source_figure="Figure 8.27",
844 notes="Installed vacuum liquid ring pump cost. R2 0.995, df 1. Installed cost does not include piping, valves, or instrumentation.",
845 applicability_warning="Preliminary estimate only; partial installed basis, small data set.",
846 ),
847 _template(
848 value="scenz_2004_compressor_reciprocal_diaphragm",
849 name="SCENZ compressor - Reciprocal diaphragm",
850 category="compressor_blower",
851 subtype="Reciprocal diaphragm",
852 variable_symbol="wf",
853 input_unit="kW",
854 expression_text="1.96 * 10 ^ 3 * wf + 11.5 * 10 ^ 3",
855 valid_min="10",
856 valid_max="10000",
857 valid_range_note="Visual-inferred power range from Figure 8.1; reviewer confirmation required.",
858 source_page="PDF p.29, doc p.25",
859 source_figure="Figure 8.1",
860 notes="Purchased blower/compressor cost by fluid power. Source equation transcribed from SCENZ Figure 8.1.",
861 applicability_warning="Preliminary estimate only; compressor type, pressure ratio, materials, and driver scope must match the selected service.",
862 ),
863 _template(
864 value="scenz_2004_compressor_centrifugal_axial",
865 name="SCENZ compressor - Centrifugal/axial",
866 category="compressor_blower",
867 subtype="Centrifugal (turbo) and axial",
868 variable_symbol="wf",
869 input_unit="kW",
870 expression_text="962 * wf + 14.0 * 10 ^ 3",
871 valid_min="10",
872 valid_max="10000",
873 valid_range_note="Visual-inferred power range from Figure 8.1; reviewer confirmation required.",
874 source_page="PDF p.29, doc p.25",
875 source_figure="Figure 8.1",
876 notes="Purchased blower/compressor cost by fluid power. Source equation transcribed from SCENZ Figure 8.1.",
877 applicability_warning="Preliminary estimate only; do not use for positive-displacement compressor service without user confirmation.",
878 ),
879 _template(
880 value="scenz_2004_boiler_hot_water_electric",
881 name="SCENZ boiler - Hot water electric",
882 category="boiler",
883 subtype="Hot water - electric",
884 variable_symbol="Q",
885 input_unit="kW",
886 expression_text="334 * Q ^ 0.813",
887 valid_min="10",
888 valid_max="10000",
889 valid_range_note="Visual-inferred heating-duty range from Figure 8.2; reviewer confirmation required.",
890 source_page="PDF p.30, doc p.26",
891 source_figure="Figure 8.2",
892 source_data_origin="NZ",
893 notes="Purchased hot-water boiler cost. R2 0.994, df 1.",
894 applicability_warning="Preliminary estimate only; electric boiler package scope and site electrical requirements must match the selected curve.",
895 ),
896 _template(
897 value="scenz_2004_boiler_hot_water_oil_gas",
898 name="SCENZ boiler - Hot water oil/gas",
899 category="boiler",
900 subtype="Hot water - oil/gas fired",
901 variable_symbol="Q",
902 input_unit="kW",
903 expression_text="52.4 * Q + 4.99 * 10 ^ 3",
904 valid_min="10",
905 valid_max="10000",
906 valid_range_note="Visual-inferred heating-duty range from Figure 8.2; reviewer confirmation required.",
907 source_page="PDF p.30, doc p.26",
908 source_figure="Figure 8.2",
909 source_data_origin="NZ",
910 notes="Purchased hot-water boiler cost. R2 1.00, df 1.",
911 applicability_warning="Preliminary estimate only; fuel, package scope, pressure, and hot-water service must match the selected curve.",
912 ),
913 _template(
914 value="scenz_2004_boiler_hot_water_coal",
915 name="SCENZ boiler - Hot water coal",
916 category="boiler",
917 subtype="Hot water - coal fired",
918 variable_symbol="Q",
919 input_unit="kW",
920 expression_text="42.0 * Q + 24.9 * 10 ^ 3",
921 valid_min="10",
922 valid_max="10000",
923 valid_range_note="Visual-inferred heating-duty range from Figure 8.2; reviewer confirmation required.",
924 source_page="PDF p.30, doc p.26",
925 source_figure="Figure 8.2",
926 source_data_origin="NZ",
927 notes="Purchased hot-water boiler cost. R2 0.991, df 2.",
928 applicability_warning="Preliminary estimate only; fuel handling, emissions controls, and site-specific ancillaries may not be included.",
929 ),
930 _template(
931 value="scenz_2004_boiler_hot_water_diesel",
932 name="SCENZ boiler - Hot water diesel",
933 category="boiler",
934 subtype="Hot water - diesel fired",
935 variable_symbol="Q",
936 input_unit="kW",
937 expression_text="19.9 * Q + 5.66 * 10 ^ 3",
938 valid_min="10",
939 valid_max="10000",
940 valid_range_note="Visual-inferred heating-duty range from Figure 8.2; reviewer confirmation required.",
941 source_page="PDF p.30, doc p.26",
942 source_figure="Figure 8.2",
943 source_data_origin="NZ",
944 notes="Purchased hot-water boiler cost. R2 0.988, df 2.",
945 applicability_warning="Preliminary estimate only; fuel system, emissions controls, package scope, and hot-water service must match the selected curve.",
946 ),
947 _template(
948 value="scenz_2004_boiler_hot_water_lpg",
949 name="SCENZ boiler - Hot water LPG",
950 category="boiler",
951 subtype="Hot water - LPG fired",
952 variable_symbol="Q",
953 input_unit="kW",
954 expression_text="21.1 * Q + 7.38 * 10 ^ 3",
955 valid_min="10",
956 valid_max="10000",
957 valid_range_note="Visual-inferred heating-duty range from Figure 8.2; reviewer confirmation required.",
958 source_page="PDF p.30, doc p.26",
959 source_figure="Figure 8.2",
960 source_data_origin="NZ",
961 notes="Purchased hot-water boiler cost. R2 0.982, df 2.",
962 applicability_warning="Preliminary estimate only; LPG storage/supply, package scope, and hot-water service must match the selected curve.",
963 ),
964 _template(
965 value="scenz_2004_boiler_steam_oil_gas",
966 name="SCENZ boiler - Steam oil/gas",
967 category="boiler",
968 subtype="Steam - oil/gas fired water-tube",
969 variable_symbol="Q",
970 input_unit="kW",
971 expression_text="532 * Q ^ 0.805",
972 valid_min="10000",
973 valid_max="1000000",
974 valid_range_note="Visual-inferred heating-duty range from Figure 8.3; oil/gas data is for 2 MPa gauge service.",
975 source_page="PDF p.31, doc p.27",
976 source_figure="Figure 8.3",
977 source_data_origin="US - Ulrich (2004)",
978 notes="Purchased field-erected water-tube steam boiler cost.",
979 applicability_warning="Preliminary estimate only; steam pressure, fuel, field-erection scope, and boiler auxiliaries must be reviewed.",
980 ),
981 _template(
982 value="scenz_2004_boiler_steam_coal",
983 name="SCENZ boiler - Steam coal",
984 category="boiler",
985 subtype="Steam - coal fired water-tube",
986 variable_symbol="Q",
987 input_unit="kW",
988 expression_text="10.0 * Q ^ 1.25",
989 valid_min="10000",
990 valid_max="1000000",
991 valid_range_note="Visual-inferred heating-duty range from Figure 8.3; coal-fired data is for 4 MPa gauge service.",
992 source_page="PDF p.31, doc p.27",
993 source_figure="Figure 8.3",
994 source_data_origin="NZ",
995 notes="Purchased field-erected water-tube steam boiler cost. R2 0.995, df 3.",
996 applicability_warning="Preliminary estimate only; coal handling, emissions controls, ash handling, and auxiliaries may not be included.",
997 ),
998 _template(
999 value="scenz_2004_evaporator_falling_film",
1000 name="SCENZ evaporator - Falling film",
1001 category="evaporator",
1002 subtype="Falling film",
1003 variable_symbol="mw",
1004 input_unit="tonne/hr",
1005 expression_text="632 * 10 ^ 3 * mw ^ 0.597",
1006 valid_min="1",
1007 valid_max="1000",
1008 valid_range_note="Visual-inferred water-evaporation range from Figure 8.12; reviewer confirmation required.",
1009 source_page="PDF p.40, doc p.36",
1010 source_figure="Figure 8.12",
1011 source_data_origin="NZ",
1012 notes="Purchased falling-film evaporator cost. R2 0.989, df 2.",
1013 applicability_warning="Preliminary estimate only; use for crystallizers only when evaporation service and equipment scope match.",
1014 ),
1015 _template(
1016 value="scenz_2004_membrane_ultrafiltration_installed",
1017 name="SCENZ membrane - Ultrafiltration installed",
1018 category="membrane_equipment",
1019 subtype="Ultrafiltration spiral membrane plant",
1020 cost_basis=CostBasis.INSTALLED,
1021 variable_symbol="v",
1022 input_unit="m^3/h",
1023 expression_text="15.2 * 10 ^ 3 * v ^ 0.947",
1024 valid_min="10",
1025 valid_max="1000",
1026 valid_range_note="Visual-inferred volumetric-flow range from Figure 8.20; reviewer confirmation required.",
1027 source_page="PDF p.48, doc p.44",
1028 source_figure="Figure 8.20",
1029 source_data_origin="NZ",
1030 notes="Installed ultrafiltration plant cost. R2 0.998, df 1. Installed basis excludes piping, valves, and instrumentation.",
1031 applicability_warning="Preliminary estimate only; this is an ultrafiltration flow curve, not a reverse-osmosis membrane-area curve.",
1032 ),
1033 _template(
1034 value="scenz_2004_mixer_agitator_helical_anchor",
1035 name="SCENZ mixer - Agitator helical/anchor",
1036 category="mixer",
1037 subtype="Agitator - helical or anchor",
1038 variable_symbol="P",
1039 input_unit="kW",
1040 expression_text="2.20 * 10 ^ 3 * P + 21.5 * 10 ^ 3",
1041 valid_min="1",
1042 valid_max="1000",
1043 valid_range_note="Visual-inferred power range from Figure 8.21; reviewer confirmation required.",
1044 source_page="PDF p.49, doc p.45",
1045 source_figure="Figure 8.21",
1046 source_data_origin="US - Ulrich (2004)",
1047 notes="Purchased agitator cost including motor, speed reducer, and impeller ready for vessel installation.",
1048 applicability_warning="Preliminary estimate only; vessel cost is separate and helical/anchor agitator service must be confirmed.",
1049 ),
1050 _template(
1051 value="scenz_2004_mixer_agitator_mechanical_seal",
1052 name="SCENZ mixer - Agitator mechanical seal",
1053 category="mixer",
1054 subtype="Agitator - mechanical seal",
1055 variable_symbol="P",
1056 input_unit="kW",
1057 expression_text="1.70 * 10 ^ 3 * P + 14.5 * 10 ^ 3",
1058 valid_min="1",
1059 valid_max="1000",
1060 valid_range_note="Visual-inferred power range from Figure 8.21; reviewer confirmation required.",
1061 source_page="PDF p.49, doc p.45",
1062 source_figure="Figure 8.21",
1063 source_data_origin="US - Ulrich (2004)",
1064 notes="Purchased agitator cost including motor, speed reducer, and impeller; mechanical seals are suitable for toxic or critical fluids up to 80 bar gauge.",
1065 applicability_warning="Preliminary estimate only; vessel cost is separate and seal/service assumptions must match the selected curve.",
1066 ),
1067 _template(
1068 value="scenz_2004_mixer_agitator_stuffing_box",
1069 name="SCENZ mixer - Agitator stuffing box",
1070 category="mixer",
1071 subtype="Agitator - stuffing box",
1072 variable_symbol="P",
1073 input_unit="kW",
1074 expression_text="1.50 * 10 ^ 3 * P + 6.65 * 10 ^ 3",
1075 valid_min="1",
1076 valid_max="1000",
1077 valid_range_note="Visual-inferred power range from Figure 8.21; reviewer confirmation required.",
1078 source_page="PDF p.49, doc p.45",
1079 source_figure="Figure 8.21",
1080 source_data_origin="US - Ulrich (2004)",
1081 notes="Purchased agitator cost including motor, speed reducer, and impeller; stuffing-box seals are suitable up to 10 bar gauge.",
1082 applicability_warning="Preliminary estimate only; vessel cost is separate and seal/service assumptions must match the selected curve.",
1083 ),
1084 _template(
1085 value="scenz_2004_mixer_agitator_open_tank",
1086 name="SCENZ mixer - Agitator open tank",
1087 category="mixer",
1088 subtype="Agitator - open tank",
1089 variable_symbol="P",
1090 input_unit="kW",
1091 expression_text="1.13 * 10 ^ 3 * P + 3.91 * 10 ^ 3",
1092 valid_min="1",
1093 valid_max="1000",
1094 valid_range_note="Visual-inferred power range from Figure 8.21; reviewer confirmation required.",
1095 source_page="PDF p.49, doc p.45",
1096 source_figure="Figure 8.21",
1097 source_data_origin="NZ",
1098 notes="Purchased agitator cost including motor, speed reducer, and impeller ready for vessel installation.",
1099 applicability_warning="Preliminary estimate only; vessel cost is separate and should not be double counted.",
1100 ),
1101 _template(
1102 value="scenz_2004_mixer_inline",
1103 name="SCENZ mixer - Inline mixer",
1104 category="mixer",
1105 subtype="Inline mixer",
1106 variable_symbol="P",
1107 input_unit="kW",
1108 expression_text="7.03 * 10 ^ 3 * P ^ 0.467",
1109 valid_min="1",
1110 valid_max="1000",
1111 valid_range_note="Visual-inferred power range from Figure 8.21; reviewer confirmation required.",
1112 source_page="PDF p.49, doc p.45",
1113 source_figure="Figure 8.21",
1114 source_data_origin="US - Ulrich (2004)",
1115 notes="Purchased inline mixer cost by power consumption.",
1116 applicability_warning="Preliminary estimate only; confirm inline service and mixer scope before applying.",
1117 ),
1118 _template(
1119 value="scenz_2004_mixer_heavy_duty_rotor",
1120 name="SCENZ mixer - Heavy duty rotor",
1121 category="mixer",
1122 subtype="Heavy duty - rotor",
1123 variable_symbol="P",
1124 input_unit="kW",
1125 expression_text="28.9 * 10 ^ 3 * P ^ 0.681",
1126 valid_min="1",
1127 valid_max="10000",
1128 valid_range_note="Visual-inferred power range from Figure 8.22; reviewer confirmation required.",
1129 source_page="PDF p.50, doc p.46",
1130 source_figure="Figure 8.22",
1131 source_data_origin="US - Ulrich (2004)",
1132 notes="Purchased heavy-duty mixer cost including drives for doughs, pastes, and powders.",
1133 applicability_warning="Preliminary estimate only; heavy-duty rotor mixer service must match dough, paste, or powder handling assumptions.",
1134 ),
1135 _template(
1136 value="scenz_2004_mixer_heavy_duty_extruder",
1137 name="SCENZ mixer - Heavy duty extruder",
1138 category="mixer",
1139 subtype="Heavy duty - extruder",
1140 variable_symbol="P",
1141 input_unit="kW",
1142 expression_text="295 * P + 59.5 * 10 ^ 3",
1143 valid_min="1",
1144 valid_max="10000",
1145 valid_range_note="Visual-inferred power range from Figure 8.22; reviewer confirmation required.",
1146 source_page="PDF p.50, doc p.46",
1147 source_figure="Figure 8.22",
1148 source_data_origin="US - Ulrich (2004)",
1149 notes="Purchased heavy-duty mixer cost including drives for doughs, pastes, and powders.",
1150 applicability_warning="Preliminary estimate only; heavy-duty extruder mixer service must match dough, paste, or powder handling assumptions.",
1151 ),
1152 _template(
1153 value="scenz_2004_mixer_heavy_duty_muller",
1154 name="SCENZ mixer - Heavy duty muller",
1155 category="mixer",
1156 subtype="Heavy duty - muller",
1157 variable_symbol="P",
1158 input_unit="kW",
1159 expression_text="7.60 * 10 ^ 3 * P ^ 0.630",
1160 valid_min="1",
1161 valid_max="10000",
1162 valid_range_note="Visual-inferred power range from Figure 8.22; reviewer confirmation required.",
1163 source_page="PDF p.50, doc p.46",
1164 source_figure="Figure 8.22",
1165 source_data_origin="US - Ulrich (2004)",
1166 notes="Purchased heavy-duty mixer cost including drives for doughs, pastes, and powders.",
1167 applicability_warning="Preliminary estimate only; heavy-duty muller mixer service must match dough, paste, or powder handling assumptions.",
1168 ),
1169 _template(
1170 value="scenz_2004_mixer_heavy_duty_roll",
1171 name="SCENZ mixer - Heavy duty roll",
1172 category="mixer",
1173 subtype="Heavy duty - roll",
1174 variable_symbol="P",
1175 input_unit="kW",
1176 expression_text="2.97 * 10 ^ 3 * P ^ 0.406",
1177 valid_min="1",
1178 valid_max="10000",
1179 valid_range_note="Visual-inferred power range from Figure 8.22; reviewer confirmation required.",
1180 source_page="PDF p.50, doc p.46",
1181 source_figure="Figure 8.22",
1182 source_data_origin="US - Ulrich (2004)",
1183 notes="Purchased heavy-duty mixer cost including drives for doughs, pastes, and powders.",
1184 applicability_warning="Preliminary estimate only; heavy-duty roll mixer service must match dough, paste, or powder handling assumptions.",
1185 ),
1186 _template(
1187 value="scenz_2004_mixer_heavy_duty_kneader",
1188 name="SCENZ mixer - Heavy duty kneader",
1189 category="mixer",
1190 subtype="Heavy duty - kneader",
1191 variable_symbol="P",
1192 input_unit="kW",
1193 expression_text="208 * P + 51.8 * 10 ^ 3",
1194 valid_min="1",
1195 valid_max="10000",
1196 valid_range_note="Visual-inferred power range from Figure 8.22; reviewer confirmation required.",
1197 source_page="PDF p.50, doc p.46",
1198 source_figure="Figure 8.22",
1199 source_data_origin="US - Ulrich (2004)",
1200 notes="Purchased heavy-duty mixer cost including drives for doughs, pastes, and powders.",
1201 applicability_warning="Preliminary estimate only; heavy-duty kneader service must match dough, paste, or powder handling assumptions.",
1202 ),
1203 _template(
1204 value="scenz_2004_mixer_heavy_duty_ribbon",
1205 name="SCENZ mixer - Heavy duty ribbon",
1206 category="mixer",
1207 subtype="Heavy duty - ribbon",
1208 variable_symbol="P",
1209 input_unit="kW",
1210 expression_text="3.22 * 10 ^ 3 * P ^ 0.504",
1211 valid_min="1",
1212 valid_max="10000",
1213 valid_range_note="Visual-inferred power range from Figure 8.22; reviewer confirmation required.",
1214 source_page="PDF p.50, doc p.46",
1215 source_figure="Figure 8.22",
1216 source_data_origin="US - Ulrich (2004)",
1217 notes="Purchased heavy-duty mixer cost including drives for doughs, pastes, and powders.",
1218 applicability_warning="Preliminary estimate only; heavy-duty ribbon mixer service must match dough, paste, or powder handling assumptions.",
1219 ),
1220 _template(
1221 value="scenz_2004_cooling_tower_water_installed",
1222 name="SCENZ cooling tower - Water installed",
1223 category="cooling_tower",
1224 subtype="Water cooling tower",
1225 cost_basis=CostBasis.INSTALLED,
1226 variable_symbol="q",
1227 input_unit="m^3/s",
1228 expression_text="842 * 10 ^ 3 * q ^ 0.679",
1229 valid_min="0.01",
1230 valid_max="10",
1231 valid_range_note="Visual-inferred cooling-water-capacity range from Figure 8.40; basis is 45 degC inlet, 30 degC outlet, 25 degC wet bulb.",
1232 source_page="PDF p.68, doc p.64",
1233 source_figure="Figure 8.40",
1234 source_data_origin="US - Ulrich (2004)",
1235 notes="Installed cooling tower cost including delivery, erection, foundation, basin, pumps, and drives.",
1236 applicability_warning="Installed-cost curve already includes several installation elements; do not apply a normal Lang factor without review.",
1237 ),
1238)
1241def cost_curve_equipment_categories(curves: Iterable[CostCurve] = ()) -> list[CostCurveEquipmentCategory]:
1242 """
1243 Return backend-owned cost-curve category/subtype options.
1245 The built-in vocabulary and copyable template configs are transcribed from
1246 the SCENZ v1 seed specification. Existing user-authored curves are merged in
1247 so project-local categories or subtypes remain selectable after creation.
1248 """
1249 merged = {
1250 CUSTOM_EQUIPMENT_CATEGORY: {
1251 "label": CUSTOM_EQUIPMENT_CATEGORY_LABEL,
1252 "subtypes": [],
1253 "templates": [],
1254 }
1255 }
1256 for value, label in SCENZ_CATEGORY_LABELS.items():
1257 merged[value] = {
1258 "label": label,
1259 "subtypes": [],
1260 "templates": [],
1261 }
1262 for template in SCENZ_COST_CURVE_TEMPLATES:
1263 entry = merged.setdefault(
1264 template.equipment_category,
1265 {
1266 "label": template.equipment_category.replace("_", " ").title(),
1267 "subtypes": [],
1268 "templates": [],
1269 },
1270 )
1271 if template.equipment_subtype not in entry["subtypes"]: 1271 ↛ 1273line 1271 didn't jump to line 1273 because the condition on line 1271 was always true
1272 entry["subtypes"].append(template.equipment_subtype)
1273 entry["templates"].append(template)
1275 for curve in curves:
1276 category = curve.equipment_category.strip()
1277 if not category: 1277 ↛ 1278line 1277 didn't jump to line 1278 because the condition on line 1277 was never true
1278 continue
1279 entry = merged.setdefault(
1280 category,
1281 {
1282 "label": category.replace("_", " ").title(),
1283 "subtypes": [],
1284 },
1285 )
1286 subtype = curve.equipment_subtype.strip()
1287 if subtype and subtype not in entry["subtypes"]: 1287 ↛ 1288line 1287 didn't jump to line 1288 because the condition on line 1287 was never true
1288 entry["subtypes"].append(subtype)
1290 return [
1291 CostCurveEquipmentCategory(
1292 value=value,
1293 label=str(entry["label"]),
1294 subtypes=tuple(str(subtype) for subtype in entry["subtypes"]),
1295 templates=tuple(entry["templates"]),
1296 )
1297 for value, entry in sorted(merged.items(), key=lambda item: item[1]["label"])
1298 ]
1301def cost_curve_category_requires_subtype(category: str, curves: Iterable[CostCurve] = ()) -> bool:
1302 """
1303 Return whether a category should reject a blank cost-curve subtype.
1305 Categories with known subtype-specific templates or user-authored subtype
1306 curves are ambiguous without a subtype unless a generic category-level curve
1307 already exists for that category.
1308 """
1309 category = category.strip()
1310 if not category: 1310 ↛ 1311line 1310 didn't jump to line 1311 because the condition on line 1310 was never true
1311 return False
1313 has_subtype_specific_curve = any(
1314 template.equipment_category == category and bool(template.equipment_subtype.strip())
1315 for template in SCENZ_COST_CURVE_TEMPLATES
1316 )
1317 has_generic_curve = False
1318 for curve in curves:
1319 if curve.equipment_category.strip() != category: 1319 ↛ 1320line 1319 didn't jump to line 1320 because the condition on line 1319 was never true
1320 continue
1321 if curve.equipment_subtype.strip(): 1321 ↛ 1322line 1321 didn't jump to line 1322 because the condition on line 1321 was never true
1322 has_subtype_specific_curve = True
1323 else:
1324 has_generic_curve = True
1326 return has_subtype_specific_curve and not has_generic_curve