From f1c6825cae95bdd38b921ae0586602bfc724e19e Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Fri, 19 Jun 2026 13:18:45 +0000 Subject: [PATCH] =?UTF-8?q?Resolve=20a=20landlord=20double-glazing=20overr?= =?UTF-8?q?ide=20to=20its=20glazing=20code=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../epc/property_overlays/glazing_overlay.py | 23 +++++++++++++++++++ domain/modelling/simulation.py | 23 +++++++++++++++++++ tests/domain/epc/test_glazing_overlay.py | 20 ++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 domain/epc/property_overlays/glazing_overlay.py create mode 100644 tests/domain/epc/test_glazing_overlay.py diff --git a/domain/epc/property_overlays/glazing_overlay.py b/domain/epc/property_overlays/glazing_overlay.py new file mode 100644 index 00000000..9da7582d --- /dev/null +++ b/domain/epc/property_overlays/glazing_overlay.py @@ -0,0 +1,23 @@ +"""Map a Landlord-Override glazing value to a glazing Simulation Overlay. + +A glazing value is one canonical glazing description carrying type + era +("Double glazing, 2002 or later", "Single glazing", "Triple glazing, 2002 or +later"). The calculator derives each window's U-value from its SAP10 +`glazing_type` code via the RdSAP Table 24 cascade, so the overlay decomposes +the value into that code and emits a whole-dwelling `GlazingOverlay` (a landlord +describes the dwelling's glazing as a whole, with no per-window geometry, so +`building_part` is ignored). `_fold_glazing` expands it across every window. +Unresolvable values produce no overlay. +""" + +from __future__ import annotations + +from typing import Optional + +from domain.modelling.simulation import EpcSimulation + + +def glazing_overlay_for( + glazing_value: str, building_part: int +) -> Optional[EpcSimulation]: + raise NotImplementedError diff --git a/domain/modelling/simulation.py b/domain/modelling/simulation.py index 624826f6..217d446b 100644 --- a/domain/modelling/simulation.py +++ b/domain/modelling/simulation.py @@ -73,6 +73,28 @@ class WindowOverlay: solar_transmittance: Optional[float] = None +@dataclass(frozen=True) +class GlazingOverlay: + """All-optional partial of the dwelling's whole-glazing state — the + correction a Landlord Override makes when the lodged glazing is wrong. + + Unlike a per-window `WindowOverlay` (keyed by `sap_windows` index), this + targets no single window: a landlord describes the dwelling's glazing as a + whole ("Double glazing, 2002 or later") with no per-window geometry, so the + overlay builder (which never sees the baseline window list) emits one of + these and `_fold_glazing` expands it across every `sap_windows` entry. + + `glazing_type` is the SAP10 glazing-type code (Table 24 / `u_window` + cascade: 1=single, 2=double 2002-2021, 3=double pre-2002, 9=triple 2002+, + …). The fold sets it on every window AND clears each window's lodged + transmission U-value, so the Table-24 cascade re-derives the corrected U + from the new type (the lodged U was for the OLD, mis-recorded glazing). + A `None` field means "leave the baseline value unchanged". + """ + + glazing_type: Optional[int] = None + + @dataclass(frozen=True) class LightingOverlay: """All-optional partial of the dwelling's fixed-lighting bulb counts — the @@ -220,6 +242,7 @@ class EpcSimulation: windows: Mapping[int, WindowOverlay] = field(default_factory=_no_windows) ventilation: Optional[VentilationOverlay] = None lighting: Optional[LightingOverlay] = None + glazing: Optional[GlazingOverlay] = None heating: Optional[HeatingOverlay] = None secondary_heating: Optional[SecondaryHeatingOverlay] = None solar: Optional[SolarOverlay] = None diff --git a/tests/domain/epc/test_glazing_overlay.py b/tests/domain/epc/test_glazing_overlay.py new file mode 100644 index 00000000..b54e134e --- /dev/null +++ b/tests/domain/epc/test_glazing_overlay.py @@ -0,0 +1,20 @@ +"""The Landlord-Override glazing → glazing Simulation Overlay mapping. + +A glazing value resolves to the SAP10 `glazing_type` code the calculator's +Table-24 cascade reads; the overlay is whole-dwelling (expanded across every +window by `_fold_glazing`). +""" + +from __future__ import annotations + +from domain.epc.property_overlays.glazing_overlay import glazing_overlay_for + + +def test_double_glazing_post_2002_overlays_its_glazing_code() -> None: + # Act + simulation = glazing_overlay_for("Double glazing, 2002 or later", 0) + + # Assert — double glazing 2002-2021 is SAP10 glazing_type code 2. + assert simulation is not None + assert simulation.glazing is not None + assert simulation.glazing.glazing_type == 2