From b22b27c0ff8ac200a252674e29ca43ba2e7a8537 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 20 Jun 2026 13:57:29 +0000 Subject: [PATCH] fix(uvalues): correct rafter-roof age-M default U 0.18->0.15 (RdSAP 10 Table 18 col 2, PDF p.46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A full row-by-row audit of the roof U-value tables (16, 17, 18) and floor tables (19, 20) against the PDF found one numeric error: Table 18 column (2) "Pitched, insulation at rafters" band M is 0.15 W/m²K (footnote (1) only — no country variation; the whole M row converges to 0.15), but _ROOF_RAFTERS_BY_AGE carried 0.18. The rafters column diverges above the joist column at H-L (0.35/0.35/0.20/0.20/0.18) and rejoins it at M=0.15. Everything else in the roof/floor tables is exact: Table 16 joist + rafter thickness ladders, Table 17 room-in-roof (all 6 columns), Table 18 cols (1) joists / (3) flat / (4) room-in-roof, Table 19 (England & Wales) floor insulation defaults, and Table 20 (England) exposed-floor U-values. Known remaining gaps (NOT fixed — zero England-corpus reach, would need a new roof country-override mechanism): Scotland Table 18 footnote (2) K=0.20 (flat/ RR/thatch) and (3) joists-L=0.15; Scotland/NI Table 19 thicknesses; Scotland/ Wales Table 20 L/M overrides. Logged for a future country pass. Corpus unchanged (band-M rafter roofs essentially absent from the 1000-cert England corpus). pyright not installed in this container. Co-Authored-By: Claude Opus 4.8 (1M context) --- domain/sap10_ml/rdsap_uvalues.py | 21 +++++++++++---------- domain/sap10_ml/tests/test_rdsap_uvalues.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/domain/sap10_ml/rdsap_uvalues.py b/domain/sap10_ml/rdsap_uvalues.py index 725c7726..6b1e35fc 100644 --- a/domain/sap10_ml/rdsap_uvalues.py +++ b/domain/sap10_ml/rdsap_uvalues.py @@ -839,20 +839,21 @@ _ROOF_RAFTERS_BY_THICKNESS: Final[list[tuple[int, float]]] = [ (400, 0.14), ] -# Table 18 rafters column: pitched-roof "insulation at rafters" default U -# by age band when the thickness cannot be determined. RdSAP 10 §5.11 -# Table 18 (PDF p.45). Identical to the joist column (1) for bands A-G +# Table 18 column (2) "Pitched, insulation at rafters": pitched-roof default +# U by age band when the thickness cannot be determined. RdSAP 10 §5.11 +# Table 18 (PDF p.46). Identical to the joist column (1) for bands A-G # (2.30 → 0.40), then diverges higher (H 0.35 vs 0.30, I 0.35 vs 0.26, -# J/K 0.20 vs 0.16, L 0.18 vs 0.16). Unlike the loft-joist default this -# does NOT collapse to the optimistic 0.40 "assume modern retrofit" floor -# at old bands — a rafter cavity cannot be topped up from the loft, so an -# unknown-thickness rafter roof keeps the as-built age-band U (band F -# 0.68, band E 1.50, A-D 2.30). Worksheet-validated by simulated case 41 -# Ext3 (band F, R Rafters, As Built → P960 §3 (30) U=0.68). +# J/K 0.20 vs 0.16, L 0.18 vs 0.16) before converging to 0.15 at band M. +# Unlike the loft-joist default this does NOT collapse to the optimistic +# 0.40 "assume modern retrofit" floor at old bands — a rafter cavity cannot +# be topped up from the loft, so an unknown-thickness rafter roof keeps the +# as-built age-band U (band F 0.68, band E 1.50, A-D 2.30). Worksheet- +# validated by simulated case 41 Ext3 (band F, R Rafters, As Built → P960 +# §3 (30) U=0.68). _ROOF_RAFTERS_BY_AGE: Final[dict[str, float]] = { "A": 2.30, "B": 2.30, "C": 2.30, "D": 2.30, "E": 1.50, "F": 0.68, "G": 0.40, "H": 0.35, "I": 0.35, "J": 0.20, - "K": 0.20, "L": 0.18, "M": 0.18, + "K": 0.20, "L": 0.18, "M": 0.15, } # Table 18 column (3): flat-roof default U by age band when thickness unknown. diff --git a/domain/sap10_ml/tests/test_rdsap_uvalues.py b/domain/sap10_ml/tests/test_rdsap_uvalues.py index 8d670a2f..77de58eb 100644 --- a/domain/sap10_ml/tests/test_rdsap_uvalues.py +++ b/domain/sap10_ml/tests/test_rdsap_uvalues.py @@ -1265,6 +1265,25 @@ def test_u_roof_at_rafters_unknown_thickness_uses_table18_rafters_age_band() -> assert abs(band_c - 2.30) <= 0.001 +def test_u_roof_at_rafters_unknown_thickness_age_m_returns_0_15_per_table18() -> None: + # Arrange — RdSAP 10 Table 18 column (2) "Pitched, insulation at + # rafters" (PDF p.46): band M = 0.15 (footnote (1) only, no country + # variation — the whole M row converges to 0.15). The rafters column + # diverges above the joist column at H-L (0.35/0.35/0.20/0.20/0.18) + # but rejoins it at M = 0.15; the table previously carried 0.18 here. + + # Act + band_m = u_roof( + country=Country.ENG, + age_band="M", + insulation_thickness_mm=None, + insulation_at_rafters=True, + ) + + # Assert + assert abs(band_m - 0.15) <= 0.001 + + def test_u_roof_unknown_age_band_falls_back_to_mid_range() -> None: # Arrange — nothing known.