mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(heat-network): derive dwelling age band from first non-empty building part
The GOV.UK API lodges a junk empty leading building part (all fields
None) ahead of the real Main Dwelling on some certs. Four sites in
cert_to_inputs.py read `sap_building_parts[0].construction_age_band` →
got None → silently dropped the dwelling age band. New `_dwelling_age_band`
helper takes the first part that lodges a band (a no-op for normal certs
where [0] is the Main part).
Closes two age-band-keyed defects on the 5 affected certs:
- SAP 10.2 Table 12c (p.193): the heat-network Distribution Loss Factor
defaulted to the K-or-newer 1.50 instead of the dwelling's true band
(cert 8536-0929-6500-0815-7206 is age A → 1.20), inflating distribution
loss by 30%.
- RdSAP 10 §4.1 Table 5 (p.28): the empty band ("") fell through the
age-band branches to the H–M habitable-rooms branch, defaulting in
phantom extract fans. The true band A correctly yields 0 fans
(bands A–E → 0).
Cert 8536: 31.76 → 41.12 vs lodged 39 (was −7.24, now +2.12). API eval
mean|err| 1.197 → 1.192, signed −0.229 → −0.218; headline within-0.5
holds at 56.8% (8536 lands at +2.1, a documented overshoot vs the
faithful case-31 worksheet — separate slice).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e89b4041c7
commit
ba56647401
2 changed files with 59 additions and 15 deletions
|
|
@ -1021,6 +1021,27 @@ def _heat_network_dlf(age_band: Optional[str]) -> float:
|
|||
raise UnmappedSapCode("heat_network_age_band", age_band)
|
||||
|
||||
|
||||
def _dwelling_age_band(epc: EpcPropertyData) -> Optional[str]:
|
||||
"""The dwelling's construction age band, read from the first building
|
||||
part that lodges one.
|
||||
|
||||
The GOV.UK API can lodge a junk empty leading building part (all
|
||||
fields absent) ahead of the real Main Dwelling — reading
|
||||
`sap_building_parts[0]` then yields None and silently drops the age
|
||||
band (e.g. defaulting the heat-network DLF to the K-or-newer 1.50
|
||||
instead of the dwelling's true band). A no-op for normal certs, where
|
||||
`[0]` is the Main part and already carries a valid band.
|
||||
"""
|
||||
return next(
|
||||
(
|
||||
bp.construction_age_band
|
||||
for bp in epc.sap_building_parts
|
||||
if bp.construction_age_band
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
# SAP 10.2 Table 12 fuel code 50 — "electricity for pumping in
|
||||
# distribution network". Its CO2 / PE factors vary by month per Table
|
||||
# 12d / 12e (= standard-electricity profile); worksheet (372)/(472)
|
||||
|
|
@ -1751,10 +1772,7 @@ def _main_heating_detail_efficiency(
|
|||
else:
|
||||
eff = seasonal_efficiency(main_code, main_category, main_fuel)
|
||||
if _is_heat_network_main(main):
|
||||
primary_age = (
|
||||
epc.sap_building_parts[0].construction_age_band
|
||||
if epc.sap_building_parts else None
|
||||
)
|
||||
primary_age = _dwelling_age_band(epc)
|
||||
eff = 1.0 / _heat_network_dlf(primary_age)
|
||||
return eff
|
||||
|
||||
|
|
@ -4726,10 +4744,7 @@ def ventilation_from_cert(
|
|||
# lodged count is below the age-band minimum. The Elmhurst Summary
|
||||
# renders "0" as the form for unknown; the worksheet applies the
|
||||
# default via `max(lodged, table_5_default)`.
|
||||
age_band = (
|
||||
epc.sap_building_parts[0].construction_age_band
|
||||
if epc.sap_building_parts else ""
|
||||
)
|
||||
age_band = _dwelling_age_band(epc) or ""
|
||||
is_park_home = (epc.property_type or "").strip().lower() == "park home"
|
||||
table_5_fan_default = _rdsap_extract_fans_default(
|
||||
age_band, epc.habitable_rooms_count, is_park_home=is_park_home,
|
||||
|
|
@ -5116,10 +5131,7 @@ def _apply_rdsap_no_water_heating_system_default(
|
|||
"""
|
||||
if epc.sap_heating.water_heating_code != _WHC_NO_WATER_HEATING_SYSTEM:
|
||||
return epc
|
||||
age_band = (
|
||||
epc.sap_building_parts[0].construction_age_band
|
||||
if epc.sap_building_parts else None
|
||||
)
|
||||
age_band = _dwelling_age_band(epc)
|
||||
band = (age_band or "")[:1].upper()
|
||||
default = _TABLE_29_DEFAULT_CYLINDER_INSULATION_BY_AGE.get(band)
|
||||
if default is None:
|
||||
|
|
@ -6642,9 +6654,7 @@ def cert_to_inputs(
|
|||
# 10-hour) instead of `ALL_OTHER_USES` (0.80) — see
|
||||
# `_pumps_fans_fuel_cost_gbp_per_kwh`. Zero when no MEV is lodged.
|
||||
mev_kwh_for_cost_split = _mev_decentralised_kwh_per_yr_from_cert(epc)
|
||||
primary_age = (
|
||||
epc.sap_building_parts[0].construction_age_band if epc.sap_building_parts else None
|
||||
)
|
||||
primary_age = _dwelling_age_band(epc)
|
||||
|
||||
# SAP 10.2 Appendix D2.1: if the cert lodges a PCDB index number that
|
||||
# resolves to a Table 105 (gas/oil boilers) record, the PCDB winter
|
||||
|
|
|
|||
|
|
@ -234,6 +234,40 @@ def test_heat_network_main_applies_table12c_dlf_to_main_heating_efficiency() ->
|
|||
assert inputs.main_heating_efficiency == pytest.approx(1.0 / 1.41, abs=0.005)
|
||||
|
||||
|
||||
def test_heat_network_dlf_uses_first_non_empty_building_part_age_band() -> None:
|
||||
# Arrange — the GOV.UK API lodges a junk empty leading building part
|
||||
# (all fields absent) before the real Main Dwelling. The dwelling age
|
||||
# band (here A, 1.20 DLF per SAP 10.2 Table 12c) must be read from the
|
||||
# first NON-empty part, not `sap_building_parts[0]` — otherwise the
|
||||
# heat-network DLF defaults to the K-or-newer 1.50, inflating the
|
||||
# distribution loss by 30%. Reproduces cert 8536-0929-6500-0815-7206.
|
||||
main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=20, # mains gas (community)
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2106,
|
||||
main_heating_category=6,
|
||||
sap_main_heating_code=301,
|
||||
)
|
||||
empty_leading_part = make_building_part(construction_age_band="")
|
||||
main_dwelling_part = make_building_part(construction_age_band="A")
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[empty_leading_part, main_dwelling_part],
|
||||
sap_heating=make_sap_heating(main_heating_details=[main]),
|
||||
)
|
||||
|
||||
# Act
|
||||
inputs = cert_to_inputs(epc)
|
||||
|
||||
# Assert — age band A → Table 12c DLF = 1.20 → efficiency = 1/1.20,
|
||||
# NOT the empty-band default DLF 1.50 (1/1.50 = 0.667).
|
||||
assert abs(inputs.main_heating_efficiency - 1.0 / 1.20) <= 1e-9
|
||||
|
||||
|
||||
def test_heat_network_distribution_electricity_per_sap_10_2_appendix_c_3_2() -> None:
|
||||
# Arrange — heat-network main (Table 4a code 301 = community heating,
|
||||
# category 6). SAP 10.2 Appendix C §C3.2 (PDF p.51): distribution
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue