Slice S0380.55: cascade WHC 914 → Main 2 water-heating efficiency routing

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-28 22:39:11 +00:00 committed by Jun-te Kim
parent 6d82f8842d
commit b8ea20988f

View file

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