mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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:
parent
58ff7d8881
commit
c75ef6417f
3 changed files with 103 additions and 5 deletions
|
|
@ -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/m².
|
||||
"""
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue