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

1"""Electrical-upgrade peak-demand helpers. 

2 

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""" 

8 

9from __future__ import annotations 

10 

11from dataclasses import dataclass 

12from decimal import Decimal, InvalidOperation 

13 

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 

19 

20 

21PEAK_DEMAND_UNIT = "kW" 

22PEAK_DEMAND_QUANTUM = Decimal("0.00000001") 

23 

24 

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 

31 

32 

33@dataclass(frozen=True) 

34class PeakDemandBasis: 

35 quantity_kw: Decimal | None 

36 unit: str 

37 contributions: tuple[PeakDemandContribution, ...] 

38 

39 

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 

43 

44 

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 ) 

74 

75 

76def unit_work_peak_demand_kw(costable_item) -> Decimal | None: 

77 """Return positive work-property capacity for a costable unit in kW. 

78 

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 

102 

103 

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 

113 

114 

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)