Coverage for backend/django/Economics/costing/capital/electrical_upgrade.py: 94%
71 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-23 21:51 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-23 21:51 +0000
1"""Electrical-upgrade peak-demand helpers.
3The electrical upgrade is a capital allowance sized from peak electrical
4demand, not annual operating energy. Generated equipment capital lines carry a
5work-derived demand floor plus an editable selected peak demand; the aggregate
6project upgrade uses the selected peak demand across included capital lines.
7"""
9from __future__ import annotations
11from dataclasses import dataclass
12from decimal import Decimal, InvalidOperation
14from core.auxiliary.models.PropertyInfo import PropertyInfo
15from Economics.costing.models import CapitalCostLine
16from Economics.studies.models import EconomicsStudy
17from Economics.shared.unit_conversion import convert_quantity
18from Economics.costing.operating.stream_properties import UNIT_POWER_WORK_SOURCE_KIND, unit_energy_source_kind
21PEAK_DEMAND_UNIT = "kW"
22PEAK_DEMAND_QUANTUM = Decimal("0.00000001")
25@dataclass(frozen=True)
26class PeakDemandContribution:
27 capital_line_id: int
28 label: str
29 selected_peak_demand_kw: Decimal
30 minimum_peak_demand_kw: Decimal | None
33@dataclass(frozen=True)
34class PeakDemandBasis:
35 quantity_kw: Decimal | None
36 unit: str
37 contributions: tuple[PeakDemandContribution, ...]
40def peak_demand_for_unit_capital_line(line: CapitalCostLine) -> Decimal | None:
41 """Return the editable peak demand basis for one generated capital line."""
42 return line.peak_demand_kw.quantize(PEAK_DEMAND_QUANTUM) if line.peak_demand_kw is not None else None
45def derive_peak_demand_basis(study: EconomicsStudy) -> PeakDemandBasis:
46 """Sum included capital-line peak demand values for upgrade sizing."""
47 contributions: list[PeakDemandContribution] = []
48 lines = list(
49 study.capital_lines.filter(
50 included=True,
51 peak_demand_kw__isnull=False,
52 )
53 )
54 for line in lines:
55 demand = peak_demand_for_unit_capital_line(line)
56 if demand is None or demand <= 0:
57 continue
58 contributions.append(
59 PeakDemandContribution(
60 capital_line_id=line.pk,
61 label=line.label,
62 selected_peak_demand_kw=demand,
63 minimum_peak_demand_kw=line.minimum_peak_demand_kw,
64 )
65 )
66 total = sum((row.selected_peak_demand_kw for row in contributions), Decimal("0")).quantize(
67 PEAK_DEMAND_QUANTUM
68 )
69 return PeakDemandBasis(
70 quantity_kw=total if total > 0 or lines else None,
71 unit=PEAK_DEMAND_UNIT,
72 contributions=tuple(contributions),
73 )
76def unit_work_peak_demand_kw(costable_item) -> Decimal | None:
77 """Return positive work-property capacity for a costable unit in kW.
79 Multiple work properties on the unit are additive. This mirrors the result
80 resource classification for work properties but deliberately uses the raw
81 flowsheet capacity value rather than annualized operating hours. A
82 work-capable unit with no solved work value returns a zero floor so the UI
83 can still expose an editable peak-demand field for that unit.
84 """
85 simulation_object = getattr(costable_item, "simulation_object", None)
86 property_set = getattr(simulation_object, "properties", None)
87 if simulation_object is None or property_set is None:
88 return None
89 total = Decimal("0")
90 has_work_property = False
91 for property_info in property_set.containedProperties.all():
92 if not _is_peak_demand_work_property(property_info):
93 continue
94 has_work_property = True
95 peak_kw = _work_property_peak_kw(property_info)
96 if peak_kw is None:
97 continue
98 total += peak_kw
99 if total > 0:
100 return total.quantize(PEAK_DEMAND_QUANTUM)
101 return Decimal("0").quantize(PEAK_DEMAND_QUANTUM) if has_work_property else None
104def _is_peak_demand_work_property(property_info: PropertyInfo) -> bool:
105 unit = (property_info.unit or "").strip()
106 if not unit: 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true
107 return False
108 source_kind = unit_energy_source_kind(
109 unit=property_info.set.simulationObject,
110 property_info=property_info,
111 )
112 return source_kind == UNIT_POWER_WORK_SOURCE_KIND
115def _work_property_peak_kw(property_info: PropertyInfo) -> Decimal | None:
116 if not _is_peak_demand_work_property(property_info): 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true
117 return None
118 unit = (property_info.unit or "").strip()
119 try:
120 raw_value = Decimal(str(property_info.get_value()))
121 except (InvalidOperation, TypeError, ValueError):
122 return None
123 if raw_value == 0:
124 return None
125 converted = convert_quantity(
126 value=abs(raw_value),
127 source_unit=unit,
128 target_unit=PEAK_DEMAND_UNIT,
129 )
130 if converted is None or converted <= 0: 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true
131 return None
132 return converted.quantize(PEAK_DEMAND_QUANTUM)