Model/domain/sap10_calculator/worksheet/energy_requirements.py
Khalim Conn-Kowlessar 8ae978a646 S0380.200: SAP 10.2 §9a two-main-heating split (203)/(205)/(207)/(213)
The cascade lumped a dwelling with two main heating systems into one:
`space_heating_fuel_monthly_kwh` hard-coded (203)=0 (a documented
scope-A placeholder) and the calculator's per-month fuel read only
main_1, so the full §8 space-heat demand billed against system 1's
efficiency. Simulated case 6 (one oil boiler feeding radiators 51% +
underfloor 49%) exposed it: main fuel ≈ demand/eff1 instead of the
worksheet's (211)+(213) per-system split.

Implements the SAP 10.2 §9a two-main model:
  (204) = (202) × (1 − (203))   → system 1 share of total heat
  (205) = (202) × (203)         → system 2 share of total heat
  (211)m = (98c)m × (204) × 100 / (206)
  (213)m = (98c)m × (205) × 100 / (207)
(203) = the second system's lodged `main_heating_fraction`; (207) = its
own seasonal efficiency via the new per-detail `_main_heating_detail_
efficiency` (the core of `_main_heating_efficiency`, now reused for
system 2). Calculator `_solve_month` aggregates main_1 + main_2 into
`main_heating_fuel_kwh`. Cost (§10a 241), CO2 (§12 262) and PE (§13 276)
main_2 paths were already wired and now activate.

Site-notes gap also fixed: §14.1 Main Heating2 omits the "Fuel Type"
cell when the second system shares Main 1's fuel (case 6: one oil boiler,
two emitters). `_map_elmhurst_main_heating_2` now inherits Main 1's
resolved fuel as a fallback.

Blast radius: only dual-main certs. 0240 (2× oil code 130, identical
Eq-D1 efficiency) is unchanged — its split collapses to the lumped total.
Suite: 2355 passed, 1 skipped. New code: 0 pyright errors.

NOTE: case 6 is not yet fully pinnable end-to-end — its two systems have
DIFFERENT efficiencies (radiators 55°C → 79%, underfloor 35°C → 84%), a
flow-temperature boiler-efficiency adjustment not yet modelled, and its
dual-system auxiliary pumps ((230c)+(230d)=356) differ from the cascade.
Both are separate follow-on features; this slice is the §9a fuel split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:09:43 +00:00

