Coverage for backend/django/Economics/costing/operating/resource_basis.py: 85%
94 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1"""Shared operating-resource classification helpers."""
3from __future__ import annotations
5from dataclasses import dataclass
6from decimal import Decimal, InvalidOperation
7from typing import Any
9from core.auxiliary.models.PropertyInfo import PropertyInfo
10from flowsheetInternals.unitops.models.SimulationObject import SimulationObject
11from Economics.costing.models import OperatingCostLine
12from Economics.shared.choices import OperatingLineCategory, OutletStreamDisposition
13from Economics.shared.unit_conversion import convert_quantity
14from Economics.costing.operating.stream_properties import (
15 UNIT_COOLING_DUTY_SOURCE_KIND,
16 UNIT_HEATING_DUTY_SOURCE_KIND,
17 UNIT_POWER_WORK_SOURCE_KIND,
18 unit_energy_source_kind,
19)
22RESOURCE_CATEGORY_ANNUAL_COOLING_DUTY = "annual_cooling_duty"
23RESOURCE_CATEGORY_ANNUAL_HEATING_DUTY = "annual_heating_duty"
24RESOURCE_CATEGORY_ANNUAL_WORK = "annual_work"
25RESOURCE_CATEGORY_PRODUCT_OUTPUT = "product_output"
26RESOURCE_CATEGORY_RAW_MATERIAL_CONSUMPTION = "raw_material_consumption"
27RESOURCE_CATEGORY_WASTE_DISPOSAL = "waste_disposal"
30@dataclass(frozen=True)
31class AnnualResourceQuantity:
32 """Annual quantity snapshot used by high-level resource metrics."""
34 quantity: Decimal
35 unit: str
38def operating_resource_source_kind(
39 *,
40 line: OperatingCostLine,
41 source_object: SimulationObject | None = None,
42 property_info: PropertyInfo | None = None,
43) -> str:
44 """Classify the process source behind an operating line."""
45 if property_info is None and line.source_property_info_id: 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true
46 property_info = line.source_property_info
47 if source_object is None and property_info is not None: 47 ↛ 48line 47 didn't jump to line 48 because the condition on line 47 was never true
48 source_object = property_info.set.simulationObject
49 if line.category == OperatingLineCategory.FEEDSTOCK and property_info is not None:
50 return "starting_input_stream"
51 if (
52 line.category in (OperatingLineCategory.OUTPUT_REVENUE, OperatingLineCategory.DISPOSAL)
53 and property_info is not None
54 ):
55 return "terminal_output_stream"
56 if line.category != OperatingLineCategory.ENERGY or source_object is None or property_info is None:
57 return ""
58 return unit_energy_source_kind(unit=source_object, property_info=property_info) or ""
61def operating_resource_breakdown_category(
62 *,
63 line: OperatingCostLine,
64 source_kind: str | None = None,
65) -> str:
66 """Return the stable resource breakdown bucket for an operating line."""
67 if line.category == OperatingLineCategory.FEEDSTOCK:
68 return RESOURCE_CATEGORY_RAW_MATERIAL_CONSUMPTION
69 if (
70 line.category == OperatingLineCategory.OUTPUT_REVENUE
71 or line.outlet_stream_disposition == OutletStreamDisposition.SOLD
72 ):
73 return RESOURCE_CATEGORY_PRODUCT_OUTPUT
74 if (
75 line.category == OperatingLineCategory.DISPOSAL
76 or line.outlet_stream_disposition == OutletStreamDisposition.DISPOSED
77 ):
78 return RESOURCE_CATEGORY_WASTE_DISPOSAL
79 if line.category != OperatingLineCategory.ENERGY:
80 return ""
81 source_kind = source_kind if source_kind is not None else operating_resource_source_kind(line=line)
82 if source_kind == UNIT_COOLING_DUTY_SOURCE_KIND:
83 return RESOURCE_CATEGORY_ANNUAL_COOLING_DUTY
84 if source_kind == UNIT_HEATING_DUTY_SOURCE_KIND:
85 return RESOURCE_CATEGORY_ANNUAL_HEATING_DUTY
86 if source_kind == UNIT_POWER_WORK_SOURCE_KIND:
87 return RESOURCE_CATEGORY_ANNUAL_WORK
88 return ""
91def steam_fuel_energy_from_default_rate(
92 *,
93 annual_basis_quantity: Decimal | None,
94 annual_basis_unit: str,
95 source_default_rate: Any,
96 target_unit: str,
97) -> Decimal | None:
98 """Derive annual steam fuel energy from persisted basis and default-rate assumptions."""
99 useful_steam_energy = _steam_output_energy_gj_per_year(
100 annual_basis_quantity=annual_basis_quantity,
101 annual_basis_unit=annual_basis_unit,
102 source_default_rate=source_default_rate,
103 )
104 efficiency_percent = _steam_boiler_efficiency_percent(source_default_rate)
105 if useful_steam_energy is None or efficiency_percent is None or efficiency_percent <= 0: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true
106 return None
107 fuel_gj_per_year = useful_steam_energy / (efficiency_percent / Decimal("100"))
108 if target_unit == "GJ/year": 108 ↛ 110line 108 didn't jump to line 110 because the condition on line 108 was always true
109 return fuel_gj_per_year
110 return convert_quantity(
111 value=fuel_gj_per_year,
112 source_unit="GJ/year",
113 target_unit=target_unit,
114 )
117def annual_resource_quantity(
118 *,
119 annual_basis_quantity: Decimal | None,
120 annual_basis_unit: str,
121) -> AnnualResourceQuantity | None:
122 """Return a persisted annual quantity without changing its magnitude."""
123 if annual_basis_quantity is None or not annual_basis_unit: 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true
124 return None
125 return AnnualResourceQuantity(
126 quantity=annual_basis_quantity,
127 unit=_annual_unit(annual_basis_unit),
128 )
131def converted_annual_resource_quantity(
132 *,
133 annual_basis_quantity: Decimal | None,
134 annual_basis_unit: str,
135 target_unit: str,
136) -> Decimal | None:
137 """Convert a persisted annual resource quantity into the requested unit."""
138 if annual_basis_quantity is None or not annual_basis_unit: 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true
139 return None
140 return convert_quantity(
141 value=annual_basis_quantity,
142 source_unit=_annual_unit(annual_basis_unit),
143 target_unit=target_unit,
144 )
147def _steam_output_energy_gj_per_year(
148 *,
149 annual_basis_quantity: Decimal | None,
150 annual_basis_unit: str,
151 source_default_rate: Any,
152) -> Decimal | None:
153 annual_steam_tonnes = converted_annual_resource_quantity(
154 annual_basis_quantity=annual_basis_quantity,
155 annual_basis_unit=annual_basis_unit,
156 target_unit="t/year",
157 )
158 if annual_steam_tonnes is not None: 158 ↛ 161line 158 didn't jump to line 161 because the condition on line 158 was always true
159 steam_energy = _steam_energy_gj_per_t(source_default_rate)
160 return annual_steam_tonnes * steam_energy if steam_energy is not None else None
161 return converted_annual_resource_quantity(
162 annual_basis_quantity=annual_basis_quantity,
163 annual_basis_unit=annual_basis_unit,
164 target_unit="GJ/year",
165 )
168def _steam_energy_gj_per_t(source_default_rate: Any) -> Decimal | None:
169 metadata = _steam_default_metadata(source_default_rate)
170 return _decimal_or_none(metadata.get("steam_energy_gj_per_t"))
173def _steam_boiler_efficiency_percent(source_default_rate: Any) -> Decimal | None:
174 metadata = _steam_default_metadata(source_default_rate)
175 return _decimal_or_none(metadata.get("default_boiler_efficiency_percent"))
178def _steam_default_metadata(source_default_rate: Any) -> dict:
179 return (
180 source_default_rate.metadata
181 if source_default_rate is not None and isinstance(source_default_rate.metadata, dict)
182 else {}
183 )
186def _annual_unit(unit: str) -> str:
187 current = unit.strip()
188 if not current: 188 ↛ 189line 188 didn't jump to line 189 because the condition on line 188 was never true
189 return ""
190 if "/" in current and current.rsplit("/", 1)[1].strip() in {"y", "yr", "year", "years"}:
191 return current
192 return f"{current}/year"
195def _decimal_or_none(value: object) -> Decimal | None:
196 if value in (None, ""): 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true
197 return None
198 try:
199 return Decimal(str(value))
200 except (ArithmeticError, InvalidOperation, ValueError):
201 return None