Audit: pin u_roof description cascade against RdSAP10 Table 16 for golden cert cohort

Mirror of the wall cohort pin. Worksheet fixtures lodge roofs=[] so the description-driven branch of u_roof was never validated at cascade level. New parametrised test pins 8 (description, age, thickness) tuples from the golden certs against the Table 16 col-1 (loft insulation thickness known) value. All 8 cases match spec: u_roof is correct on the thickness-known path even when joined-description from multiple roof rows contains noise.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-24 17:16:17 +00:00
parent 15789f5acf
commit acc6331dc3

View file

@ -1230,3 +1230,56 @@ def test_u_wall_matches_table6_for_every_cohort_description_age_pair(
# Assert
assert abs(u - expected_u) < 1e-4
# ----- Description-cascade cohort pins (Roofs) -----
#
# Mirror of the wall cohort pins above. The Elmhurst worksheet fixtures
# lodge `roofs=[]`, so the cascade-pin tests do not exercise u_roof's
# description path either. The 8 golden API certs lodge `roofs[].
# description` strings — these tests pin each (description, age,
# thickness) tuple seen in that cohort against the RdSAP10 Table 16
# (loft-insulation-thickness-known) value the spec mandates.
#
# Excluded: ambiguous joined-description cases where one bp lodges no
# thickness and another lodges a value — the calc routes through Table
# 18 defaults whose interaction with the description cascade needs
# separate pinning. "(another dwelling above)" is also excluded — its
# u_roof value is ignored by heat_transmission once roof_area is zeroed.
_TABLE_16_ENG_ROOF_COHORT_PINS: tuple[tuple[str, str, int, float], ...] = (
# (joined_description, age_band, thickness_mm, expected_u_w_per_m2k)
# Table 16 col 1 — thickness-known path, U independent of age band.
("Pitched, 100 mm loft insulation", "D", 100, 0.40), # cert 7536 bp0
("Pitched, 100 mm loft insulation | Pitched, insulated (assumed)", "D", 100, 0.40), # cert 7536 bp0 (joined)
("Pitched, 270 mm loft insulation", "D", 270, 0.16), # cert 0300 bp0
("Pitched, 300 mm loft insulation | Flat, no insulation", "D", 300, 0.14), # cert 0390-2954 bp0
("Pitched, 300 mm loft insulation | Roof room(s), limited insulation (assumed)", "A", 300, 0.14), # cert 6035 bp0
("Pitched, 300 mm loft insulation | Flat, insulated", "C", 300, 0.14), # cert 8135 bp0
("Pitched, 300 mm loft insulation | Pitched, 100 mm loft insulation", "B", 300, 0.14), # cert 2130 bp0
("Pitched, 400+ mm loft insulation | Pitched, insulated (assumed)", "J", 400, 0.11), # cert 0240 bp0
)
@pytest.mark.parametrize(
"description, age_band, thickness_mm, expected_u",
_TABLE_16_ENG_ROOF_COHORT_PINS,
)
def test_u_roof_matches_table16_for_every_cohort_description_thickness_pair(
description: str,
age_band: str,
thickness_mm: int,
expected_u: float,
) -> None:
# Arrange — inputs replicate what `heat_transmission_from_cert` feeds
# `u_roof` for the main building part in the golden cert cohort.
# Act
u = u_roof(
country=Country.ENG,
age_band=age_band,
insulation_thickness_mm=thickness_mm,
description=description,
)
# Assert
assert abs(u - expected_u) < 1e-4