117 lines
5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""SAP 10.2 §9a Energy requirements — individual heating systems.
Spec lines 7909-7953 (worksheet block §9a). Composes the per-system fuel
kWh from the §8 space-heating tuple (98c)m, the Table 11 secondary
fraction (201), and the per-system efficiencies (206)/(207)/(208). The
formula for the main system 1 line ref (211)m is:
(211)m = (98c)m × (204) × 100 / (206)
where (204) = (202) × (1 (203)) and (202) = 1 (201). Single-main
case ((203) = 0) collapses (204) to (202), so (211)m = (98c)m × (202) ×
100 / (206). Same shape for secondary (215)m and main 2 (213)m.
Two-main split ((203) > 0) is implemented: (211)m = (98c)m × (204) ×
100 / (206) for system 1 and (213)m = (98c)m × (205) × 100 / (207) for
system 2, where (204) = (202) × (1 (203)) and (205) = (202) × (203).
Cooling-fuel (209)/(221) remains a zero-branch placeholder.
Reference: SAP 10.2 specification (14-03-2025) §9a (lines 7909-7953).
"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class EnergyRequirementsResult:
"""SAP 10.2 §9a worksheet line refs (201)..(221).
Populated lines: (201)-(208), (211)m/(211), (213)m/(213) (two-main
split), (215)m/(215). Cooling-fuel line refs ((209), (221)) are
zero-branch placeholders until the first fixed-AC cert lands.
"""
# Fractions (Table 11)
secondary_heating_fraction: float # (201)
main_heating_total_fraction: float # (202) = 1 - (201)
main_2_of_main_fraction: float # (203)
main_1_of_total_fraction: float # (204) = (202) × (1 - (203))
main_2_of_total_fraction: float # (205) = (202) × (203)
# Efficiencies (%)
main_1_efficiency_pct: float # (206)
main_2_efficiency_pct: float # (207)
secondary_efficiency_pct: float # (208)
cooling_seer: float # (209)
# Per-month fuel (kWh)
main_1_fuel_monthly_kwh: tuple[float, ...] # (211)m
main_2_fuel_monthly_kwh: tuple[float, ...] # (213)m
secondary_fuel_monthly_kwh: tuple[float, ...] # (215)m
# Annual totals (kWh)
main_1_fuel_kwh_per_yr: float # (211)
main_2_fuel_kwh_per_yr: float # (213)
secondary_fuel_kwh_per_yr: float # (215)
cooling_fuel_kwh_per_yr: float # (221)
def space_heating_fuel_monthly_kwh(
*,
space_heating_monthly_kwh: tuple[float, ...],
secondary_heating_fraction: float,
main_heating_efficiency_pct: float,
secondary_heating_efficiency_pct: float,
main_2_of_main_fraction: float = 0.0,
main_2_efficiency_pct: float = 0.0,
) -> EnergyRequirementsResult:
"""SAP 10.2 §9a orchestrator — produce (201)..(221) line refs.
Single-main certs leave `main_2_of_main_fraction` = 0, collapsing
(204) to (202) and zeroing (213)m. Dual-main certs (cert 0240 /
simulated case 6) pass (203) = fraction of main heating from main
system 2 and (207) = main system 2 efficiency; the §8 space-heat
demand then splits (204)=(202)×(1(203)) to system 1 and
(205)=(202)×(203) to system 2, each at its own efficiency. Cooling-
fuel (Table 10c SEER) remains a zero-branch placeholder.
"""
fraction_201 = secondary_heating_fraction
fraction_202 = 1.0 - fraction_201
fraction_203 = main_2_of_main_fraction
fraction_204 = fraction_202 * (1.0 - fraction_203)
fraction_205 = fraction_202 * fraction_203
main_1_eff = main_heating_efficiency_pct
main_2_eff = main_2_efficiency_pct
secondary_eff = secondary_heating_efficiency_pct
main_1_fuel_monthly = tuple(
q * fraction_204 * 100.0 / main_1_eff if main_1_eff > 0 else 0.0
for q in space_heating_monthly_kwh
)
main_2_fuel_monthly = tuple(
q * fraction_205 * 100.0 / main_2_eff if main_2_eff > 0 else 0.0
for q in space_heating_monthly_kwh
)
secondary_fuel_monthly = tuple(
q * fraction_201 * 100.0 / secondary_eff if secondary_eff > 0 else 0.0
for q in space_heating_monthly_kwh
)
return EnergyRequirementsResult(
secondary_heating_fraction=fraction_201,
main_heating_total_fraction=fraction_202,
main_2_of_main_fraction=fraction_203,
main_1_of_total_fraction=fraction_204,
main_2_of_total_fraction=fraction_205,
main_1_efficiency_pct=main_1_eff,
main_2_efficiency_pct=main_2_eff,
secondary_efficiency_pct=secondary_eff,
cooling_seer=0.0,
main_1_fuel_monthly_kwh=main_1_fuel_monthly,
main_2_fuel_monthly_kwh=main_2_fuel_monthly,
secondary_fuel_monthly_kwh=secondary_fuel_monthly,
main_1_fuel_kwh_per_yr=sum(main_1_fuel_monthly),
main_2_fuel_kwh_per_yr=sum(main_2_fuel_monthly),
secondary_fuel_kwh_per_yr=sum(secondary_fuel_monthly),
cooling_fuel_kwh_per_yr=0.0,
)