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

1from __future__ import annotations 

2 

3from dataclasses import dataclass 

4from typing import Iterable 

5 

6from Economics.shared.choices import CostBasis, CostCurveEvaluationKind 

7 

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 

11 

12 

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" 

20 

21 

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 

51 

52 

53@dataclass(frozen=True) 

54class CostCurveEquipmentCategory: 

55 value: str 

56 label: str 

57 subtypes: tuple[str, ...] 

58 templates: tuple[CostCurveTemplate, ...] 

59 

60 

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) 

125 

126 

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" 

148 

149 

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 ) 

163 

164 

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 ) 

189 

190 

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 ) 

214 

215 

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 ) 

262 

263 

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 ) 

314 

315 

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 ) 

377 

378 

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 ) 

440 

441 

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 ) 

543 

544 

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) 

1239 

1240 

1241def cost_curve_equipment_categories(curves: Iterable[CostCurve] = ()) -> list[CostCurveEquipmentCategory]: 

1242 """ 

1243 Return backend-owned cost-curve category/subtype options. 

1244 

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) 

1274 

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) 

1289 

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 ] 

1299 

1300 

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. 

1304 

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 

1312 

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 

1325 

1326 return has_subtype_specific_curve and not has_generic_curve