Merge branch 'main' into feature/pashub-map-main-heating-control-1557

This commit is contained in:
Daniel Roth 2026-07-14 16:47:17 +00:00
commit 1e9231a086
2 changed files with 76 additions and 8 deletions

View file

@ -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,

View file

@ -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,