fix(glazing): remap divergent RdSAP-21 glazing codes 4/5 to cascade g slots

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-09 10:42:16 +00:00
parent 8e1e746a3e
commit 49fb6c1b8e
2 changed files with 61 additions and 0 deletions

View file

@ -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)
}

View file

@ -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