From 49fb6c1b8e09f8792977302170f5de03f26c748c Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 9 Jun 2026 10:42:16 +0000 Subject: [PATCH] fix(glazing): remap divergent RdSAP-21 glazing codes 4/5 to cascade g slots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The g-value tables (_G_PERPENDICULAR_BY_GLAZING_TYPE solar g⊥, _G_LIGHT_BY_GLAZING_CODE daylight g_L) are keyed on the SAP 10.2 Table 6b cascade enum, but _api_cascade_glazing_type only translated code 1. Codes 4 and 5 sit in the 1-6 range where RdSAP-21 and the cascade enum disagree (RdSAP-21 4=secondary/5=single vs cascade 4=double-low-E/5=secondary), so an API single-glazed window read the cascade-5 secondary g (0.76/0.80) instead of single (0.85/0.90), and a secondary window read cascade-4 double-low-E (0.63). Added the {4:5, 5:1} remap entries the existing design comment already anticipated ("only divergent codes need a remap"). Correctness fix: solar/daylight gains are second-order, so eval is unchanged (56.66% within-0.5, 0 certs flip) — the dominant single-glazing error was the U-value, closed in a0432977's Table 24 transmission map. This closes the keying inconsistency to prevent future drift. 4 AAA tests, goldens + gate green, pyright net-zero (38=38). Co-Authored-By: Claude Opus 4.8 --- datatypes/epc/domain/mapper.py | 10 ++++ .../domain/tests/test_from_rdsap_schema.py | 51 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 1f55d4e3..72e10f44 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -2988,6 +2988,16 @@ def _api_glazing_transmission( # remap — incremental coverage as new fixtures surface them. _API_TO_SAP10_CASCADE_GLAZING_CODE: Dict[int, int] = { 1: 2, # RdSAP 21 DG pre-2002 → cascade DG (g_L=0.80, not single 0.90) + # RdSAP-21 codes 4 and 5 DIVERGE from the cascade enum in the 1-6 range + # (cascade 4 = double low-E soft-coat, 5 = secondary). Without these the + # g-tables read the wrong slot: a single-glazed window (RdSAP-21 5) took + # the cascade-5 secondary g (g⊥ 0.76 / g_L 0.80) for solar + daylight + # gains instead of single (0.85 / 0.90), and a secondary-glazed window + # (RdSAP-21 4) took cascade-4 double-low-E (0.63). Correctness fix + # (gains are second-order — negligible SAP impact; the dominant single- + # glazing error was the U-value, closed in the Table 24 transmission map). + 4: 5, # RdSAP 21 secondary glazing → cascade secondary slot (0.76/0.80) + 5: 1, # RdSAP 21 single glazing → cascade single slot (0.85/0.90) } diff --git a/datatypes/epc/domain/tests/test_from_rdsap_schema.py b/datatypes/epc/domain/tests/test_from_rdsap_schema.py index 8cadcc6b..b882a4c0 100644 --- a/datatypes/epc/domain/tests/test_from_rdsap_schema.py +++ b/datatypes/epc/domain/tests/test_from_rdsap_schema.py @@ -1188,3 +1188,54 @@ class TestApiGlazingTransmissionTable24: # Assert assert result == (2.0, 0.72, 0.70) + + +class TestApiCascadeGlazingCodeDivergentRemap: + """`_api_cascade_glazing_type` must translate the RdSAP-21 glazing codes + that DIVERGE from the SAP 10.2 Table 6b cascade enum the g-value tables + (`_G_PERPENDICULAR_BY_GLAZING_TYPE` / `_G_LIGHT_BY_GLAZING_CODE`) are + keyed on. Only code 1 was ever remapped; codes 4 and 5 sit in the 1-6 + range where the two enums disagree — RdSAP-21 4=secondary / 5=single, + cascade 4=double-low-E / 5=secondary — so an API single-glazed window + (5) read the cascade-5 (secondary) g slot for solar + daylight gains.""" + + def test_single_glazing_code_5_remaps_to_cascade_single_slot_1(self) -> None: + # Arrange — RdSAP-21 code 5 = single glazing; cascade single slot + # is 1 (g⊥ 0.85, g_L 0.90), not cascade 5 (secondary, 0.76/0.80). + from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage] + + # Act + result = _api_cascade_glazing_type(5) + + # Assert + assert result == 1 + + def test_secondary_glazing_code_4_remaps_to_cascade_secondary_slot_5(self) -> None: + # Arrange — RdSAP-21 code 4 = secondary glazing; cascade secondary + # slot is 5 (g⊥ 0.76, g_L 0.80), not cascade 4 (double low-E 0.63). + from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage] + + # Act + result = _api_cascade_glazing_type(4) + + # Assert + assert result == 5 + + def test_double_pre_2002_code_1_remap_unchanged(self) -> None: + # Arrange — regression guard: the existing code-1 remap (→2) stands. + from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage] + + # Act + result = _api_cascade_glazing_type(1) + + # Assert + assert result == 2 + + def test_rdsap21_native_codes_pass_through(self) -> None: + # Arrange — codes 9-15 already coincide with the g-table's RdSAP-21 + # extension slots, so they must pass through untranslated. + from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage] + + # Act / Assert + assert _api_cascade_glazing_type(14) == 14 + assert _api_cascade_glazing_type(9) == 9