mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.176: Table 4b combi sub-row dispatch for (61)m
SAP 10.2 §4 line 7702 (PDF p.137) defines (61)m as "Combi loss for
each month from Table 3a, 3b or 3c (enter '0' if not a combi
boiler)". Table 4b sub-rows 128 / 129 / 130 are explicit combi sub-
rows per the spec row names:
128: Combi oil boiler, pre-1998
129: Combi oil boiler, 1998 or later
130: Condensing combi oil boiler
Pre-slice `_table_3a_combi_loss_default_applies` gated only on
`main_heating_category ∈ {1, 2, 3, 6}`. The Elmhurst mapper leaves
`main_heating_category=None` on Table 4b liquid-fuel boilers (FAME,
HVO, B30K) — the cascade fell through to (61)m=0 despite the lodged
SAP code being a combi sub-row, under-counting (62)m by 600 kWh/yr
for FAME combi certs.
Extended the helper with a `_TABLE_4B_COMBI_OR_CPSU_CODES` fall-
through (set already exists for the symmetric `_primary_loss_
applies` Table 4b non-combi branch — see S0380.146). The set carries
the canonical combi + CPSU sub-row codes (103/104/107/108/112/113/
118/120-123/128-130). For cylinder-lodged certs the existing
`if epc.has_hot_water_cylinder: combi_loss_override = zero_monthly`
guard in `_water_heating_worksheet_and_gains` still pre-empts the
combi-loss fall-through correctly — non-combi codes with cylinders
remain (61)m=0.
Closures (heating-systems corpus 001431):
oil 3 (code 128, FAME, no cylinder) ALL EXACT (±0.0000):
ΔSAP_c +2.5863 → -0.0000
Δcost -£61.89 → -£0.00
ΔCO2 -14.58 → +0.00
ΔPE -967.10 → +0.00
oil 4 (code 129, FAME, no cylinder) ALL EXACT (±0.0000):
ΔSAP_c +2.5603 → +0.0000
Δcost -£56.66 → +£0.00
ΔCO2 -13.35 → +0.00
ΔPE -884.90 → +0.00
Oil 6 (code 126, NOT a combi, with cylinder) unchanged — the fix
is gated on the combi sub-row set. Cohort moves from 9 pinned
residuals to 7.
933 pass + 0 fail (+1 new mapper test). Pyright net-zero on cert_
to_inputs.py + tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
eda07d12dc
commit
326066ee12
3 changed files with 100 additions and 3 deletions
|
|
@ -433,9 +433,20 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
|
||||||
# 32 code-73 price flip (5.44 → 7.64) per S0380.131's TODO. oil 6
|
# 32 code-73 price flip (5.44 → 7.64) per S0380.131's TODO. oil 6
|
||||||
# (B30K) carries a cascade-side residual (HW kWh / SH demand /
|
# (B30K) carries a cascade-side residual (HW kWh / SH demand /
|
||||||
# CO2/PE blend) — see open fronts in the post-S0380.168 handover.
|
# CO2/PE blend) — see open fronts in the post-S0380.168 handover.
|
||||||
|
#
|
||||||
|
# Slice S0380.176 closed oil 3 + oil 4 fully via Table 4b combi sub-
|
||||||
|
# row dispatch in `_table_3a_combi_loss_default_applies`. Pre-slice
|
||||||
|
# the helper gated only on `main_heating_category` ∈ {1, 2, 3, 6};
|
||||||
|
# the Elmhurst mapper leaves `main_heating_category=None` on Table
|
||||||
|
# 4b liquid-fuel boilers, so the cascade fell through to (61)m=0
|
||||||
|
# despite codes 128/129 being explicit combi sub-rows per SAP 10.2
|
||||||
|
# Table 4b row names ("Combi oil boiler, pre-/post-1998"). Adding
|
||||||
|
# the `_TABLE_4B_COMBI_OR_CPSU_CODES` fall-through lands (61)m at
|
||||||
|
# the spec Table 3a row 1 keep-hot 600 kWh/yr default. Both oil 3
|
||||||
|
# and oil 4 now EXACT on SAP / cost / CO2 / PE.
|
||||||
_CorpusExpectation(variant='oil 2', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=+0.0000),
|
_CorpusExpectation(variant='oil 2', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=+0.0000),
|
||||||
_CorpusExpectation(variant='oil 3', block='11a', expected_sap_resid=+2.5863, expected_cost_resid_gbp=-61.8906, expected_co2_resid_kg=-14.5815, expected_pe_resid_kwh=-967.0971),
|
_CorpusExpectation(variant='oil 3', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=+0.0000),
|
||||||
_CorpusExpectation(variant='oil 4', block='11a', expected_sap_resid=+2.5603, expected_cost_resid_gbp=-56.6586, expected_co2_resid_kg=-13.3489, expected_pe_resid_kwh=-884.8990),
|
_CorpusExpectation(variant='oil 4', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=+0.0000),
|
||||||
_CorpusExpectation(variant='oil 5', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000),
|
_CorpusExpectation(variant='oil 5', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000),
|
||||||
_CorpusExpectation(variant='oil 6', block='11a', expected_sap_resid=+3.0518, expected_cost_resid_gbp=-69.7943, expected_co2_resid_kg=-240.6595, expected_pe_resid_kwh=-1112.6558),
|
_CorpusExpectation(variant='oil 6', block='11a', expected_sap_resid=+3.0518, expected_cost_resid_gbp=-69.7943, expected_co2_resid_kg=-240.6595, expected_pe_resid_kwh=-1112.6558),
|
||||||
_CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000),
|
_CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000),
|
||||||
|
|
|
||||||
|
|
@ -4794,10 +4794,24 @@ def _table_3a_combi_loss_default_applies(main: Optional[MainHeatingDetail]) -> b
|
||||||
boiler family or a community heat network — outside that set the spec's
|
boiler family or a community heat network — outside that set the spec's
|
||||||
"enter '0' if not a combi boiler" rule fires and the cascade must zero
|
"enter '0' if not a combi boiler" rule fires and the cascade must zero
|
||||||
(61)m.
|
(61)m.
|
||||||
|
|
||||||
|
The `main_heating_category` route covers the Open EPC API path where
|
||||||
|
the cert lodges a SAP 10.2 boiler / heat-network category integer.
|
||||||
|
The `_TABLE_4B_COMBI_OR_CPSU_CODES` fall-through covers the Elmhurst-
|
||||||
|
path case where the mapper leaves `main_heating_category=None` but
|
||||||
|
the cert lodges a Table 4b combi sub-row directly (oil 3 / oil 4 in
|
||||||
|
heating-systems corpus 001431 — SAP codes 128 / 129 "Combi oil
|
||||||
|
boiler, pre-/post-1998", FAME fuel — Elmhurst's mapper artifact
|
||||||
|
leaves the category unset).
|
||||||
"""
|
"""
|
||||||
if main is None:
|
if main is None:
|
||||||
return False
|
return False
|
||||||
return main.main_heating_category in _TABLE_3A_COMBI_LOSS_MAIN_HEATING_CATEGORIES
|
if main.main_heating_category in _TABLE_3A_COMBI_LOSS_MAIN_HEATING_CATEGORIES:
|
||||||
|
return True
|
||||||
|
code = main.sap_main_heating_code
|
||||||
|
if isinstance(code, int) and code in _TABLE_4B_COMBI_OR_CPSU_CODES:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _water_heating_worksheet_and_gains(
|
def _water_heating_worksheet_and_gains(
|
||||||
|
|
|
||||||
|
|
@ -4460,6 +4460,78 @@ def test_air_source_heat_pump_main_heating_zeroes_table_3a_combi_loss_per_sap_4_
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_table_4b_combi_oil_boiler_applies_table_3a_combi_loss_per_sap_4_line_7702() -> None:
|
||||||
|
"""SAP 10.2 §4 line 7702: "Combi loss for each month from Table 3a,
|
||||||
|
3b or 3c (enter '0' if not a combi boiler)". Table 4b sub-rows 128
|
||||||
|
("Combi oil boiler, pre-1998") and 129 ("Combi oil boiler, 1998 or
|
||||||
|
later") are explicitly combi boilers per the Table 4b row names.
|
||||||
|
|
||||||
|
Pre-slice `_table_3a_combi_loss_default_applies` gated only on
|
||||||
|
`main_heating_category` ∈ {1, 2, 3, 6}. The Elmhurst mapper leaves
|
||||||
|
`main_heating_category=None` on Table 4b liquid-fuel boilers (FAME,
|
||||||
|
HVO, B30K — codes 128/129/130 combi sub-rows and 124/125/126/127
|
||||||
|
regular sub-rows). For combi-without-cylinder certs the (61)m
|
||||||
|
keep-hot default fell through to zero instead of the spec's 600
|
||||||
|
kWh/yr Table 3a row 1.
|
||||||
|
|
||||||
|
Worksheet evidence for heating-systems corpus 001431 oil 3 (Table
|
||||||
|
4b code 128 + WHC=901 + no cylinder + FAME fuel):
|
||||||
|
(61)m Jan ≈ 50.96 kWh, annual ≈ 600 kWh/yr
|
||||||
|
(62)m Jan = 251.39 = 0.85 × (45) + (46) + (61)
|
||||||
|
(219) HW fuel sum ≈ 3787 kWh/yr (= (62)m / (217)m via Eq D1)
|
||||||
|
|
||||||
|
Pre-slice cascade for oil 3: (61)m = 0, (62)m sum = 1935.37 (≡ (45)
|
||||||
|
sum, off by -600 vs ws 2535.37), (219) = 2876 (off by -911 vs ws
|
||||||
|
3787).
|
||||||
|
"""
|
||||||
|
# Arrange — synthesise oil 3 / oil 4 cert shape: Table 4b code 128
|
||||||
|
# combi oil boiler (FAME fuel = Table 32 code 73), no cylinder,
|
||||||
|
# WHC=901 ("HW from main heating"), main_heating_category=None
|
||||||
|
# (Elmhurst mapper artifact).
|
||||||
|
combi_main = MainHeatingDetail(
|
||||||
|
has_fghrs=False,
|
||||||
|
main_fuel_type=73, # FAME fuel
|
||||||
|
heat_emitter_type=1,
|
||||||
|
emitter_temperature=1,
|
||||||
|
main_heating_control=2106,
|
||||||
|
main_heating_category=None, # Elmhurst leaves None on Table 4b
|
||||||
|
sap_main_heating_code=128, # Combi oil boiler, pre-1998
|
||||||
|
)
|
||||||
|
epc = make_minimal_sap10_epc(
|
||||||
|
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||||
|
habitable_rooms_count=4,
|
||||||
|
country_code="ENG",
|
||||||
|
has_hot_water_cylinder=False, # combi → no cylinder
|
||||||
|
sap_building_parts=[make_building_part()],
|
||||||
|
sap_heating=make_sap_heating(
|
||||||
|
main_heating_details=[combi_main],
|
||||||
|
water_heating_code=901,
|
||||||
|
water_heating_fuel=73,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
wh_result, _ = _water_heating_worksheet_and_gains(
|
||||||
|
epc=epc,
|
||||||
|
water_efficiency_pct=0.62, # summer eff for Table 4b code 128
|
||||||
|
is_instantaneous=False,
|
||||||
|
primary_age="D",
|
||||||
|
pcdb_record=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert — (61)m must be Table 3a row 1 default (600 kWh/yr
|
||||||
|
# prorated by days-in-month), NOT zero. Sum must land at ~600.
|
||||||
|
assert wh_result is not None
|
||||||
|
annual_combi_loss = sum(wh_result.combi_loss_monthly_kwh)
|
||||||
|
assert abs(annual_combi_loss - 600.0) < 1e-3, (
|
||||||
|
f"(61)m sum: got {annual_combi_loss!r}, want 600.0 — Table 4b "
|
||||||
|
f"combi sub-row 128 must apply Table 3a row 1 keep-hot default "
|
||||||
|
f"per SAP 10.2 §4 line 7702. Pre-slice cascade gated on "
|
||||||
|
f"main_heating_category ∈ {{1, 2, 3, 6}} only; the Elmhurst-"
|
||||||
|
f"path Table 4b combi codes (128/129/130) need a fall-through."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_lighting_co2_factor_blends_table_12a_grid_2_with_table_12d_dual_rate_on_off_peak_certs() -> None:
|
def test_lighting_co2_factor_blends_table_12a_grid_2_with_table_12d_dual_rate_on_off_peak_certs() -> None:
|
||||||
"""SAP 10.2 Table 12a Grid 2 (PDF p.191) + Table 12d (PDF p.194) —
|
"""SAP 10.2 Table 12a Grid 2 (PDF p.191) + Table 12d (PDF p.194) —
|
||||||
"other electricity uses" (lighting, pumps + fans, electric shower) on
|
"other electricity uses" (lighting, pumps + fans, electric shower) on
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue