mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
§9a slice 1: space_heating_fuel_monthly_kwh orchestrator + EnergyRequirementsResult + 4 synthetic tests
Spec lines 7909-7953 (worksheet block §9a). Composes per-system fuel kWh from (98c)m, Table 11 secondary fraction (201), and per-system efficiencies (206)/(207)/(208). Formula: (211)m = (98c)m × (204) × 100 / (206) where (204) collapses to (202) = 1 − (201) in scope A's single-main case. EnergyRequirementsResult dataclass mirrors the full §9a worksheet shape with 16 fields including (203)/(205)/(207)/(209)/(213)/(221) zero-branch placeholders — worksheet-shape-fidelity precedent (§8c Q4/Q7/Q9, §8f Q3 grilling). First multi-main / fixed-AC / PCDB cert triggers the slices that populate them. Synthetic tests: (a) single-main no-secondary 80% efficiency Σ(211)=Σ(98c)/0.8, (b) Table 11 secondary fraction split (201)=0.1 produces (211)+(215) at correct ratios, (c) summer-clamp zeros from §8 (98c)m propagate through linearly to (211)m/(215)m, (d) scope-A two-main + cooling-fuel fields remain zero regardless of inputs. Calculator + cert_to_inputs wiring lands in slice 3. PDF-derived ALL_FIXTURES pins for slice 2 deferred until PCDB integration grounds LINE_206 (Manufacturer-declared boiler efficiency); flagged in the SPEC_COVERAGE PCDB gap-list entry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
05d9dc73f8
commit
2b5fc6a575
2 changed files with 219 additions and 0 deletions
105
packages/domain/src/domain/sap/worksheet/energy_requirements.py
Normal file
105
packages/domain/src/domain/sap/worksheet/energy_requirements.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"""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) and cooling-fuel (209)/(221) are zero-branch
|
||||
placeholders in scope A — populated once first cert exercises them.
|
||||
|
||||
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).
|
||||
|
||||
Scope-A populated lines: (201), (202), (204), (206), (208), (211)m,
|
||||
(211), (215)m, (215). Two-main and cooling-fuel line refs ((203),
|
||||
(205), (207), (209), (213)m, (213), (221)) are zero-branch
|
||||
placeholders until the first multi-main / 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,
|
||||
) -> EnergyRequirementsResult:
|
||||
"""SAP 10.2 §9a orchestrator — produce (201)..(221) line refs.
|
||||
|
||||
Scope A: single-main + secondary only. Two-main ((203) > 0) and
|
||||
cooling-fuel (Table 10c SEER) populate the zero-branch placeholder
|
||||
fields with computed values when their respective slices land.
|
||||
"""
|
||||
fraction_201 = secondary_heating_fraction
|
||||
fraction_202 = 1.0 - fraction_201
|
||||
fraction_203 = 0.0 # scope A: no main 2
|
||||
fraction_204 = fraction_202 * (1.0 - fraction_203)
|
||||
fraction_205 = fraction_202 * fraction_203
|
||||
|
||||
main_1_eff = main_heating_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
|
||||
)
|
||||
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=0.0,
|
||||
secondary_efficiency_pct=secondary_eff,
|
||||
cooling_seer=0.0,
|
||||
main_1_fuel_monthly_kwh=main_1_fuel_monthly,
|
||||
main_2_fuel_monthly_kwh=(0.0,) * 12,
|
||||
secondary_fuel_monthly_kwh=secondary_fuel_monthly,
|
||||
main_1_fuel_kwh_per_yr=sum(main_1_fuel_monthly),
|
||||
main_2_fuel_kwh_per_yr=0.0,
|
||||
secondary_fuel_kwh_per_yr=sum(secondary_fuel_monthly),
|
||||
cooling_fuel_kwh_per_yr=0.0,
|
||||
)
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
"""Tests for SAP 10.2 §9a Energy requirements (individual heating systems).
|
||||
|
||||
Reference: SAP 10.2 specification (14-03-2025) worksheet block §9a
|
||||
(lines 7909-7953).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from domain.sap.worksheet.energy_requirements import (
|
||||
space_heating_fuel_monthly_kwh,
|
||||
)
|
||||
|
||||
|
||||
def test_single_main_no_secondary_converts_q_heat_by_efficiency() -> None:
|
||||
"""Spec lines 7940-7941: (211)m = (98c)m × (204) × 100 / (206). With no
|
||||
secondary system (201)=0, (204) collapses to 1.0 so (211)m = (98c)m /
|
||||
(206)/100. With Σ(98c) = 4400 kWh at 80% efficiency, Σ(211) = 4400 ×
|
||||
100/80 = 5500 kWh."""
|
||||
# Arrange
|
||||
monthly_space_heating = (
|
||||
1000.0, 800.0, 600.0, 400.0, 200.0, 0.0,
|
||||
0.0, 0.0, 0.0, 200.0, 400.0, 800.0,
|
||||
)
|
||||
|
||||
# Act
|
||||
result = space_heating_fuel_monthly_kwh(
|
||||
space_heating_monthly_kwh=monthly_space_heating,
|
||||
secondary_heating_fraction=0.0,
|
||||
main_heating_efficiency_pct=80.0,
|
||||
secondary_heating_efficiency_pct=100.0,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.main_1_fuel_kwh_per_yr == 5500.0
|
||||
|
||||
|
||||
def test_table_11_secondary_fraction_splits_q_heat_between_main_and_secondary() -> None:
|
||||
"""Spec lines 7912-7914 + 7940-7951: (201) Table 11 secondary fraction
|
||||
routes (98c)m × (201) to the secondary system, (98c)m × (202) to the
|
||||
main. With (201)=0.1, both efficiencies 80%, Σ(98c)=4400 → Σ(211) =
|
||||
4400 × 0.9 × 100/80 = 4950 kWh; Σ(215) = 4400 × 0.1 × 100/80 = 550 kWh."""
|
||||
# Arrange
|
||||
monthly_space_heating = (
|
||||
1000.0, 800.0, 600.0, 400.0, 200.0, 0.0,
|
||||
0.0, 0.0, 0.0, 200.0, 400.0, 800.0,
|
||||
)
|
||||
|
||||
# Act
|
||||
result = space_heating_fuel_monthly_kwh(
|
||||
space_heating_monthly_kwh=monthly_space_heating,
|
||||
secondary_heating_fraction=0.1,
|
||||
main_heating_efficiency_pct=80.0,
|
||||
secondary_heating_efficiency_pct=80.0,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.secondary_heating_fraction == 0.1
|
||||
assert result.main_heating_total_fraction == 0.9
|
||||
assert result.main_1_fuel_kwh_per_yr == 4950.0
|
||||
assert result.secondary_fuel_kwh_per_yr == 550.0
|
||||
|
||||
|
||||
def test_per_month_fuel_preserves_summer_clamp_zeros_from_98c() -> None:
|
||||
"""The §8 Table 9c summer clamp zeros (98c)m for Jun..Sep. §9a's per-
|
||||
month (211)m / (215)m tuples are linear in (98c)m so they inherit the
|
||||
summer zeros — important downstream because §10a fuel-cost cascade
|
||||
indexes the per-month tuples directly."""
|
||||
# Arrange
|
||||
monthly_space_heating = (
|
||||
1000.0, 800.0, 600.0, 400.0, 200.0, 0.0,
|
||||
0.0, 0.0, 0.0, 200.0, 400.0, 800.0,
|
||||
)
|
||||
|
||||
# Act
|
||||
result = space_heating_fuel_monthly_kwh(
|
||||
space_heating_monthly_kwh=monthly_space_heating,
|
||||
secondary_heating_fraction=0.1,
|
||||
main_heating_efficiency_pct=80.0,
|
||||
secondary_heating_efficiency_pct=80.0,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.main_1_fuel_monthly_kwh[5] == 0.0 # Jun
|
||||
assert result.main_1_fuel_monthly_kwh[8] == 0.0 # Sep
|
||||
assert result.secondary_fuel_monthly_kwh[5] == 0.0
|
||||
assert result.secondary_fuel_monthly_kwh[8] == 0.0
|
||||
assert result.main_1_fuel_monthly_kwh[0] == 1125.0 # Jan: 1000 × 0.9 × 100/80
|
||||
assert result.secondary_fuel_monthly_kwh[0] == 125.0 # Jan: 1000 × 0.1 × 100/80
|
||||
|
||||
|
||||
def test_scope_a_two_main_and_cooling_fuel_remain_zero_placeholders() -> None:
|
||||
"""Scope A defers two-main system ((203)/(205)/(207)/(213)) and cooling
|
||||
SEER ((209)/(221)). EnergyRequirementsResult mirrors the worksheet's
|
||||
full §9a shape but those fields stay zero regardless of inputs — first
|
||||
multi-main / fixed-AC cert triggers the slice that populates them."""
|
||||
# Arrange
|
||||
monthly_space_heating = (1000.0,) * 12
|
||||
|
||||
# Act
|
||||
result = space_heating_fuel_monthly_kwh(
|
||||
space_heating_monthly_kwh=monthly_space_heating,
|
||||
secondary_heating_fraction=0.1,
|
||||
main_heating_efficiency_pct=80.0,
|
||||
secondary_heating_efficiency_pct=80.0,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.main_2_of_main_fraction == 0.0
|
||||
assert result.main_2_of_total_fraction == 0.0
|
||||
assert result.main_2_efficiency_pct == 0.0
|
||||
assert result.main_2_fuel_monthly_kwh == (0.0,) * 12
|
||||
assert result.main_2_fuel_kwh_per_yr == 0.0
|
||||
assert result.cooling_seer == 0.0
|
||||
assert result.cooling_fuel_kwh_per_yr == 0.0
|
||||
Loading…
Add table
Reference in a new issue