S0380.210: cert 0390 cavity "partial insulation (assumed)" → as-built row, not filled

Golden cert 0390-2954-3640 (detached, TFA 360, age F) carried a +7 SAP /
-28 kWh/m² PE residual the audit attributed to a demand-side fabric gap.
Walking the §3 cascade localised it to the Main wall: lodged
wall_construction=4 (cavity), wall_insulation_type=4 (as-built / assumed),
description "Cavity wall, as built, partial insulation (assumed)". The
cascade mis-routed it to the Table 6 "Filled cavity" row (band F = 0.40)
because `_described_as_insulated` matches the "partial insulation"
substring.

RdSAP 10 Specification (10-06-2025) Table 6 — Wall U-values, England
distinguishes two cavity rows:
  "Cavity as built"  A-E 1.5, F 1.0, G 0.60, H 0.60, I 0.45, J 0.35, ...
  "Filled cavity"    A-E 0.7, F 0.40, G 0.35, H 0.35, I 0.45†, J 0.35†, ...
An "as built ... partial insulation (assumed)" cavity is the as-built
partial fill of the age band, NOT a retrofit cavity fill (a genuine fill
lodges the distinct "Cavity wall, filled cavity", wall_insulation_type=2).
It therefore routes to "Cavity as built" (band F = 1.0), mirroring the
worksheet-validated solid-brick rule in S0380.209 (cases 9/10: "as built,
insulated (assumed)" → as-built age-band row, not retrofit).

New `_cavity_described_as_filled` predicate is used only in u_wall's
cavity filled-row branch; it excludes the "partial insulation" substring
while keeping "insulated (assumed)" → filled (the unrelated, separately
asserted test_cavity_as_built_insulated_assumed_uses_filled_cavity_row is
unchanged). The shared `_described_as_insulated` (also consumed by the
roof/floor paths) is left untouched.

Wall HLC +53.6 W/K (U 0.40 → 1.0 over ~268 m²) lifts all four metrics
together — the signature of a real fabric bug, not a tuned offset:
  SAP  +7      → +0
  PE   -27.9745 → +0.5281 kWh/m²
  CO2  -2.7134  → -0.1189 t/yr
Bands I-M are unaffected (the two rows coincide per the † footnote), so
golden certs 0535 (band M) / 7536 (band L) with "insulated (assumed)"
cavities continue to pin at 0. Full suite 2384 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 21:57:00 +00:00
parent 58ff7d8881
commit c75ef6417f
3 changed files with 103 additions and 5 deletions

View file

@ -66,6 +66,41 @@ def _described_as_insulated(description: Optional[str]) -> bool:
return "insulated" in desc or "partial insulation" in desc
def _cavity_described_as_filled(description: Optional[str]) -> bool:
"""True when an as-built cavity wall's description asserts the cavity is
insulated/filled, routing it to the Table 6 "Filled cavity" row.
Distinguishes the three as-built cavity states the EPC renders by age
band when wall_insulation_type=4 ("as-built / assumed"):
- "...insulated (assumed)" Filled cavity (assessor judges
the cavity filled but lodges no
thickness)
- "...partial insulation (assumed)" "Cavity as built" row (the
as-built partial fill of the age
band, NOT a retrofit cavity fill)
- "...no insulation (assumed)" "Cavity as built" row
Narrower than `_described_as_insulated`: it excludes the "partial
insulation" substring so a "partial insulation (assumed)" cavity stays on
the as-built row. RdSAP 10 Table 6 (England) "Cavity as built" band F =
1.0 vs "Filled cavity" band F = 0.40 for an as-built band-F cavity the
filled row understates heat loss by 2.5x. A genuine retrofit fill is
lodged distinctly as "Cavity wall, filled cavity"
(wall_insulation_type=2), handled by the explicit-code branch.
Real-cert evidence: golden cert 0390-2954-3640 (detached, band F, cavity
type 4, "partial insulation (assumed)") closes all four SAP metrics on
the as-built 1.0 row; the filled 0.40 row under-counts PE by ~28 kWh/.
"""
if description is None:
return False
desc = description.lower()
if "no insulation" in desc:
return False
return "insulated" in desc
# ---------------------------------------------------------------------------
# Country
# ---------------------------------------------------------------------------
@ -597,7 +632,7 @@ def u_wall(
)
if wall_type == WALL_CAVITY and (
wall_insulation_type == WALL_INSULATION_FILLED_CAVITY
or _described_as_insulated(description)
or _cavity_described_as_filled(description)
):
return _CAVITY_FILLED_ENG[age_idx]
bucket = _insulation_bucket(insulation_thickness_mm, insulation_present)

View file

@ -236,9 +236,9 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
_GoldenExpectation(
cert_number="0390-2954-3640-2196-4175",
actual_sap=60,
expected_sap_resid=+7,
expected_pe_resid_kwh_per_m2=-27.9745,
expected_co2_resid_tonnes_per_yr=-2.7134,
expected_sap_resid=+0,
expected_pe_resid_kwh_per_m2=+0.5281,
expected_co2_resid_tonnes_per_yr=-0.1189,
notes=(
"Detached, TFA 360, age F, Firebird oil combi PCDF 9005 "
"(winter eff 86.4%). PCDB record lodges separate_dhw_tests=0 + "
@ -269,7 +269,18 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
"Slice S0380.151 wired RdSAP 10 §4.1 Table 5 (PDF p.28) "
"extract-fans default (age F → 1 fan). Cascade ventilation "
"HLC rises ~0.03 ACH × volume; PE -28.0830 → -27.9745 "
"(closer to zero), CO2 -2.7342 → -2.7134."
"(closer to zero), CO2 -2.7342 → -2.7134. "
"Slice S0380.210 CLOSED the residual: the Main cavity wall lodges "
"wall_insulation_type=4 (as-built/assumed) + description "
"'Cavity wall, as built, partial insulation (assumed)'. The "
"cascade mis-routed it to the Table 6 'Filled cavity' row "
"(band F = 0.40) via the 'partial insulation' substring; "
"RdSAP 10 Table 6 (England) routes an as-built partial-fill "
"cavity to the 'Cavity as built' row (band F = 1.0). New "
"`_cavity_described_as_filled` excludes 'partial insulation' "
"(keeping 'insulated (assumed)' → filled). Wall HLC +53.6 W/K "
"(0.40 → 1.0 over 268 m²) lifted all four metrics together: "
"SAP +7 → +0, PE -27.9745 → +0.5281, CO2 -2.7134 → -0.1189."
),
),
_GoldenExpectation(

View file

@ -307,6 +307,58 @@ def test_cavity_as_built_insulated_assumed_uses_filled_cavity_row() -> None:
assert result.walls_w_per_k == pytest.approx(70.0, abs=1.0)
def test_cavity_as_built_partial_insulation_assumed_uses_as_built_row() -> None:
# Arrange — the EPC renders a cavity wall lodged wall_insulation_type=4
# (as-built / assumed) with description "Cavity wall, as built, partial
# insulation (assumed)" for age bands where the as-built construction
# carries only partial cavity fill. "Partial insulation" is the as-built
# thermal state of the age band, NOT a retrofit cavity fill — the spec
# routes it to the "Cavity as built" row, not "Filled cavity":
# RdSAP 10 Table 6 (England) "Cavity as built" band F = 1.0 vs
# "Filled cavity" band F = 0.40. A genuine fill renders the distinct
# "Cavity wall, filled cavity" description (wall_insulation_type=2),
# caught separately. Contrast the "insulated (assumed)" variant above,
# which the assessor judges as filled.
#
# Real-cert evidence: golden cert 0390-2954-3640 (detached, band F,
# cavity type 4, "partial insulation (assumed)") closes all four SAP
# metrics (PE/SAP/CO2/cost) on the as-built 1.0 row — at the filled
# 0.40 row its PE under-counts by ~28 kWh/m².
# Geometry: 100 m² floor, 40 m perimeter, 2.5 m height, single storey
# → gross_wall = 100 m². walls_w_per_k expected = 1.0 × 100 = 100 W/K.
main = make_building_part(
construction_age_band="F",
wall_construction=4,
wall_insulation_type=4,
party_wall_construction=1,
roof_construction=4,
floor_dimensions=[
make_floor_dimension(
total_floor_area_m2=100.0, room_height_m=2.5,
party_wall_length_m=0.0, heat_loss_perimeter_m=40.0, floor=0,
),
],
)
epc = make_minimal_sap10_epc(
total_floor_area_m2=100.0,
country_code="ENG",
sap_building_parts=[main],
)
epc.walls = [
EnergyElement(
description="Cavity wall, as built, partial insulation (assumed)",
energy_efficiency_rating=3,
environmental_efficiency_rating=3,
),
]
# Act
result = heat_transmission_from_cert(epc)
# Assert — U=1.0 × 100 m² gross wall = 100 W/K (as-built, not filled 70).
assert abs(result.walls_w_per_k - 100.0) <= 1.0
def test_walls_description_measured_transmittance_overrides_construction_cascade() -> None:
# Arrange — a full-SAP (not RdSAP) cert lodges the wall U-value
# directly in walls[i].description ("Average thermal transmittance