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 ab5afd2e..0fbd4bb5 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 @@ -132,13 +132,15 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( _GoldenExpectation( cert_number="7536-3827-0600-0600-0276", actual_sap=68, - expected_sap_resid=+4, - expected_pe_resid_kwh_per_m2=-24.7328, - expected_co2_resid_tonnes_per_yr=-0.6580, + expected_sap_resid=+3, + expected_pe_resid_kwh_per_m2=-22.5292, + expected_co2_resid_tonnes_per_yr=-0.5993, notes=( - "Detached + 2 extensions, TFA 152, age D, gas PCDB. Slice 59 " - "per-bp window apportionment tightens PE -27.17 → -24.73 and " - "CO2 -0.72 → -0.66; SAP residual unchanged at +4." + "Detached + 2 extensions, TFA 152. Multi-age bps (Main=D, " + "Ext1=L, Ext2=F). Slice 59 (per-bp window apportionment) and " + "Slice 60 (dwelling-wide thermal bridging y from primary bp's " + "age band, not per-bp) jointly tightened: SAP +4 → +3, PE " + "-27.17 → -22.53, CO2 -0.72 → -0.60." ), ), _GoldenExpectation( diff --git a/packages/domain/src/domain/sap/worksheet/heat_transmission.py b/packages/domain/src/domain/sap/worksheet/heat_transmission.py index b0973689..37a3f935 100644 --- a/packages/domain/src/domain/sap/worksheet/heat_transmission.py +++ b/packages/domain/src/domain/sap/worksheet/heat_transmission.py @@ -395,6 +395,17 @@ def heat_transmission_from_cert( bridging = 0.0 total_external_area = 0.0 + # RdSAP10 Table 21 — thermal-bridging factor `y` is keyed off the + # *dwelling's* age band (typically the Main part's), not per bp. + # Elmhurst's worksheet reports y as a single user-defined value + # applied to total exposed area (see worksheet row "Thermal Bridges + # Bridging User Input Y 0.15"). For multi-bp dwellings with mixed-age + # extensions (cert 001479: Main=C, Ext1=M, Ext2=C), applying per-bp + # y mis-models Ext1's bridging at 0.08 instead of 0.15 — a 0.07 × + # 27 m² ≈ 1.9 W/K under-count on this cert. + primary_age_band = parts[0].construction_age_band + dwelling_y = thermal_bridging_y(age_band=primary_age_band) + # Pre-compute per-bp window areas so each bp's gross wall is reduced by # only the openings physically cut into it. Previously every window # was apportioned to part i==0 (Main); that's heat-loss-invariant when @@ -493,7 +504,10 @@ def heat_transmission_from_cert( description=floor_description, ) upw = u_party_wall(party_wall_construction=party_construction) - y = thermal_bridging_y(age_band=age_band) + # Per-bp `y` for backwards compat: when the bp's own age band + # differs from the dwelling's primary, the cascade applies the + # dwelling-wide value (RdSAP10 Table 21 convention). + y = dwelling_y # RdSAP10 §15 — element gross areas enter the SAP calculator at # 2 d.p. precision. `_part_geometry` rounds gross wall + party