From 6bb1843be6e9dddb921f70ef4be6f7df5830338a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 14 Jul 2026 17:43:37 +0000 Subject: [PATCH] Map PasHub boiler PCDB product_id to main_heating_index_number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_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) --- datatypes/epc/domain/mapper.py | 5 ++++ .../epc/domain/tests/test_from_site_notes.py | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 166e40686..47e3b0492 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -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, diff --git a/datatypes/epc/domain/tests/test_from_site_notes.py b/datatypes/epc/domain/tests/test_from_site_notes.py index 324ddff45..961191cc2 100644 --- a/datatypes/epc/domain/tests/test_from_site_notes.py +++ b/datatypes/epc/domain/tests/test_from_site_notes.py @@ -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,