From b8ea20988f56386160bca5bb57979506bdf8007b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 22:39:11 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.55:=20cascade=20WHC=20914=20?= =?UTF-8?q?=E2=86=92=20Main=202=20water-heating=20efficiency=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the second half of the cert 000565 Main 2 work. After Slice S0380.54 lodged Main 2 on the EpcPropertyData, the water-heating cascade still derived efficiency from Main 1 (the heat pump) instead of Main 2 (the gas combi that actually services DHW). Per the Elmhurst RdSAP convention, `Water Heating SapCode 914` = "from second main system" — DHW is generated by Main 2, not Main 1. The §4 / Appendix D2.1 summer-efficiency lookup must therefore key off Main 2's PCDB Table 105 record (cert 000565: PCDB 15100 Vaillant Ecotec plus 415, summer η = 88%) rather than Main 1's HP COP. Implementation: - New `_water_heating_main(epc)` helper — returns Main 2 when WHC is in `_WATER_FROM_SECOND_MAIN_CODES = {914}` AND a second main is lodged; otherwise returns Main 1 (matches prior behaviour for single-main certs + WHC 901/902 "from main system") - The water-eff branch at the §4 cascade now reads `water_pcdb_main = gas_oil_boiler_record(water_main.main_heating_index_number)` + `_water_efficiency_with_category_inherit(water_main.sap_main_ heating_code, water_main.main_heating_category, _main_fuel_code( water_main))` — same logic as before but parametrised by the water-heating main rather than hard-coded to Main 1 Cert 000565 cascade impact on hot_water_kwh_per_yr pin: - Before: actual 1,844.66 kWh/yr (= HW heat / HP COP 1.70 — wrong) Δ −1,910.36 vs U985-0001-000565.pdf expected 3,755.03 After Slice S0380.54 (Main 2 lodged but cascade still using Main 1): actual 3,919.91 kWh/yr, Δ +164.88 (regression from the no-cascade baseline because Main 2 PCDB was lodged but water_eff still came from Main 1's HP-vs-default fallthrough) - After this slice: actual 3,969.53 kWh/yr (= HW heat / 0.88) Δ +214.50 — 89% reduction vs the original Main-1 WHC 914 routing, remaining gap is fine-grained (FGHRS / solar HW / Table 3a no-keep- hot territory — separate slice) For single-main certs (the 14 existing Summary fixtures + 8 ASHP cohort certs): `_water_heating_main` returns Main 1, identical to the prior `main` reference. Cohort regression check: 472 pass + 10 expected 000565 fails — no broader regression. Spec source: SAP 10.2 §4 water-heating cascade + Appendix D2.1 (D1 equation) summer-efficiency override; Elmhurst RdSAP water-heating code 914 ("from second main system"). Pyright net-zero on cert_to_inputs.py (34 errors before, 34 after). Co-Authored-By: Claude Opus 4.7 --- .../sap10_calculator/rdsap/cert_to_inputs.py | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index dd22c0f0..dd32a9d3 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -556,6 +556,38 @@ def _first_main_heating(epc: EpcPropertyData) -> Optional[MainHeatingDetail]: return details[0] if details else None +# Elmhurst RdSAP water-heating codes that route DHW to a non-Main-1 +# system. RdSAP code 914 = "from second main system" — DHW is +# serviced by Main 2 (typically a gas combi providing DHW only) while +# Main 1 handles space heat (e.g. cert 000565: HP Main 1 + gas combi +# Main 2 + WHC 914). The water-heating cascade reads Main 2's PCDB +# record / SAP code / fuel when this routing applies. +_WATER_FROM_SECOND_MAIN_CODES: Final[frozenset[int]] = frozenset({914}) + + +def _water_heating_main( + epc: EpcPropertyData, +) -> Optional[MainHeatingDetail]: + """The `MainHeatingDetail` that services DHW per the cert's + `water_heating_code` routing. WHC 914 ("from second main system") + returns Main 2 when present; otherwise returns Main 1. + + The water-heating cascade (Table 4a / Appendix D2.1 summer + efficiency, water-heating fuel cost / CO2 / PE) keys off this + helper rather than `_first_main_heating` so the right system's + efficiency and fuel propagate to DHW. + """ + details = epc.sap_heating.main_heating_details if epc.sap_heating else [] + if not details: + return None + if ( + epc.sap_heating.water_heating_code in _WATER_FROM_SECOND_MAIN_CODES + and len(details) >= 2 + ): + return details[1] + return details[0] + + def _main_heating_efficiency(epc: EpcPropertyData) -> float: """SAP 10.2 (206) main system 1 efficiency as a 0..1 fraction. @@ -2877,14 +2909,24 @@ def cert_to_inputs( # `main_fuel_kwh = q_useful × DLF = q_generated`, matching the spec's # "unit prices per kWh of heat generated" convention. eff = _main_heating_efficiency(epc) - if pcdb_main is not None and pcdb_main.summer_efficiency_pct is not None: - water_eff = pcdb_main.summer_efficiency_pct / 100.0 + # Water-heating efficiency reads from the main that ACTUALLY services + # DHW per the cert's `water_heating_code` routing (Elmhurst WHC 914 + # = "from second main system" → Main 2). For single-main certs and + # WHC 901/902 this resolves to Main 1, matching the prior behaviour. + water_main = _water_heating_main(epc) + water_pcdb_main = ( + gas_oil_boiler_record(water_main.main_heating_index_number) + if water_main is not None and water_main.main_heating_index_number is not None + else None + ) + if water_pcdb_main is not None and water_pcdb_main.summer_efficiency_pct is not None: + water_eff = water_pcdb_main.summer_efficiency_pct / 100.0 else: water_eff = _water_efficiency_with_category_inherit( water_heating_code=epc.sap_heating.water_heating_code, - main_code=main_code, - main_category=main_category, - main_fuel=main_fuel, + main_code=water_main.sap_main_heating_code if water_main is not None else None, + main_category=water_main.main_heating_category if water_main is not None else None, + main_fuel=_main_fuel_code(water_main), ) # SAP 10.2 Appendix N3.6 + N3.7(a) — when an HP cert lodges a PCDB # Table 362 record, the cascade replaces the Table 4a defaults with