From faf116bd7075bcb272d267e6d962cf4d32a634b9 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 12:01:34 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.30:=20extend=20g=5FL=20+=20g?= =?UTF-8?q?=E2=8A=A5=20Table=206b=20to=20RdSAP=2021=20codes=208-15=20?= =?UTF-8?q?=E2=80=94=20closes=20API=20path=20cohort=20residual=20cluster?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the RdSAP 21 schema in [datatypes/epc/domain/epc_codes.csv][1], the `glazing_type` enum extends to 15 codes; the legacy SAP 10.2 Table 6b cascade lookups in `internal_gains.py:106` and `solar_gains.py:178` only knew codes 1-7. Every API-path cert in the cohort lodges `glazing_type` via the RdSAP 21 numbering, and triple-glazed lodgements surface as **code 14** ("triple glazing, installed 2022+"). Pre-slice the cascade fell through to the 0.80 / 0.76 double-glazed defaults for codes 8-15: Internal gains g_L (Table 6b): code 14 → default 0.80 (DG) vs spec 0.70 (TG) → daylight factor over-bonused → lighting kWh under-counted Solar gains g⊥ (Table 6b): code 14 → default 0.76 (DG) vs spec 0.68 (TG) → solar gains over-counted For cert 0350-2968-2650-2796-5255 (semi-detached, 9 triple-glazed windows lodged as code 14), this drove: lighting_kwh_per_yr: cascade 221.79 vs Summary-path 228.44 (-6.65 kWh/yr — daylight bonus too generous → lighting too low) space_heating_kwh_per_yr: cascade 7000.21 vs Summary-path 6996.94 (+3.28 kWh/yr — extra solar gains lower HP demand) net ECF: -0.0022 vs Summary-path → SAP +0.031 Same mechanism on the other 5 cohort-1 ASHP API certs. Fix: extend both lookup tables with the RdSAP 21 additions per the schema CSV semantics: | code | description (RdSAP 21) | g_L | g⊥ | |------|----------------------------------|------|------| | 8 | triple glazing, known data | 0.70 | 0.68 | | 9 | triple glazing, 2002-2022 | 0.70 | 0.68 | | 10 | triple glazing, pre-2002 | 0.70 | 0.68 | | 11 | secondary glazing, normal-E | 0.80 | 0.76 | | 12 | secondary glazing, low-E | 0.80 | 0.76 | | 13 | double glazing, 2022+ | 0.80 | 0.76 | | 14 | triple glazing, 2022+ | 0.70 | 0.68 | | 15 | single glazing, known data | 0.90 | 0.85 | Solar gains also adds code 7 (double known data) for `_G_PERPENDICULAR_BY_GLAZING_TYPE` to align with the existing `_G_LIGHT_BY_GLAZING_CODE` code-7 entry (which already mapped to 0.80 = double). Outcome — Cohort-1 ASHP cohort API path: cert 0380: +0.025 → +1e-6 (close to exact) cert 0350: +0.031 → +2.2e-5 (close to exact) cert 2225: +0.029 → -4.8e-5 (close to exact) cert 2636: +0.015 → -0.015 (sign flip; cantilever-specific residual surfaces; same |Δ| as Summary) cert 3800: +0.023 → -2e-5 (close to exact) cert 9285: +0.029 → -3.4e-5 (close to exact) 5 of 6 API path certs now sit at <1e-4 vs worksheet. Cert 2636 matches its Summary-path residual (-0.015) — the cantilever fixture has its own non-glazing residual to be diagnosed separately. Cohort-2 Summary path unchanged (33 exact + 5 ≤0.07) — the cohort-2 certs lodge glazing codes 1-7 (RdSAP 17 numbering still surfaces in Elmhurst Summary PDF lookups), so codes 8-15 only affect the RdSAP-21-schema API path. Golden API fixture pins updated to reflect the tightened cascade-vs-API alignment (7 certs: 0380, 0350, 2225, 2636, 3800, 9285, 9418). SAP integer residuals unchanged (all sit at +0). Pyright net-zero on touched files (22 → 22). Tests: 710 → **711** pass (+1 new: cert 0350 fixture-shape test for glazing_type=14 routing to g⊥=0.68 with `total_solar_gains_monthly_w[0] ≈ 67.00 W` (vs pre-slice 74.88 W at the DG default), proving code 14 hits the triple-glazed Table 6b row.) 10 expected fails unchanged. [1]: datatypes/epc/domain/epc_codes.csv (RdSAP-Schema-21.0.1). Co-Authored-By: Claude Opus 4.7 --- .../rdsap/tests/test_golden_fixtures.py | 28 +++++++-------- .../worksheet/internal_gains.py | 17 +++++++++ .../sap10_calculator/worksheet/solar_gains.py | 16 +++++++++ .../worksheet/tests/test_solar_gains.py | 36 +++++++++++++++++++ 4 files changed, 83 insertions(+), 14 deletions(-) diff --git a/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py b/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py index 2a05962b..90705e70 100644 --- a/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py +++ b/domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py @@ -258,8 +258,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="0380-2471-3250-2596-8761", actual_sap=89, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-14.6848, - expected_co2_resid_tonnes_per_yr=+0.2780, + expected_pe_resid_kwh_per_m2=-14.5971, + expected_co2_resid_tonnes_per_yr=+0.2785, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, semi-detached bungalow " "TFA 60.43 age D, PV 3 kWp. Worksheet SAP 88.5104 — slice " @@ -275,8 +275,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="0350-2968-2650-2796-5255", actual_sap=84, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-7.8741, - expected_co2_resid_tonnes_per_yr=+0.1701, + expected_pe_resid_kwh_per_m2=-7.7832, + expected_co2_resid_tonnes_per_yr=+0.1709, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert. " "Worksheet SAP 84.1367 — cascade integer matches lodged." @@ -286,8 +286,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="2225-3062-8205-2856-7204", actual_sap=89, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-11.8557, - expected_co2_resid_tonnes_per_yr=+0.2621, + expected_pe_resid_kwh_per_m2=-11.7684, + expected_co2_resid_tonnes_per_yr=+0.2628, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " "PV. Worksheet SAP 88.7921. Slice 102f-prep.8 closed the " @@ -298,8 +298,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="2636-0525-2600-0401-2296", actual_sap=86, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-9.6692, - expected_co2_resid_tonnes_per_yr=+0.2193, + expected_pe_resid_kwh_per_m2=-9.5780, + expected_co2_resid_tonnes_per_yr=+0.2200, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with " "PV + 3.74 m² cantilever exposed floor + 12.76 m² alt wall. " @@ -313,8 +313,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="3800-8515-0922-3398-3563", actual_sap=86, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-9.6838, - expected_co2_resid_tonnes_per_yr=+0.2603, + expected_pe_resid_kwh_per_m2=-9.6121, + expected_co2_resid_tonnes_per_yr=+0.2609, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert. " "Worksheet SAP 86.1458." @@ -324,8 +324,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="9285-3062-0205-7766-7200", actual_sap=84, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-8.0466, - expected_co2_resid_tonnes_per_yr=+0.1564, + expected_pe_resid_kwh_per_m2=-7.9597, + expected_co2_resid_tonnes_per_yr=+0.1571, notes=( "Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert. " "Worksheet SAP 84.1369." @@ -335,8 +335,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="9418-3062-8205-3566-7200", actual_sap=85, expected_sap_resid=+0, - expected_pe_resid_kwh_per_m2=-9.5795, - expected_co2_resid_tonnes_per_yr=+0.2311, + expected_pe_resid_kwh_per_m2=-9.4849, + expected_co2_resid_tonnes_per_yr=+0.2317, notes=( "Daikin Altherma EDLQ05CAV3 PCDB 102421 (heating_duration " "code '24' — continuous, all days at Th). Worksheet SAP " diff --git a/domain/sap10_calculator/worksheet/internal_gains.py b/domain/sap10_calculator/worksheet/internal_gains.py index 90071b1c..084e1797 100644 --- a/domain/sap10_calculator/worksheet/internal_gains.py +++ b/domain/sap10_calculator/worksheet/internal_gains.py @@ -103,6 +103,14 @@ _LIGHTING_L8C_EFFICACY_LM_PER_W: Final[float] = 21.3 # glazed = 0.90; double-glazed variants = 0.80; triple-glazed = 0.70. # Mirrors the SAP code mapping in cert_to_inputs._g_perpendicular but # returns the light column, not solar. +# +# Codes 1-7 follow the legacy SAP 10.2 Table 6b ordering (also matched +# by the RdSAP 17 schema for the modal cohort lodgement values 2/3/6). +# Codes 8-15 are RdSAP-21 schema additions (per +# datatypes/epc/domain/epc_codes.csv) — every API-path cert lodges its +# glazing-type integer via the RdSAP 21 enum, and triple-glazed certs +# in the cohort surface as code 14 (triple 2022+) which previously fell +# through to the 0.80 default and over-bonused the daylight factor. _G_LIGHT_BY_GLAZING_CODE: Final[dict[int, float]] = { 1: 0.90, # single glazed 2: 0.80, # double glazed (air filled, pre-2002) @@ -111,6 +119,15 @@ _G_LIGHT_BY_GLAZING_CODE: Final[dict[int, float]] = { 5: 0.80, # double glazed (low-E argon) 6: 0.70, # triple glazed 7: 0.80, # secondary glazing + # RdSAP 21 schema extensions + 8: 0.70, # triple glazing, known data + 9: 0.70, # triple glazing, installed 2002-2022 + 10: 0.70, # triple glazing, installed pre-2002 + 11: 0.80, # secondary glazing, normal emissivity + 12: 0.80, # secondary glazing, low emissivity + 13: 0.80, # double glazing, installed 2022+ + 14: 0.70, # triple glazing, installed 2022+ + 15: 0.90, # single glazing, known data } _G_LIGHT_DEFAULT: Final[float] = 0.80 # treat unknowns as DG (modal) diff --git a/domain/sap10_calculator/worksheet/solar_gains.py b/domain/sap10_calculator/worksheet/solar_gains.py index 8e94e9c5..d5f747c4 100644 --- a/domain/sap10_calculator/worksheet/solar_gains.py +++ b/domain/sap10_calculator/worksheet/solar_gains.py @@ -175,6 +175,11 @@ def window_solar_gain_w( # SAP 10.2 Table 6b — total solar energy transmittance g⊥ at normal incidence # by SAP10 glazing_type code (p178). Default 0.76 (modal double-glazed UK # stock) when the cert's glazing_type is missing or unrecognised. +# +# Codes 1-6 follow the SAP 10.2 Table 6b ordering. Codes 7-15 are RdSAP-21 +# schema additions (per datatypes/epc/domain/epc_codes.csv); triple-glazed +# cohort certs lodge code 14 (triple 2022+) which previously fell through +# to the 0.76 DG default and over-counted solar gains. _G_PERPENDICULAR_BY_GLAZING_TYPE: Final[dict[int, float]] = { 1: 0.85, # single glazed 2: 0.76, # double glazed (air or argon filled) — 2002-2022 @@ -182,6 +187,17 @@ _G_PERPENDICULAR_BY_GLAZING_TYPE: Final[dict[int, float]] = { 4: 0.63, # double glazed low-E soft-coat 5: 0.76, # window with secondary glazing 6: 0.68, # triple glazed (air or argon filled) + # RdSAP 21 schema extensions — triple-glazed variants all share the + # same Table 6b g⊥; secondary glazing mirrors the existing code 5 value. + 7: 0.76, # double glazing, known data + 8: 0.68, # triple glazing, known data + 9: 0.68, # triple glazing, installed 2002-2022 + 10: 0.68, # triple glazing, installed pre-2002 + 11: 0.76, # secondary glazing, normal emissivity + 12: 0.76, # secondary glazing, low emissivity + 13: 0.76, # double glazing, installed 2022+ + 14: 0.68, # triple glazing, installed 2022+ + 15: 0.85, # single glazing, known data } _G_PERPENDICULAR_DEFAULT: Final[float] = 0.76 diff --git a/domain/sap10_calculator/worksheet/tests/test_solar_gains.py b/domain/sap10_calculator/worksheet/tests/test_solar_gains.py index 2298f554..0374ce2c 100644 --- a/domain/sap10_calculator/worksheet/tests/test_solar_gains.py +++ b/domain/sap10_calculator/worksheet/tests/test_solar_gains.py @@ -99,6 +99,42 @@ def test_solar_gains_from_cert_prefers_manufacturer_g_perpendicular_over_table_6 assert result.total_solar_gains_monthly_w[0] == pytest.approx(74.8788, abs=5e-3) +def test_solar_gains_recognises_rdsap_21_triple_glazed_code_14_as_triple() -> None: + """RdSAP 21 schema (per datatypes/epc/domain/epc_codes.csv) adds + glazing_type codes 8-15 to the legacy SAP 10.2 Table 6b codes 1-7. + Triple-glazed lodgements surface as code 14 ("triple glazing, + installed 2022+") on every cohort-1 API path triple-glazed cert + (0380/0350/2225/2636/3800/9285/9418). Pre-slice S0380.30 the cascade + fell through to the 0.76 double-glazed default for codes 8-15, + over-counting solar gains and pushing API path SAP residuals to + +0.014..+0.031. Code 14 must route to the same g⊥=0.68 as code 6 + ("triple glazed") with no `window_transmission_details` override.""" + # Arrange — identical geometry as the existing + # `prefers_manufacturer_g_perpendicular_over_table_6b` test but with + # `glazing_type=14` and `window_transmission_details=None` so the + # cascade falls back to the Table 6b lookup. + window = make_window( + orientation=4, width=5.52, height=1.0, glazing_type=14, + ) + window.window_transmission_details = None + epc = make_minimal_sap10_epc( + total_floor_area_m2=53.0, + sap_windows=[window], + ) + + # Act + result = solar_gains_from_cert( + epc=epc, region=0, overshading=OvershadingCategory.AVERAGE, + ) + + # Assert — code 14 lookup returns g⊥=0.68 (matching code 6 triple). + # Same window at g⊥=0.76 (DG default) would give 74.8788 W Jan; + # at g⊥=0.68 (TG) the gain scales by 0.68/0.76 = 0.8947 → + # 74.8788 × 0.8947 ≈ 67.00 W Jan. Pre-slice the cascade returned + # 74.8788 (DG default); post-slice 67.00. + assert abs(result.total_solar_gains_monthly_w[0] - 67.00) < 0.5 + + def test_z_solar_for_overshading_returns_table_6d_first_column() -> None: # Arrange — SAP 10.2 Table 6d "Winter solar access factor (for calculation # of solar gains for heating)" — first numeric column on p178. Used for