diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 97f8b218b..41066c19d 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -6026,7 +6026,7 @@ def _map_sap_window(window: Window) -> SapWindow: glazing_gap=window.glazing_gap, orientation=window.orientation, window_type=window.window_type, - glazing_type=window.glazing_type, + glazing_type=_pashub_glazing_type_int(window.glazing_type), window_width=window.width_m, window_height=window.height_m, draught_proofed=window.draught_proofed, @@ -7261,6 +7261,37 @@ def _pashub_wall_insulation_type_int(label: Optional[str]) -> Union[int, str]: return _PASHUB_WALL_INSULATION_TYPE_TO_SAP10[key] +# PasHub surveyed window `glazing_type` label → SAP10 cascade glazing code that +# the calculator reads for both the window U (`_GLAZING_CODE_TO_UWINDOW` / +# u_window, RdSAP 10 Table 24) and the solar g-value +# (`_G_PERPENDICULAR_BY_GLAZING_TYPE`, Table 6b). The install-year band is +# load-bearing on U (double pre-2002 = 2.8, 2002-2021 = 2.0, post-2022 = 1.4); +# an unknown install date defaults to the conservative pre-2002 code (per the +# `_API_GLAZING_TYPE_TO_TRANSMISSION` "pre-2002 / unknown install date" pairing). +_PASHUB_GLAZING_TYPE_TO_SAP10: Dict[str, int] = { + "Double glazing installed before 2002": 3, # double pre-2002 (U 2.8) + "Double glazing installed between 2002 - 2021": 2, # double 2002-2022 (U 2.0) + "Double glazing, Unknown install date": 3, # unknown → pre-2002 default (U 2.8) + "Double glazing installed during or after 2022": 13, # double 2022+ (U 1.4) +} + + +def _pashub_glazing_type_int(label: Optional[str]) -> Union[int, str]: + """Resolve a PasHub surveyed window `glazing_type` label to its SAP10 cascade + glazing code at the mapper boundary. A blank label is "no lodging" and passes + through as the empty string (the field is `Union[int, str]`; the calculator's + isinstance-int guards then fall to their double-glazed defaults); a non-empty + label the lookup does not cover strict-raises `UnmappedPasHubLabel` so the + coverage gap is fixed here rather than silently pinning every window to the + double-pre-2002 U=2.8 default downstream (#1562).""" + if label is None or not label.strip(): + return "" + key = label.strip() + if key not in _PASHUB_GLAZING_TYPE_TO_SAP10: + raise UnmappedPasHubLabel("glazing type", label) + return _PASHUB_GLAZING_TYPE_TO_SAP10[key] + + def _resolve_elmhurst_underfloor_subtype( main_floor: ElmhurstFloorDetails, main_age_band: str, diff --git a/datatypes/epc/domain/tests/test_from_site_notes.py b/datatypes/epc/domain/tests/test_from_site_notes.py index 6a1b7e19c..c6647ca82 100644 --- a/datatypes/epc/domain/tests/test_from_site_notes.py +++ b/datatypes/epc/domain/tests/test_from_site_notes.py @@ -151,6 +151,43 @@ class TestWallInsulationTypeCoding: EpcPropertyDataMapper.from_site_notes(survey) +class TestGlazingTypeCoding: + """`from_site_notes` must int-code the PAS Hub window `glazing_type` label to + the SAP10 cascade glazing code the calculator reads (`_GLAZING_CODE_TO_UWINDOW` + for the window U, `_G_PERPENDICULAR_BY_GLAZING_TYPE` for solar g), not copy the + raw label. A raw label reads as `None`, so every window falls to the double- + pre-2002 U=2.8 default — over-counting heat loss on the ~852/1669 windows the + survey dates to 2002-2021 (which are U=2.0) (Guinness cohort, issue #1562). + """ + + @pytest.mark.parametrize( + "label, expected_code", + [ + ("Double glazing installed before 2002", 3), # double pre-2002, U 2.8 + ("Double glazing installed between 2002 - 2021", 2), # double 2002-2022, U 2.0 + ("Double glazing, Unknown install date", 3), # unknown → pre-2002 default, U 2.8 + ("Double glazing installed during or after 2022", 13), # double 2022+, U 1.4 + ("", ""), # no lodging — passes through (calc nulls it) + ], + ) + def test_label_maps_to_sap_code( + self, label: str, expected_code: "int | str" + ) -> None: + data = load("pashub_rdsap_site_notes_example1.json") + for window in data["windows"]: + window["glazing_type"] = label + survey = from_dict(PasHubRdSapSiteNotes, data) + result = EpcPropertyDataMapper.from_site_notes(survey) + assert result.sap_windows[0].glazing_type == expected_code + + def test_unknown_label_strict_raises(self) -> None: + data = load("pashub_rdsap_site_notes_example1.json") + data["windows"][0]["glazing_type"] = "Quadruple glazing, vacuum" + survey = from_dict(PasHubRdSapSiteNotes, data) + with pytest.raises(UnmappedPasHubLabel): + EpcPropertyDataMapper.from_site_notes(survey) + + class TestFromSiteNotesExample1: """ Fixture: pashub_rdsap_site_notes_example1.json @@ -325,9 +362,9 @@ class TestFromSiteNotesExample1: assert result.sap_windows[0].orientation == "South East" def test_window_glazing_type(self, result: EpcPropertyData) -> None: - assert ( - result.sap_windows[0].glazing_type == "Double glazing, Unknown install date" - ) + # windows[].glazing_type: "Double glazing, Unknown install date" + # → SAP10 cascade code 3 (double pre-2002 default, U 2.8) + assert result.sap_windows[0].glazing_type == 3 # --- building parts --- @@ -529,7 +566,7 @@ class TestFromSiteNotesExample1: glazing_gap="16 mm or more", orientation="South East", window_type="Window", - glazing_type="Double glazing, Unknown install date", + glazing_type=3, # "Double glazing, Unknown install date" → cascade code 3 window_width=1.0, window_height=1.36, draught_proofed=True, @@ -542,7 +579,7 @@ class TestFromSiteNotesExample1: glazing_gap="16 mm or more", orientation="South East", window_type="Window", - glazing_type="Double glazing, Unknown install date", + glazing_type=3, # "Double glazing, Unknown install date" → cascade code 3 window_width=0.96, window_height=1.33, draught_proofed=True, @@ -555,7 +592,7 @@ class TestFromSiteNotesExample1: glazing_gap="16 mm or more", orientation="North West", window_type="Window", - glazing_type="Double glazing, Unknown install date", + glazing_type=3, # "Double glazing, Unknown install date" → cascade code 3 window_width=0.96, window_height=1.04, draught_proofed=True, @@ -568,7 +605,7 @@ class TestFromSiteNotesExample1: glazing_gap="16 mm or more", orientation="North West", window_type="Window", - glazing_type="Double glazing, Unknown install date", + glazing_type=3, # "Double glazing, Unknown install date" → cascade code 3 window_width=0.97, window_height=1.02, draught_proofed=True,