From 1c5675a06375de720c036fbf206d635031642461 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sun, 7 Jun 2026 18:54:00 +0000 Subject: [PATCH] =?UTF-8?q?fix(mapper):=20floor=5Fheat=5Floss=20code=208?= =?UTF-8?q?=20=E2=86=92=20no=20floor=20heat=20loss=20(extension=20over=20h?= =?UTF-8?q?eated=20space)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API floor_heat_loss=8 is observed on EXTENSION building parts whose floor sits over a heated space within the SAME dwelling (an upper-storey extension over a heated room). RdSAP 10 §3 gives an internal floor between heated storeys no floor heat loss — mechanically identical to a code-6 party floor. `_api_floor_type_str` had no entry for 8, raising UnmappedApiCode and blocking certs 0370-2254-6520-2426-5971 and 0997-1206-9806-0715-2904. Map code 8 to the code-6 no-heat-loss string "(another dwelling below)" (consumed by heat_transmission's party-floor suppression; != "Ground floor" so the §5 (12) suspended-timber rule stays inert). Empirically confirmed against both certs: the no-heat-loss treatment lands them within 0.5 of lodged (0370-2254 68.92 vs 69; 0997-1206 40.68 vs 41), whereas Ground-floor / unheated / external mappings miss 0997 by ~4 SAP. Eval computed 906→908. Regression green (only the pre-existing test_total_floor_area fails); pyright net-zero (38=38). Co-Authored-By: Claude Opus 4.8 --- datatypes/epc/domain/mapper.py | 22 ++++++++++++++++--- .../domain/tests/test_from_rdsap_schema.py | 18 +++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index ea1e2d29..a292bcce 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -2647,16 +2647,32 @@ def _api_floor_construction_str(value: Optional[int]) -> Optional[str]: # the §5 (12) suspended-timber rule stays # inert (short-circuits exactly as None did). # 7 = "Ground floor" — typical ground-floor heat loss +# 8 = "(another dwelling below)" — observed on EXTENSION building parts +# whose floor sits over a heated space +# within the SAME dwelling (an upper-storey +# extension over a heated room). RdSAP 10 §3 +# gives an internal floor between heated +# storeys no floor heat loss — mechanically +# identical to a code-6 party floor, so it +# reuses that suppression string (consumed +# by heat_transmission's party-floor +# override; != "Ground floor" so §5 (12) +# stays inert). Empirically confirmed: both +# code-8 certs land within 0.5 of lodged +# (0370-2254 68.9 vs 69; 0997-1206 40.7 vs +# 41), while Ground-floor / unheated / +# external mappings miss 0997 by ~4 SAP. # -# Codes 4/5/8+ are not yet observed in any fixture; the strict-raise -# path catches them at the extraction boundary so the next cert forces -# an explicit mapping decision. +# Codes 4/5 are not yet observed in any fixture; the strict-raise path +# catches them at the extraction boundary so the next cert forces an +# explicit mapping decision. _API_FLOOR_HEAT_LOSS_TO_FLOOR_TYPE: Dict[int, Optional[str]] = { 1: "To external air", 2: "To unheated space", 3: "To unheated space", 6: "(another dwelling below)", 7: "Ground floor", + 8: "(another dwelling below)", } diff --git a/datatypes/epc/domain/tests/test_from_rdsap_schema.py b/datatypes/epc/domain/tests/test_from_rdsap_schema.py index f20a5615..c3ebb7c6 100644 --- a/datatypes/epc/domain/tests/test_from_rdsap_schema.py +++ b/datatypes/epc/domain/tests/test_from_rdsap_schema.py @@ -914,6 +914,24 @@ class TestApiFloorTypeCode: # Act / Assert assert _api_floor_type_str(7) == "Ground floor" + def test_code_8_maps_to_no_floor_heat_loss(self) -> None: + # Arrange — code 8 is observed on EXTENSION building parts whose + # floor sits over a heated space within the same dwelling (an + # upper-storey extension over a heated room). RdSAP 10 §3 treats an + # internal floor between heated storeys as no floor heat loss — + # mechanically identical to a code-6 party floor. Empirically + # confirmed: routing code 8 to the no-heat-loss treatment lands + # both code-8 certs within 0.5 of lodged (0370-2254 68.9 vs 69; + # 0997-1206 40.7 vs 41), whereas Ground-floor / unheated / external + # mappings miss 0997 by ~4 SAP. Reuses code 6's suppression string + # (consumed by heat_transmission's party-floor override); it is + # != "Ground floor", so the §5 (12) suspended-timber rule stays + # inert. Pre-this, code 8 raised UnmappedApiCode, blocking the cert. + from datatypes.epc.domain.mapper import _api_floor_type_str # pyright: ignore[reportPrivateUsage] + + # Act / Assert — no-heat-loss signal (not None, not "Ground floor"). + assert _api_floor_type_str(8) == "(another dwelling below)" + class TestApiFloorConstructionCode: """`_api_floor_construction_str` maps the GOV.UK API integer