fix(uvalues): correct rafter-roof age-M default U 0.18->0.15 (RdSAP 10 Table 18 col 2, PDF p.46)

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) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-20 13:57:29 +00:00
parent 104e3725b8
commit b22b27c0ff
2 changed files with 30 additions and 10 deletions

View file

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

View file

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