Audit: pin u_wall description cascade against RdSAP10 Table 6 (England) for golden cert cohort

Worksheet fixtures lodge walls=[] so the description-driven branches of u_wall — the codepath real API certs trigger — were never validated at cascade level. New parametrised test pins each (description, age) pair seen in the 8 golden certs against the Table 6 value the spec mandates. All 7 clean cases match spec: the description cascade is correct where Table 6 gives a direct value. Cases routing through §5.7 / §5.8 formulas are excluded pending separate pinning.

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

View file

@ -16,6 +16,8 @@ defaults -> country defaults -> final mid-range value so that callers can
treat the envelope as if RdSAP had assigned an as-built default.
"""
from typing import Optional
import pytest
from domain.ml.rdsap_uvalues import (
@ -1166,3 +1168,65 @@ def test_u_rr_stud_wall_thickness_125mm_takes_nearest_tabulated_row_below() -> N
# Assert
assert result == pytest.approx(0.31, abs=0.001)
# ----- Description-cascade cohort pins (Walls) -----
#
# The Elmhurst worksheet fixtures lodge `walls=[]` and so cannot exercise
# the description-driven branches of `u_wall`. The 8 golden API certs DO
# carry `walls[0].description` strings that route through `_described_as_
# insulated`, `_wall_type_from_description`, and the Table 6 footnote
# 50 mm-bucket override. These tests pin every (description, age) pair
# seen in that cohort against the RdSAP10 Table 6 (England) value the
# spec mandates — closing the cascade-coverage gap identified during the
# 2026-05-24 audit (description cascade was 100% spec-correct on clean
# Table 6 rows; this test locks that in for regression).
#
# Cases routing through §5.7 (solid brick from wall thickness) or §5.8
# (stone/brick with insulation, ages AD — formula not table) are
# intentionally excluded — they need separate pinning when those
# formulas land.
_TABLE_6_ENG_WALL_COHORT_PINS: tuple[tuple[str, str, Optional[int], Optional[int], Optional[int], bool, float], ...] = (
# (description, age_band, wall_construction, wall_insulation_type,
# insulation_thickness_mm, insulation_present, expected_u_w_per_m2k)
# `insulation_present` mirrors the heat_transmission cascade: type != 4 (NONE)
# OR description asserts insulation per _described_as_insulated.
("Sandstone, as built, insulated (assumed)", "J", 2, 4, 0, True, 0.25), # cert 0240 (50 mm bucket per footnote)
("Cavity wall, filled cavity", "C", 4, 2, 0, True, 0.7), # cert 8135 bp0
("Cavity wall, filled cavity", "D", 4, 2, 0, True, 0.7), # cert 0300, 7536 bp0
("Cavity wall, filled cavity", "F", 4, 2, 0, True, 0.40), # cert 7536 bp2
("Cavity wall, filled cavity", "G", 4, 2, 0, True, 0.35), # cert 8135 bp1
("Cavity wall, filled cavity", "L", 4, 4, 0, False, 0.28), # cert 7536 bp1 (assumed-as-built †)
("Cavity wall, as built, no insulation (assumed)", "D", 4, 4, 0, False, 1.5), # cert 0390-2954, 9390
)
@pytest.mark.parametrize(
"description, age_band, construction, insulation_type, thickness_mm, insulation_present, expected_u",
_TABLE_6_ENG_WALL_COHORT_PINS,
)
def test_u_wall_matches_table6_for_every_cohort_description_age_pair(
description: str,
age_band: str,
construction: Optional[int],
insulation_type: Optional[int],
thickness_mm: Optional[int],
insulation_present: bool,
expected_u: float,
) -> None:
# Arrange — inputs replicate what `heat_transmission_from_cert` feeds
# `u_wall` for the corresponding building part in the golden cert cohort.
# Act
u = u_wall(
country=Country.ENG,
age_band=age_band,
construction=construction,
insulation_thickness_mm=thickness_mm,
insulation_present=insulation_present,
description=description,
wall_insulation_type=insulation_type,
)
# Assert
assert abs(u - expected_u) < 1e-4