mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 102a: gate Table 3a combi-loss default by main heating category
SAP 10.2 §4 line 7702 (full spec PDF p.137): "Combi loss for each month
from Table 3a, 3b or 3c (enter '0' if not a combi boiler)". The cascade
in `_water_heating_worksheet_and_gains` was falling through to the
Table 3a keep-hot 600 kWh/yr default whenever no PCDB Table 105 boiler
record was found — including every heat-pump cert (Table 105 only
contains gas/oil boilers).
Open EPC API certs typically lodge `sap_main_heating_code = None`, so
the gate keys off `main_heating_category` instead: {1, 2} for the
gas/oil/solid-fuel boiler family + {3, 6} for community heat networks
(preserves the existing DLF-scaling regression test). Categories 4
(heat pump), 5 (warm air), 7 (electric storage), 10 (room heaters) and
all other non-combi mains zero (61)m per the spec parenthetical.
Cert 0380 (Mitsubishi ASHP, cat=4): HW kWh/yr drops 503.08 → 242.21,
removing the bogus 600 kWh × 0.18 £/kWh = £77/yr inflation. Closed
boiler certs (001479, 0330, 9501 — all cat=2) and heat-network cert
parity unchanged.
This commit is contained in:
parent
ac867499ea
commit
5e8ba9773f
2 changed files with 89 additions and 0 deletions
|
|
@ -1835,6 +1835,33 @@ def pcdb_combi_loss_override(
|
|||
return None
|
||||
|
||||
|
||||
# SAP 10.2 §4 line 7702 gates the Table 3a keep-hot combi loss default
|
||||
# to combi boilers ("enter '0' if not a combi boiler"). The Open EPC API
|
||||
# typically lodges `sap_main_heating_code = None` so we cannot key off the
|
||||
# precise SAP code; the next best signal is `main_heating_category`.
|
||||
# Categories 1 and 2 enumerate the gas / oil / solid-fuel boiler family
|
||||
# (which contains all combi boilers); categories 3 and 6 are community
|
||||
# heat networks (treated as boiler-like by the cascade and the existing
|
||||
# DLF-scaling regression test). Categories 4 (heat pump), 5 (warm air),
|
||||
# 7 (electric storage), 10 (room heaters) etc. are never combis and must
|
||||
# zero (61)m per the spec.
|
||||
_TABLE_3A_COMBI_LOSS_MAIN_HEATING_CATEGORIES: Final[frozenset[int]] = frozenset(
|
||||
{1, 2, 3, 6}
|
||||
)
|
||||
|
||||
|
||||
def _table_3a_combi_loss_default_applies(main: Optional[MainHeatingDetail]) -> bool:
|
||||
"""Gate for the Table 3a keep-hot 600 kWh/yr fall-through per SAP 10.2
|
||||
§4 line 7702. Returns True only when the main heating system is in the
|
||||
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
|
||||
(61)m.
|
||||
"""
|
||||
if main is None:
|
||||
return False
|
||||
return main.main_heating_category in _TABLE_3A_COMBI_LOSS_MAIN_HEATING_CATEGORIES
|
||||
|
||||
|
||||
def _water_heating_worksheet_and_gains(
|
||||
*,
|
||||
epc: EpcPropertyData,
|
||||
|
|
@ -1869,6 +1896,14 @@ def _water_heating_worksheet_and_gains(
|
|||
energy_content_monthly_kwh=bootstrap.energy_content_monthly_kwh,
|
||||
daily_hot_water_monthly_l_per_day=bootstrap.daily_hot_water_l_per_day_monthly,
|
||||
)
|
||||
# SAP 10.2 §4 line 7702: non-combi main heating → (61)m = 0. Without
|
||||
# this gate the cascade falls through to `combi_loss_monthly_kwh_table_
|
||||
# 3a_keep_hot_time_clock()` (600 kWh/yr) on every cert lacking a PCDB
|
||||
# Table 105 boiler record — including all heat pump certs.
|
||||
if combi_loss_override is None and not _table_3a_combi_loss_default_applies(
|
||||
_first_main_heating(epc)
|
||||
):
|
||||
combi_loss_override = zero_monthly
|
||||
wh_result = water_heating_from_cert(
|
||||
epc=epc,
|
||||
mixer_shower_flow_rates_l_per_min=_mixer_shower_flow_rates_from_cert(epc),
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from domain.sap10_ml.tests._fixtures import (
|
|||
)
|
||||
from domain.sap10_calculator.calculator import Sap10Calculator, SapResult
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
_water_heating_worksheet_and_gains,
|
||||
cert_to_demand_inputs,
|
||||
cert_to_inputs,
|
||||
pcdb_combi_loss_override,
|
||||
|
|
@ -1013,3 +1014,56 @@ def test_pcdb_combi_loss_override_returns_none_for_untested_or_storage_combis()
|
|||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_air_source_heat_pump_main_heating_zeroes_table_3a_combi_loss_per_sap_4_line_7702() -> None:
|
||||
"""SAP 10.2 §4 line 7702 worksheet defines (61)m as 'Combi loss for
|
||||
each month from Table 3a, 3b or 3c (enter "0" if not a combi
|
||||
boiler)'. Air Source Heat Pump main heating (main_heating_category=4)
|
||||
is NOT a combi boiler — the Table 3a keep-hot 600 kWh/yr fall-through
|
||||
in `water_heating_from_cert` must be gated by a main-heating-category
|
||||
check so non-combi certs receive (61)m = 0 per the spec parenthetical.
|
||||
|
||||
Without this gate the cascade silently inflates HW fuel by ~260 kWh/yr
|
||||
(~1.6 SAP points) on every HP cert that lacks a PCDB Table 105 record
|
||||
(i.e. every HP cert — Table 105 only contains gas/oil boilers).
|
||||
"""
|
||||
# Arrange — synthetic semi-detached, ASHP main heating
|
||||
# (main_heating_category=4, fuel=29 electricity), hot water from
|
||||
# cylinder via main heating (water_heating_code=901).
|
||||
hp_main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=29, # electricity
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2206,
|
||||
main_heating_category=4, # heat pump
|
||||
sap_main_heating_code=None, # Open EPC API typically lodges None
|
||||
)
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[make_building_part()],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[hp_main],
|
||||
water_heating_code=901, # from main heating
|
||||
),
|
||||
)
|
||||
|
||||
# Act — run the §4 worksheet cascade as the orchestrator does.
|
||||
wh_result, _ = _water_heating_worksheet_and_gains(
|
||||
epc=epc,
|
||||
water_efficiency_pct=1.7, # arbitrary; combi loss is upstream of η
|
||||
is_instantaneous=False,
|
||||
primary_age="D",
|
||||
pcdb_record=None, # no PCDB Table 105 boiler record for an HP
|
||||
)
|
||||
|
||||
# Assert — (61)m = (0,)*12 for a non-combi main.
|
||||
assert wh_result is not None
|
||||
for month_idx, value in enumerate(wh_result.combi_loss_monthly_kwh):
|
||||
assert value == 0.0, (
|
||||
f"month {month_idx}: combi loss {value!r} should be 0 for "
|
||||
f"non-combi main heating per SAP 10.2 §4 line 7702"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue