Slice 60: thermal bridging y is dwelling-wide, not per-bp

`heat_transmission_from_cert` computed `y = thermal_bridging_y(age_
band=part.construction_age_band)` per bp, then applied each bp's y
to its own external area. That mis-models multi-age dwellings:
RdSAP10 Table 21 indexes y by the *dwelling's* age band, and Elmhurst's
worksheet reports y as a single user-defined value applied to total
exposed area (cert 001479 worksheet: "Thermal Bridges Bridging User
Input Y 0.15").

For cohort certs with uniform age-band bps the change is heat-loss-
invariant. For cert 001479 (Main=C → 0.15, Ext1=M → 0.08, Ext2=C →
0.15) the cascade was under-counting Ext1's bridging by 0.07 × 27.28
m² ≈ 1.9 W/K. For golden cert 7536-3827 (Main=D, Ext1=L, Ext2=F) the
same per-bp split was costing ~2 W/K of bridging.

Use the primary part's (parts[0]) age band for a single dwelling-wide
`dwelling_y`, applied across all parts in the heat-loss loop.

Cert 001479 chain pin closes another step: cascade SAP 70.38 → 70.20
(target 69.0094, delta 1.37 → 1.19). Golden 7536-3827 residuals
tighten in lockstep: SAP +4 → +3, PE -24.73 → -22.53, CO2 -0.66 → -0.60.
Other 7 golden certs unchanged (single-bp or uniform-age multi-bp).

70 of 71 chain+golden+heat-transmission tests green; chain pin still
RED (load-bearing). Pyright net-zero (13-error baseline on
heat_transmission.py preserved).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-24 23:22:05 +00:00
parent 175873b48b
commit 31c01a7e8c
2 changed files with 23 additions and 7 deletions

View file

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

View file

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