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). "