From bc58cf4930480ba6d1e3f80c0c9da2e3b7c734be Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 14 Jul 2026 21:28:25 +0000 Subject: [PATCH] =?UTF-8?q?Lodge=20explicit=20zero=20roof=20insulation=20f?= =?UTF-8?q?or=20a=20surveyed=20"Insulation=20At:=20None"=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Insulation At: None" is the assessor recording zero loft insulation, not an unknown — with thickness left None, u_roof fell through to the age-band "assumed insulated" default (0.16 W/m²K at band J/K) instead of the Table 16 uninsulated row (2.30), understating roof loss ~14x (issue #1590 bug 2). Harness MAE 2.701 -> 2.632 (the two ~7-SAP roof over-raters correct exactly); ceiling ratcheted to 2.64. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../documents_parser/tests/test_pashub_sap_accuracy.py | 5 ++++- datatypes/epc/domain/mapper.py | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/documents_parser/tests/test_pashub_sap_accuracy.py b/backend/documents_parser/tests/test_pashub_sap_accuracy.py index d660d8747..34141c47b 100644 --- a/backend/documents_parser/tests/test_pashub_sap_accuracy.py +++ b/backend/documents_parser/tests/test_pashub_sap_accuracy.py @@ -81,7 +81,10 @@ _MANIFEST_PATH = _FIXTURES_DIR / "manifest.json" # gap) — see docs/HANDOVER_PASHUB_838_PROBLEM_PROPERTIES.md. Re-baselined to the # full-coverage aggregate; ratchet back DOWN as those two are resolved. _MIN_WITHIN_HALF: float = 0.12 -_MAX_SAP_MAE: float = 2.71 +# Ratcheted 2.71 -> 2.64 by #1590 bug 2 (roof "Insulation At: None" -> explicit +# zero thickness): observed MAE 2.632 — the two ~7-SAP roof over-raters +# (507665533138 / 507670893756) correcting is exactly 14/205 ≈ 0.07 of MAE. +_MAX_SAP_MAE: float = 2.64 _KNOWN_GAP_REASON = ( "pashub `from_site_notes` mapper does not int-code a main-heating " diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 8ce2fc426..1c38228a1 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -5961,6 +5961,14 @@ def _map_roof( ) -> tuple[Optional[str], Optional[Union[str, int]]]: if roof is None: return None, None + # "Insulation At: None" is the assessor explicitly recording ZERO loft + # insulation (no thickness line follows — there is nothing to measure). + # Lodge an explicit 0 so `u_roof` takes the Table 16 uninsulated row + # (2.30 W/m²K) instead of falling through, on thickness=None, to the + # age-band "assumed insulated" default (issue #1590 bug 2). A blank + # `insulation_at` stays None — "not surveyed", not "no insulation". + if roof.insulation_at == "None": + return roof.insulation_at, 0 thickness: Optional[Union[str, int]] = ( roof.insulation_thickness_mm if roof.insulation_thickness_mm is not None