Map PasHub boiler PCDB product_id to main_heating_index_number

`_map_sap_heating` left `main_heating_index_number` None, so the
calculator fell to a generic Table-4b seasonal efficiency instead of the
actual appliance. The site note lodges the boiler by its PCDB `product_id`
(and `summer_efficiency` is always 0), and those ids resolve directly in
the calculator's PCDB (`gas_oil_boiler_record`) — so carrying `product_id`
onto `main_heating_index_number` gives the real SEDBUK efficiency.

Gated on `product_id > 0` (0 = no product lodged → stays None; the
calculator keeps its default). `main_heating_category`/`sap_main_heating_code`
were evaluated and add nothing measurable — the PCDB index alone is the
efficiency source.

Guinness GMCA cohort: within-0.5 12.9% -> 13.4%, MAE 2.657 -> 2.621, no
regression (harness still 206-xfail, no new hard failures).

Closes #1563

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-14 17:43:37 +00:00
parent 597ca51dd9
commit 6bb1843be6
2 changed files with 35 additions and 0 deletions

View file

@ -6098,6 +6098,11 @@ def _map_sap_heating(
condensing=main.condensing,
weather_compensator=main.weather_compensator,
central_heating_pump_age_str=main.central_heating_pump_age,
# PCDB product id → the appliance's SEDBUK efficiency source
# (`gas_oil_boiler_record`); 0 means no product lodged (#1563).
main_heating_index_number=(
main.product_id if main.product_id > 0 else None
),
)
],
has_fixed_air_conditioning=ventilation.has_fixed_air_conditioning,

View file

@ -245,6 +245,35 @@ class TestWaterHeatingCoding:
)
class TestMainHeatingEfficiencySource:
"""`from_site_notes` must carry the surveyed PCDB `product_id` onto
`main_heating_index_number`, so the calculator reads the actual appliance's
SEDBUK efficiency (`gas_oil_boiler_record`) instead of a generic Table-4b
default. The site note lodges the boiler by PCDB id (`summer_efficiency` is
always 0), so the index IS the efficiency source (issue #1563).
"""
def _survey(self, **main: object) -> PasHubRdSapSiteNotes:
data = load("pashub_rdsap_site_notes_example1.json")
data["heating_and_hot_water"]["main_heating"].update(main)
return from_dict(PasHubRdSapSiteNotes, data)
def test_product_id_maps_to_index_number(self) -> None:
result = EpcPropertyDataMapper.from_site_notes(self._survey(product_id=18400))
assert (
result.sap_heating.main_heating_details[0].main_heating_index_number
== 18400
)
def test_zero_product_id_leaves_index_none(self) -> None:
# No PCDB product lodged (e.g. selected by efficiency) → no index.
result = EpcPropertyDataMapper.from_site_notes(self._survey(product_id=0))
assert (
result.sap_heating.main_heating_details[0].main_heating_index_number
is None
)
class TestFromSiteNotesExample1:
"""
Fixture: pashub_rdsap_site_notes_example1.json
@ -597,6 +626,7 @@ class TestFromSiteNotesExample1:
condensing=True,
weather_compensator=False,
central_heating_pump_age_str="Unknown",
main_heating_index_number=18400, # PCDB product id → efficiency source
)
],
has_fixed_air_conditioning=False,