mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
slice S-B28: §5.11.4 — roof "NI" + insulated description → 50 mm joist row
346 corpus certs lodge roof_insulation_thickness="NI" (Not Indicated,
parsed to 0 by _parse_thickness_mm). When the description also signals
retrofit insulation ("Pitched, insulated (assumed)" / "Flat,
insulated" / "Roof room(s), insulated (assumed)"), our cascade
returned the uninsulated Table 16 row-0 value (U=2.30).
RdSAP 10 §5.11.4 (page 44, end of section): "If retrofit insulation
present of unknown thickness use 50 mm". That maps to Table 16 row
"Insulation at joists at ceiling level, 50 mm" = 0.68 W/m²K. The fix
is the analog of S-B27 for roofs: when insulation_thickness_mm==0
(the "NI" sentinel) and _described_as_insulated(description), return
0.68 instead of the row-0 lookup.
Per-cert delta: ΔU = 1.62 W/m²K on the affected slice; for typical
80 m² roof = 130 W/K HLC reduction ≈ 12 kWh/m² PEUI per cert.
Parity probe at 300 certs, seed=7:
SAP MAE 4.72 → 4.69 (-0.03) ← first SAP MAE drop in 3 slices
PE MAE 44.19 → 43.32 (-0.87)
PE bias 38.56 → 37.69 (-0.87)
Cumulative across S-B23 → S-B28:
PE MAE 57.28 → 43.32 (-13.96)
PE bias 51.56 → 37.69 (-13.87)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1f49fa03cd
commit
25261d5c8b
3 changed files with 96 additions and 0 deletions
|
|
@ -417,6 +417,14 @@ def u_roof(
|
|||
as uninsulated.
|
||||
3. Table 18 age-band default.
|
||||
"""
|
||||
if insulation_thickness_mm == 0 and _described_as_insulated(description):
|
||||
# Spec §5.11.4 (page 44 footnote): "If retrofit insulation
|
||||
# present of unknown thickness use 50 mm". The cert encodes
|
||||
# "thickness unknown but retrofit insulation present" as the
|
||||
# "NI" sentinel which `_parse_thickness_mm` parses to 0. Without
|
||||
# this override the Table 16 row-0 lookup below returns the
|
||||
# uninsulated 2.30 W/m²K.
|
||||
return 0.68 # Table 16 row 50, "Insulation at joists at ceiling level"
|
||||
if insulation_thickness_mm is not None:
|
||||
# nearest tabulated thickness <= supplied
|
||||
u = _ROOF_BY_THICKNESS[0][1]
|
||||
|
|
|
|||
|
|
@ -466,6 +466,49 @@ def test_u_wall_uses_rdsap_unknown_thickness_default_of_50mm_when_insulated_but_
|
|||
# ----- Roofs -----
|
||||
|
||||
|
||||
def test_u_roof_ni_thickness_with_insulated_description_applies_50mm_per_section_5_11_4() -> None:
|
||||
# Arrange — 346 corpus certs lodge roof_insulation_thickness="NI"
|
||||
# (Not Indicated, parsed to 0 by _parse_thickness_mm). When the
|
||||
# description also signals retrofit insulation ("Pitched, insulated
|
||||
# (assumed)" / "Flat, insulated" / "Roof room(s), insulated
|
||||
# (assumed)"), RdSAP 10 §5.11.4 (page 44) footnote applies:
|
||||
# "If retrofit insulation present of unknown thickness use 50 mm".
|
||||
# That maps to Table 16 row "50 mm at joists at ceiling level" = 0.68
|
||||
# W/m²K — vs the current 2.30 we return when thickness=0 hits the
|
||||
# Table 16 row-0 lookup.
|
||||
|
||||
# Act
|
||||
result = u_roof(
|
||||
country=Country.ENG,
|
||||
age_band="C",
|
||||
insulation_thickness_mm=0, # parsed from "NI"
|
||||
description="Pitched, insulated (assumed)",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(0.68, abs=0.01)
|
||||
|
||||
|
||||
def test_u_roof_ni_thickness_with_no_insulation_description_stays_at_2_30() -> None:
|
||||
# Arrange — 706 corpus certs lodge "Pitched, no insulation
|
||||
# (assumed)" which can co-occur with thickness="NI". The
|
||||
# description-based override for retrofit-insulated roofs must
|
||||
# respect the "no insulation" negation: `_described_as_insulated`
|
||||
# returns False on "no insulation" substring, so the Table 16
|
||||
# row-0 lookup applies and U = 2.30 W/m²K stays.
|
||||
|
||||
# Act
|
||||
result = u_roof(
|
||||
country=Country.ENG,
|
||||
age_band="C",
|
||||
insulation_thickness_mm=0,
|
||||
description="Pitched, no insulation (assumed)",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == pytest.approx(2.30, abs=0.01)
|
||||
|
||||
|
||||
def test_u_roof_age_band_j_pitched_returns_table18_value() -> None:
|
||||
# Arrange — Table 18, pitched insulation between joists, age J -> 0.16 W/m^2K.
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,51 @@ from domain.sap.worksheet.heat_transmission import (
|
|||
)
|
||||
|
||||
|
||||
def test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_section_5_11_4() -> None:
|
||||
# Arrange — 346 corpus certs lodge roof_insulation_thickness="NI"
|
||||
# with descriptions like "Pitched, insulated (assumed)". The
|
||||
# retrofit-insulation signal in the description must flow from
|
||||
# epc.roofs[0].description through heat_transmission_from_cert
|
||||
# into u_roof so the 50 mm assumption fires per RdSAP 10 §5.11.4.
|
||||
# Geometry: 100 m² ground floor → top floor area also 100 m² →
|
||||
# roof_area = 100 m². roof_w_per_k expected = 0.68 × 100 = 68
|
||||
# W/K (vs uninsulated 2.30 × 100 = 230 W/K).
|
||||
main = make_building_part(
|
||||
identifier="Main Dwelling",
|
||||
construction_age_band="C",
|
||||
wall_construction=3,
|
||||
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,
|
||||
),
|
||||
],
|
||||
)
|
||||
# roof_insulation_thickness "NI" mimics the cert field — parses to 0.
|
||||
main.roof_insulation_thickness = "NI"
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=100.0,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[main],
|
||||
)
|
||||
epc.roofs = [
|
||||
EnergyElement(
|
||||
description="Pitched, insulated (assumed)",
|
||||
energy_efficiency_rating=4,
|
||||
environmental_efficiency_rating=4,
|
||||
),
|
||||
]
|
||||
|
||||
# Act
|
||||
result = heat_transmission_from_cert(epc)
|
||||
|
||||
# Assert
|
||||
assert result.roof_w_per_k == pytest.approx(68.0, abs=2.0)
|
||||
|
||||
|
||||
def test_floor_insulated_assumed_with_ni_thickness_uses_50mm_per_table19_footnote() -> None:
|
||||
# Arrange — 2 413 corpus certs lodge floors with thickness="NI" and
|
||||
# description "Solid, insulated (assumed)". The retrofit-insulation
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue