From 7281b7b300016a107a6b1c679f342bc41fbf3f92 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 26 May 2026 08:18:33 +0000 Subject: [PATCH] Slice 93: API mapper window_transmission_details from glazing_type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API schema lodges `glazing_type` (int code) per window but `window_transmission_details=None` and `frame_factor=None`. Without per-window U lodgement the cascade falls back to a single global `u_window(None,None,None)=2.5` × total area, which over-shot cert 001479's window W/K by +2.63 (cascade 46.23 vs worksheet 43.60). Fix: `_API_GLAZING_TYPE_TO_TRANSMISSION` lookup translates `glazing_type` → (u_value, solar_transmittance, frame_factor) and the mapper populates `WindowTransmissionDetails` + `frame_factor` per window so the cascade uses its per-window U fast path (each window contributes A × U_eff_individual rather than total_area × U_eff_global). Two codes mapped now: 3 → DG pre-2002 U=2.8 g=0.76 FF=0.70 13 → DG post-2022 Argon U=1.4 g=0.72 FF=0.70 Cert 001479 lodges 8 Main windows at glazing_type=3 + 1 Ext1 window at glazing_type=13 — exactly the manufacturer-lodged worksheet values. The cascade now matches the worksheet's `Windows 1: 13.96 × 2.518 = 35.15 W/K` and `Windows 2: 6.37 × 1.3258 = 8.45 W/K` → **windows W/K EXACT 43.5962**. **Cert 001479 API path: fabric heat loss is now COMPLETELY EXACT across all 6 components** (walls/party/roof/floor/windows/doors all match worksheet at the worksheet's 4 d.p. precision). Total fabric: 139.4957 W/K ✓ (was 122.6130 before Slice 87) walls: 39.7652 ✓ party walls: 17.0700 ✓ roof: 10.3438 ✓ floor: 23.1705 ✓ windows: 43.5962 ✓ doors: 5.5500 ✓ API SAP delta progression through Slices 87-93: Slice 87 baseline: +3.0752 After Slice 90: +1.5298 (party walls) After Slice 91: +1.0970 (descriptive strings + roof desc) After Slice 92: +1.0022 (floor dims) After Slice 93: +1.1846 (windows — fabric now EXACT) The +1.18 SAP gap is now PURELY non-fabric: candidates are internal gains, solar gains, ventilation, MIT, or hot water cascade — to diagnose in the next slice. Golden cert residuals updated for the cascade improvements. Pyright net-zero on mapper.py (33 → 33). Co-Authored-By: Claude Opus 4.7 --- datatypes/epc/domain/mapper.py | 46 ++++++++++++++++++- .../sap/rdsap/tests/test_golden_fixtures.py | 28 +++++------ 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index d5b7e23a..f9fe566d 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -1524,7 +1524,11 @@ class EpcPropertyDataMapper: glazing_gap=w.glazing_gap, orientation=w.orientation, window_type=w.window_type, - frame_factor=w.frame_factor, + frame_factor=( + w.frame_factor + if w.frame_factor is not None + else _API_GLAZING_TYPE_TO_TRANSMISSION.get(w.glazing_type, (None, None, None))[2] + ), glazing_type=w.glazing_type, window_width=_measurement_value(w.window_width), window_height=_measurement_value(w.window_height), @@ -1532,6 +1536,16 @@ class EpcPropertyDataMapper: window_location=w.window_location, window_wall_type=w.window_wall_type, permanent_shutters_present=w.permanent_shutters_present == "Y", + # When the API lodgement carries explicit + # `window_transmission_details`, pass through verbatim + # (Manufacturer-lodged U + solar takes precedence over + # the cascade default). Otherwise derive from the + # `glazing_type` integer code via the SAP10 lookup — + # gives the cascade per-window U-values for the + # `windows_have_per_window_u` fast path in + # `heat_transmission.py`, matching the cohort + # Elmhurst behaviour (which sets these per-window via + # `make_window`). window_transmission_details=( WindowTransmissionDetails( u_value=w.window_transmission_details.u_value, @@ -1539,7 +1553,15 @@ class EpcPropertyDataMapper: solar_transmittance=w.window_transmission_details.solar_transmittance, ) if w.window_transmission_details is not None - else None + else ( + WindowTransmissionDetails( + u_value=_API_GLAZING_TYPE_TO_TRANSMISSION[w.glazing_type][0], + data_source="SAP10 lookup (glazing_type)", + solar_transmittance=_API_GLAZING_TYPE_TO_TRANSMISSION[w.glazing_type][1], + ) + if w.glazing_type in _API_GLAZING_TYPE_TO_TRANSMISSION + else None + ) ), permanent_shutters_insulated=w.permanent_shutters_insulated, ) @@ -2070,6 +2092,26 @@ def _api_roof_construction_str(value: Optional[int]) -> Optional[str]: _API_FLOOR_HEAT_LOSS_EXPOSED: Final[int] = 1 +# GOV.UK API `glazing_type` integer → (u_value W/m²K, g_perpendicular, +# frame_factor) lookup the cascade reads via `window_transmission_ +# details` for per-window cascade fidelity. The cascade defaults to a +# single global `u_window(None,None,None)=2.5` and `_G_PERPENDICULAR_ +# DEFAULT=0.76` when these are unset — close to right for typical +# DG-pre-2002 dwellings but wildly off for newer DG (e.g. cert 001479 +# Ext1 lodges glazing_type=13 → manufacturer DG post-2022 Argon +# U=1.4 / g=0.72, vs cascade default U=2.5). +# +# Codes observed across the 10 golden fixtures: 3 (Main DG pre-2002) +# and 13 (Ext1 post-2022 Argon). The wider SAP10.2 glazing-type enum +# (4-12, 14+) is not yet mapped — incremental coverage as new +# fixtures surface them. +_API_GLAZING_TYPE_TO_TRANSMISSION: Dict[int, tuple[float, float, float]] = { + # (u_value, solar_transmittance/g_⊥, frame_factor) + 3: (2.8, 0.76, 0.70), # Double glazed, pre-2002 + 13: (1.4, 0.72, 0.70), # Double glazed, Argon-filled post-2022 +} + + def _api_build_sap_floor_dimensions( fds: List[Any], floor_heat_loss: Optional[int], diff --git a/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py b/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py index 59676ff0..2c462ccd 100644 --- a/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py +++ b/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py @@ -95,8 +95,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="0300-2747-7640-2526-2135", actual_sap=78, expected_sap_resid=+2, - expected_pe_resid_kwh_per_m2=-0.9139, - expected_co2_resid_tonnes_per_yr=-0.9974, + expected_pe_resid_kwh_per_m2=-0.2955, + expected_co2_resid_tonnes_per_yr=-0.9443, notes=( "Large semi-detached, TFA 526, age D, gas boiler PCDB-listed " "(no Table 4b code). Cert lodges open_flues_count=1 + " @@ -119,8 +119,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="6035-7729-2309-0879-2296", actual_sap=70, expected_sap_resid=-5, - expected_pe_resid_kwh_per_m2=+37.7305, - expected_co2_resid_tonnes_per_yr=+0.8510, + expected_pe_resid_kwh_per_m2=+39.1452, + expected_co2_resid_tonnes_per_yr=+0.8845, notes=( "Mid-terrace, TFA 128, age A, gas combi Table 4b code 104. " "Slice 59 per-bp window apportionment tightens all 3 " @@ -132,9 +132,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( _GoldenExpectation( cert_number="7536-3827-0600-0600-0276", actual_sap=68, - expected_sap_resid=+2, - expected_pe_resid_kwh_per_m2=-11.7633, - expected_co2_resid_tonnes_per_yr=-0.3124, + expected_sap_resid=+1, + expected_pe_resid_kwh_per_m2=-8.8124, + expected_co2_resid_tonnes_per_yr=-0.2337, notes=( "Detached + 2 extensions, TFA 152. Multi-age bps (Main=D, " "Ext1=L, Ext2=F). Slice 59 (per-bp window apportionment) and " @@ -147,8 +147,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="8135-1728-8500-0511-3296", actual_sap=72, expected_sap_resid=+1, - expected_pe_resid_kwh_per_m2=-13.0069, - expected_co2_resid_tonnes_per_yr=-0.2200, + expected_pe_resid_kwh_per_m2=-10.0737, + expected_co2_resid_tonnes_per_yr=-0.1645, notes=( "Semi-detached, TFA 102, age C, gas PCDB-listed. Cert lodges " "blocked_chimneys_count=1. Slice 59 per-bp window apportionment " @@ -160,8 +160,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( cert_number="2130-1033-4050-5007-8395", actual_sap=82, expected_sap_resid=+2, - expected_pe_resid_kwh_per_m2=-44.8941, - expected_co2_resid_tonnes_per_yr=+0.2250, + expected_pe_resid_kwh_per_m2=-43.5103, + expected_co2_resid_tonnes_per_yr=+0.2414, notes=( "End-terrace + 1 extension, TFA 64, gas combi PCDB index 17505, " "postcode DE22 (PCDB Table 172 match), PV: 2× 2.04 kWp arrays " @@ -179,9 +179,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( _GoldenExpectation( cert_number="0390-2254-6420-2126-5561", actual_sap=65, - expected_sap_resid=+1, - expected_pe_resid_kwh_per_m2=-3.5091, - expected_co2_resid_tonnes_per_yr=-0.0136, + expected_sap_resid=+0, + expected_pe_resid_kwh_per_m2=-1.9117, + expected_co2_resid_tonnes_per_yr=+0.0102, notes=( "End-terrace + 1 extension, TFA 80, gas combi PCDB index 18119, " "no PV, no secondary, postcode LN12 (PCDB Table 172 match). "