slice S-B12: water-heating eff inherits main_heating_category cascade

The legacy water_heating_efficiency(901, main_code) returns 0.80 (gas
boiler default) when sap_main_heating_code is None — even if the main
system is a heat pump (category=4, efficiency 2.30). For "from main
system" water codes (901/902/914), we must inherit through the FULL
main-heating cascade including the category fallback.

Discovered by hand-tracing cert 0320-2850 (Semi-detached bungalow,
heat-pump main with no SAP code lodged, actual SAP 70, predicted 49).
HW was being charged at 0.80 eff for a 2.30-eff dwelling — 2.9× too
much HW fuel.

100-cert parity probe:
  MAE 4.66 → 4.53   (-0.13)
  RMSE 6.27 → 5.96
  bias -0.70 → -0.57
  within ±10: 94% → 95%

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-18 15:48:42 +00:00
parent 737e5d6bf5
commit 1a6996abbb

View file

@ -50,7 +50,7 @@ from datatypes.epc.domain.epc_property_data import (
from domain.ml.demand import predicted_hot_water_kwh, predicted_lighting_kwh
from domain.ml.sap_efficiencies import (
seasonal_efficiency,
water_heating_efficiency,
water_heating_efficiency as _legacy_water_heating_efficiency,
)
from domain.sap.calculator import CalculatorInputs, WindowInput
from domain.sap.tables.table_12 import co2_factor_kg_per_kwh, unit_price_p_per_kwh
@ -411,6 +411,35 @@ def _other_fuel_cost_gbp_per_kwh(prices: PriceTable) -> float:
return prices.standard_electricity_p_per_kwh * _PENCE_TO_GBP
# Water-heating codes that say "inherit from the main system" — the
# `seasonal_efficiency` cascade returns 0 as a sentinel for these in the
# legacy `domain.ml.sap_efficiencies` module. We need to inherit through
# the SAME cascade the main heating uses, including the main_heating_
# category fallback (e.g. heat pumps return 2.30 via category 4).
_WATER_INHERIT_FROM_MAIN_CODES: Final[frozenset[int]] = frozenset({901, 902, 914})
def _water_efficiency_with_category_inherit(
*,
water_heating_code: Optional[int],
main_code: Optional[int],
main_category: Optional[int],
main_fuel: Optional[int],
) -> float:
"""When the cert says "hot water comes from the main system" (codes
901 / 902 / 914), inherit the main system's efficiency — and crucially
inherit the cascade that maps `main_heating_category` to a default
when `sap_main_heating_code` is None. The legacy water_heating_efficiency
only passes main_code through and so collapses heat pumps (cat 4) +
no-code lodgements into the 0.80 gas-boiler default.
"""
if water_heating_code is None:
return _legacy_water_heating_efficiency(None, main_code)
if water_heating_code in _WATER_INHERIT_FROM_MAIN_CODES:
return seasonal_efficiency(main_code, main_category, main_fuel)
return _legacy_water_heating_efficiency(water_heating_code, main_code)
def _co2_factor_kg_per_kwh(main: Optional[MainHeatingDetail]) -> float:
@ -499,7 +528,12 @@ def cert_to_inputs(
)
eff = seasonal_efficiency(main_code, main_category, main_fuel)
water_eff = water_heating_efficiency(epc.sap_heating.water_heating_code, main_code)
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,
)
hw_kwh = predicted_hot_water_kwh(
total_floor_area_m2=epc.total_floor_area_m2,
seasonal_efficiency_water=water_eff,