mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(wall-U): apply RdSAP Table 4 "Sheltered" R=0.5 to alternative walls
Audit of the API-SAP error (53% within 0.5) localised the systematic under-rate to ELECTRIC FLATS (houses sit at 60-66% within 0.5; electric flats 13-19%). Decomposing the flat error showed it tracks space-heating demand per m² — the worst certs reach 130-289 kWh/m² (accurate certs sit at 14-110), i.e. a grossly over-stated fabric heat loss, amplified ~4x by the electricity unit price and the steep low-band SAP log curve. Root cause: the gov-EPC API lodges `sheltered_wall="Y"` on alternative wall sub-areas (a sub-area adjacent to an unheated buffer — stair core, adjoining structure), but the field was dropped by the schema + domain dataclasses and the calculator billed the alt sub-area at its full exposed U. RdSAP 10 Table 4 (PDF p.22) "Sheltered": such a wall carries an added external surface resistance R=0.5 m²K/W → U_sheltered = 1/(1/U + 0.5) — the SAME adjustment the main wall already applies for `gable_wall_type=2` (`gable_wall_sheltered`, `_SHELTERED_GABLE_ADDED_RESISTANCE_M2K_W`). Cert 0340-2976 (band-A flat, 42 m² sheltered timber-frame alt) over-stated its wall channel by ~58 W/K → walls 128 -> 70 W/K. Threads the field end-to-end: schema dataclasses (21.0.0/21.0.1) + domain `SapAlternativeWall.is_sheltered` (default False — the Summary/ Elmhurst path leaves it False, sheltering rides through its lodged U-value there, so goldens are untouched) + `from_api_response` mapping `"Y"->True` + `_alt_wall_w_per_k` applying the 0.5 resistance on the cascade path (lodged-U and basement alt-walls return before it). 140 certs (15% of the corpus) carry a sheltered alt-wall; they under- rated at median -0.82 / mean signed -1.33 / 23% within 0.5. Eval: 102 improved, 38 regressed (offsetting-error cases — fix is spec-uniform per [[feedback_software_no_special_handling]]); within-0.5 53.14% -> 54.24%, within-1.0 67.99% -> 69.64%, within-2.0 81.85% -> 83.50%, mean|err| 1.312 -> 1.248, 909 computed / 0 raises. Goldens (6035, 000565) and full calc/epc/parser regression green; pyright net-zero. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e1adc8d3d5
commit
3e05c95e65
6 changed files with 61 additions and 0 deletions
|
|
@ -440,6 +440,14 @@ class SapAlternativeWall:
|
|||
# Mirrors `SapBuildingPart.wall_thickness_mm` per the
|
||||
# [[feedback-no-misleading-insulation-type]] convention.
|
||||
wall_thickness_mm: Optional[int] = None
|
||||
# RdSAP 10 Table 4 (p.22) "Sheltered" wall: a sub-area adjacent to
|
||||
# an unheated buffer space (stair core, adjoining structure) carries
|
||||
# an added external surface resistance of R=0.5 m²K/W, so its U is
|
||||
# reduced to U_sheltered = 1/(1/U + 0.5) — the same adjustment the
|
||||
# main wall applies for `gable_wall_type=2`. The gov-EPC API lodges
|
||||
# this per alt-wall as `sheltered_wall="Y"`; the Summary/Elmhurst path
|
||||
# leaves it False (sheltering rides through the lodged U-value there).
|
||||
is_sheltered: bool = False
|
||||
# Explicit basement determination. RdSAP10 `wall_construction == 6` is
|
||||
# canonically SYSTEM-BUILT (`WALL_SYSTEM_BUILT`) — the basement
|
||||
# heuristic hijacked it because Elmhurst lodges both "SY System build"
|
||||
|
|
|
|||
|
|
@ -1469,6 +1469,7 @@ class EpcPropertyDataMapper:
|
|||
wall_insulation_type=bp.sap_alternative_wall_1.wall_insulation_type,
|
||||
wall_thickness_measured=bp.sap_alternative_wall_1.wall_thickness_measured,
|
||||
wall_insulation_thickness=bp.sap_alternative_wall_1.wall_insulation_thickness,
|
||||
is_sheltered=bp.sap_alternative_wall_1.sheltered_wall == "Y",
|
||||
)
|
||||
if bp.sap_alternative_wall_1
|
||||
else None
|
||||
|
|
@ -1481,6 +1482,7 @@ class EpcPropertyDataMapper:
|
|||
wall_insulation_type=bp.sap_alternative_wall_2.wall_insulation_type,
|
||||
wall_thickness_measured=bp.sap_alternative_wall_2.wall_thickness_measured,
|
||||
wall_insulation_thickness=bp.sap_alternative_wall_2.wall_insulation_thickness,
|
||||
is_sheltered=bp.sap_alternative_wall_2.sheltered_wall == "Y",
|
||||
)
|
||||
if bp.sap_alternative_wall_2
|
||||
else None
|
||||
|
|
@ -1745,6 +1747,7 @@ class EpcPropertyDataMapper:
|
|||
wall_insulation_type=bp.sap_alternative_wall_1.wall_insulation_type,
|
||||
wall_thickness_measured=bp.sap_alternative_wall_1.wall_thickness_measured,
|
||||
wall_insulation_thickness=bp.sap_alternative_wall_1.wall_insulation_thickness,
|
||||
is_sheltered=bp.sap_alternative_wall_1.sheltered_wall == "Y",
|
||||
)
|
||||
if bp.sap_alternative_wall_1
|
||||
else None
|
||||
|
|
@ -1757,6 +1760,7 @@ class EpcPropertyDataMapper:
|
|||
wall_insulation_type=bp.sap_alternative_wall_2.wall_insulation_type,
|
||||
wall_thickness_measured=bp.sap_alternative_wall_2.wall_thickness_measured,
|
||||
wall_insulation_thickness=bp.sap_alternative_wall_2.wall_insulation_thickness,
|
||||
is_sheltered=bp.sap_alternative_wall_2.sheltered_wall == "Y",
|
||||
)
|
||||
if bp.sap_alternative_wall_2
|
||||
else None
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ class SapAlternativeWall:
|
|||
wall_insulation_type: int
|
||||
wall_thickness_measured: str
|
||||
wall_insulation_thickness: Optional[str] = None
|
||||
sheltered_wall: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -258,6 +258,7 @@ class SapAlternativeWall:
|
|||
wall_insulation_type: int
|
||||
wall_thickness_measured: str
|
||||
wall_insulation_thickness: Optional[str] = None
|
||||
sheltered_wall: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -1339,4 +1339,16 @@ def _alt_wall_w_per_k(
|
|||
# dry-lined) → U=2.34 via §5.6 + §5.8 chain.
|
||||
wall_thickness_mm=alt_wall.wall_thickness_mm,
|
||||
)
|
||||
if alt_wall.is_sheltered:
|
||||
# RdSAP 10 Table 4 (p.22) "Sheltered" wall: an alt sub-area
|
||||
# adjacent to an unheated buffer (stair core, adjoining
|
||||
# structure) carries an added external surface resistance of
|
||||
# R=0.5 m²K/W → U_sheltered = 1/(1/U + 0.5). Mirrors the main
|
||||
# wall's `gable_wall_sheltered` branch
|
||||
# (`_SHELTERED_GABLE_ADDED_RESISTANCE_M2K_W`). The gov-EPC API
|
||||
# lodges this per alt-wall as `sheltered_wall="Y"`; without it a
|
||||
# sheltered timber/cavity alt sub-area billed its full exposed U
|
||||
# (cert 0340-2976: a 42 m² sheltered timber-frame alt at U=2.5
|
||||
# over-stated the wall channel by ~58 W/K → -5 SAP under-rate).
|
||||
alt_u = 1.0 / (1.0 / alt_u + _SHELTERED_GABLE_ADDED_RESISTANCE_M2K_W)
|
||||
return alt_u * net_alt_area
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ from domain.sap10_calculator.worksheet.heat_transmission import (
|
|||
heat_transmission_from_cert,
|
||||
)
|
||||
from domain.sap10_calculator.worksheet.heat_transmission import (
|
||||
_alt_wall_w_per_k, # pyright: ignore[reportPrivateUsage]
|
||||
_joined_main_roof_descriptions, # pyright: ignore[reportPrivateUsage]
|
||||
_part_geometry, # pyright: ignore[reportPrivateUsage]
|
||||
_round_half_up, # pyright: ignore[reportPrivateUsage]
|
||||
|
|
@ -1193,6 +1194,40 @@ def test_alternative_wall_uses_own_construction_and_deducts_from_main_wall_area(
|
|||
)
|
||||
|
||||
|
||||
def test_sheltered_alternative_wall_applies_table4_0p5_resistance() -> None:
|
||||
# Arrange — RdSAP 10 Table 4 (PDF p.22) "Sheltered" wall: a sub-area
|
||||
# adjacent to an unheated buffer carries an added external surface
|
||||
# resistance R=0.5 m²K/W, so its U reduces to 1/(1/U + 0.5). The gov
|
||||
# EPC API lodges this per alt-wall as `sheltered_wall="Y"` (mapped to
|
||||
# `is_sheltered`). Two identical timber-frame as-built alt sub-areas
|
||||
# (cavity-band-A → uninsulated cascade U), one exposed, one sheltered.
|
||||
from dataclasses import replace
|
||||
|
||||
from domain.sap10_ml.rdsap_uvalues import Country
|
||||
|
||||
area = 42.0
|
||||
exposed = SapAlternativeWall(
|
||||
wall_area=area, wall_dry_lined="N", wall_construction=5,
|
||||
wall_insulation_type=4, wall_thickness_measured="Y",
|
||||
wall_insulation_thickness="NI", is_sheltered=False,
|
||||
)
|
||||
sheltered = replace(exposed, is_sheltered=True)
|
||||
|
||||
# Act
|
||||
exposed_wpk = _alt_wall_w_per_k(
|
||||
alt_wall=exposed, country=Country.ENG, age_band="A", wall_description=None,
|
||||
)
|
||||
sheltered_wpk = _alt_wall_w_per_k(
|
||||
alt_wall=sheltered, country=Country.ENG, age_band="A", wall_description=None,
|
||||
)
|
||||
|
||||
# Assert — the sheltered U is the exposed U with R=0.5 added.
|
||||
exposed_u = exposed_wpk / area
|
||||
expected_sheltered_u = 1.0 / (1.0 / exposed_u + 0.5)
|
||||
assert abs(sheltered_wpk - expected_sheltered_u * area) <= 1e-9
|
||||
assert sheltered_wpk < exposed_wpk
|
||||
|
||||
|
||||
def test_window_uses_effective_u_value_with_curtain_resistance_per_sap10_2_section_3_2() -> None:
|
||||
"""SAP10.2 §3.2: the window U-value used for heat-transmission is the
|
||||
effective form `U_eff = 1/(1/U_raw + 0.04)` — the 0.04 m²K/W is the
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue