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