mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(roof-U): flat roof, undetermined thickness, "no/limited insulation" → Table 18 age default
A flat roof lodged "Flat, no insulation" / "Flat, limited insulation" with
an undetermined insulation thickness ('ND'/'AB' → parsed None) was given the
Table 16 row-0/12mm U (2.30 / 1.50) from the description marker, regardless
of age band. Per RdSAP 10 §5.11.4 (PDF p.44) "U-values in Table 18 are used
when thickness of insulation cannot be determined" — the column (3)
flat-roof age-band default applies. The "no/limited insulation" text is
RdSAP's as-built rendering: at old bands (A-D) the column (3) default IS
2.30 (so those certs are unchanged), but a newer-band flat roof carries the
age-band insulation as built (band H = 0.35, F = 0.68, not 2.30).
Confirmed by the description-vs-rating audit: cert 0390-2753 (top-floor
flat, band H, "Flat, no insulation", thickness 'ND') lodges roof
energy_efficiency_rating = 3 (moderate U), NOT the rating-1 that 2.30
implies — and drove a -31.78 SAP error (roof 202 W/K over 88 m²). Same
masked-at-old-bands structure as the cavity-U fix: accurate at A-D where the
default coincides with 2.30, catastrophic only where it diverges.
Pitched roofs are deliberately NOT rerouted (their "no insulation" text is
load-bearing — the broad 'ND'→Table-18 reroute was empirically net-negative
for pitched lofts).
API SAP eval: 52.1% -> 53.1% within 0.5; <1.0 67.2% -> 68.0%; median |err|
0.475 -> 0.467; mean|err| 1.497 -> 1.424; flat-roof bucket within-0.5
23% -> 35% (11 improved, 2 regressed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
898dcfda18
commit
58cff932e6
2 changed files with 63 additions and 2 deletions
|
|
@ -857,9 +857,30 @@ def u_roof(
|
|||
return u
|
||||
if description is not None:
|
||||
desc = description.lower()
|
||||
if any(marker in desc for marker in _ROOF_NO_INSULATION_MARKERS):
|
||||
no_insulation = any(marker in desc for marker in _ROOF_NO_INSULATION_MARKERS)
|
||||
limited_insulation = any(
|
||||
marker in desc for marker in _ROOF_LIMITED_INSULATION_MARKERS
|
||||
)
|
||||
if (no_insulation or limited_insulation) and is_flat_roof and age_band is not None:
|
||||
# FLAT roof reached here only when insulation_thickness_mm is None
|
||||
# — the lodged thickness is undetermined ('ND'/'AB'/absent). Per
|
||||
# RdSAP 10 §5.11.4 (PDF p.44) "U-values in Table 18 are used when
|
||||
# thickness of insulation cannot be determined", so the column (3)
|
||||
# flat-roof age-band default applies — NOT the uninsulated 2.30.
|
||||
# The "no/limited insulation" text is RdSAP's as-built rendering:
|
||||
# at old bands (A-D) the column (3) default IS 2.30 (so those certs
|
||||
# are unchanged), but a newer-band flat roof carries the age-band
|
||||
# insulation as built (band H = 0.35, F = 0.68, not 2.30). The
|
||||
# roof's deterministic energy_efficiency_rating confirms it (e.g.
|
||||
# cert 0390-2753 band H lodges rating 3 = moderate U, not the
|
||||
# rating-1 that 2.30 implies). PITCHED roofs are deliberately NOT
|
||||
# routed here — their "no insulation" text is load-bearing (the
|
||||
# broad 'ND'→Table-18 reroute was empirically net-negative for
|
||||
# pitched lofts).
|
||||
return _FLAT_ROOF_BY_AGE.get(age_band.upper(), 0.4)
|
||||
if no_insulation:
|
||||
return _ROOF_BY_THICKNESS[0][1] # 2.30 W/m^2K
|
||||
if any(marker in desc for marker in _ROOF_LIMITED_INSULATION_MARKERS):
|
||||
if limited_insulation:
|
||||
return _ROOF_BY_THICKNESS[1][1] # 1.50 W/m^2K (12mm row)
|
||||
if age_band is None:
|
||||
return 0.4
|
||||
|
|
|
|||
|
|
@ -878,6 +878,46 @@ def test_u_roof_unknown_flat_insulation_uses_table18_flat_column() -> None:
|
|||
assert abs(result - 0.35) <= 0.01
|
||||
|
||||
|
||||
def test_u_roof_flat_no_insulation_undetermined_thickness_uses_table18_by_age() -> None:
|
||||
# Arrange — a flat roof lodged "Flat, no insulation" / "Flat, limited
|
||||
# insulation" with an UNDETERMINED thickness (parsed to None from
|
||||
# 'ND'/'AB') must take the Table 18 column (3) flat-roof age-band
|
||||
# default per RdSAP 10 §5.11.4 (PDF p.44), NOT the uninsulated 2.30.
|
||||
# The "no/limited insulation" text is RdSAP's as-built rendering — at
|
||||
# old bands the column (3) default IS 2.30 (so they're unchanged), but
|
||||
# a newer-band flat roof carries the age-band insulation as built.
|
||||
# Cert 0390-2753 (top-floor flat, band H, "Flat, no insulation",
|
||||
# thickness 'ND', roof rating 3 = moderate) drove a -31.78 SAP error at
|
||||
# the 2.30 value; band H column (3) = 0.35.
|
||||
|
||||
# Act — band H "no insulation" → 0.35; band F "limited insulation" → 0.68;
|
||||
# band C "no insulation" → unchanged 2.30 (column (3) default at C).
|
||||
band_h = u_roof(
|
||||
country=Country.ENG, age_band="H", insulation_thickness_mm=None,
|
||||
description="Flat, no insulation", is_flat_roof=True,
|
||||
)
|
||||
band_f = u_roof(
|
||||
country=Country.ENG, age_band="F", insulation_thickness_mm=None,
|
||||
description="Flat, limited insulation", is_flat_roof=True,
|
||||
)
|
||||
band_c = u_roof(
|
||||
country=Country.ENG, age_band="C", insulation_thickness_mm=None,
|
||||
description="Flat, no insulation", is_flat_roof=True,
|
||||
)
|
||||
# A PITCHED roof "no insulation" with undetermined thickness is NOT
|
||||
# rerouted — its text is load-bearing (2.30 stays).
|
||||
pitched = u_roof(
|
||||
country=Country.ENG, age_band="H", insulation_thickness_mm=None,
|
||||
description="Pitched, no insulation", is_flat_roof=False,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert abs(band_h - 0.35) <= 0.01
|
||||
assert abs(band_f - 0.68) <= 0.01
|
||||
assert abs(band_c - 2.30) <= 0.01
|
||||
assert abs(pitched - 2.30) <= 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.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue