mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
mapper: disambiguate SY system-built from B basement wall (both share code 6)
RdSAP10 `wall_construction == 6` is canonically WALL_SYSTEM_BUILT, but the gov-EPC basement heuristic hijacked it: Elmhurst lodges both "SY System build" and "B Basement wall" as code 6, so a system-built wall was mis-flagged `main_wall_is_basement` and routed to the RdSAP §5.17 `u_basement_wall` override instead of the system-built U-value table. System-built stays on its canonical code 6; the basement signal moves to an explicit `is_basement` (SapAlternativeWall) / `wall_is_basement` (SapBuildingPart) Optional[bool] flag, set by the Elmhurst mapper from the distinct "SY"/"B" codes via `_elmhurst_wall_is_basement` (True for B, False for SY, None otherwise). The `main_wall_is_basement` / `is_basement_wall` properties honour the flag when set and fall back to the gov-EPC API code-6 heuristic when None — so the API path (basement lodged as integer 6, no flag) and the cert 000565 "B" cohort are unchanged. Acceptance (a recommendation-summary generator depends on it): a system-built MAIN wall reports wall_construction == 6 AND main_wall_is_basement is False; a genuine basement main wall still reports main_wall_is_basement is True. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9a483b8711
commit
193ae27124
4 changed files with 138 additions and 15 deletions
|
|
@ -4359,3 +4359,24 @@ def test_elmhurst_foam_cylinder_insulation_still_maps_to_factory_code_1() -> Non
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert code == 1
|
assert code == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_elmhurst_wall_is_basement_disambiguates_system_built_from_basement() -> None:
|
||||||
|
# Arrange — "SY System build" and "B Basement wall" both map to SAP10
|
||||||
|
# wall_construction=6 (canonical WALL_SYSTEM_BUILT). The explicit
|
||||||
|
# basement flag separates them: only "B" is a basement wall (drives
|
||||||
|
# RdSAP §5.17 u_basement_wall); "SY" is False so it routes through the
|
||||||
|
# normal system-built U-value table; any other code → None (the
|
||||||
|
# gov-EPC API code-6 heuristic still applies).
|
||||||
|
from datatypes.epc.domain.mapper import _elmhurst_wall_construction_int # pyright: ignore[reportPrivateUsage]
|
||||||
|
from datatypes.epc.domain.mapper import _elmhurst_wall_is_basement # pyright: ignore[reportPrivateUsage]
|
||||||
|
|
||||||
|
# Act / Assert — system-built keeps code 6 but is NOT basement.
|
||||||
|
assert _elmhurst_wall_construction_int("SY System build") == 6
|
||||||
|
assert _elmhurst_wall_is_basement("SY System build") is False
|
||||||
|
# Genuine basement: code 6 AND flagged basement.
|
||||||
|
assert _elmhurst_wall_construction_int("B Basement wall") == 6
|
||||||
|
assert _elmhurst_wall_is_basement("B Basement wall") is True
|
||||||
|
# Other constructions defer to the API code-6 heuristic.
|
||||||
|
assert _elmhurst_wall_is_basement("CA Cavity") is None
|
||||||
|
assert _elmhurst_wall_is_basement("") is None
|
||||||
|
|
|
||||||
|
|
@ -435,13 +435,24 @@ class SapAlternativeWall:
|
||||||
# Mirrors `SapBuildingPart.wall_thickness_mm` per the
|
# Mirrors `SapBuildingPart.wall_thickness_mm` per the
|
||||||
# [[feedback-no-misleading-insulation-type]] convention.
|
# [[feedback-no-misleading-insulation-type]] convention.
|
||||||
wall_thickness_mm: Optional[int] = None
|
wall_thickness_mm: Optional[int] = None
|
||||||
|
# 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"
|
||||||
|
# and "B Basement wall" as code 6. When the source can tell them apart
|
||||||
|
# (the Elmhurst mapper, from the distinct "SY"/"B" codes) it sets this
|
||||||
|
# flag; None falls back to the gov-EPC API code-6 heuristic so the API
|
||||||
|
# path (basement lodged as integer 6, no flag) is unchanged.
|
||||||
|
is_basement: Optional[bool] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_basement_wall(self) -> bool:
|
def is_basement_wall(self) -> bool:
|
||||||
"""True iff this alt sub-area is the dwelling's basement wall —
|
"""True iff this alt sub-area is the dwelling's basement wall.
|
||||||
identified by RdSAP10 wall_construction code = 6 (see module
|
Honours the explicit `is_basement` flag when set; otherwise falls
|
||||||
constant `BASEMENT_WALL_CONSTRUCTION_CODE`). RdSAP §5.17 / Table 23
|
back to the gov-EPC API basement sentinel `wall_construction == 6`
|
||||||
applies a special U-value lookup to basement walls."""
|
(`BASEMENT_WALL_CONSTRUCTION_CODE`). RdSAP §5.17 / Table 23 applies
|
||||||
|
a special U-value lookup to basement walls."""
|
||||||
|
if self.is_basement is not None:
|
||||||
|
return self.is_basement
|
||||||
return self.wall_construction == BASEMENT_WALL_CONSTRUCTION_CODE
|
return self.wall_construction == BASEMENT_WALL_CONSTRUCTION_CODE
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -514,12 +525,23 @@ class SapBuildingPart:
|
||||||
# The dwelling-wide `construction_age_band` does NOT govern curtain
|
# The dwelling-wide `construction_age_band` does NOT govern curtain
|
||||||
# walls; this field decouples them per spec.
|
# walls; this field decouples them per spec.
|
||||||
curtain_wall_age: Optional[str] = None
|
curtain_wall_age: Optional[str] = None
|
||||||
|
# Explicit basement determination for the primary wall. See
|
||||||
|
# `SapAlternativeWall.is_basement` — RdSAP10 code 6 is canonically
|
||||||
|
# SYSTEM-BUILT, so the Elmhurst mapper sets this flag from the distinct
|
||||||
|
# "SY"/"B" codes (False for system-built, True for basement); None
|
||||||
|
# preserves the gov-EPC API code-6 heuristic.
|
||||||
|
wall_is_basement: Optional[bool] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def main_wall_is_basement(self) -> bool:
|
def main_wall_is_basement(self) -> bool:
|
||||||
"""True iff this part's primary wall (not an alt sub-area) is the
|
"""True iff this part's primary wall (not an alt sub-area) is the
|
||||||
basement wall — happens when the whole part sits below grade.
|
basement wall — happens when the whole part sits below grade.
|
||||||
Empirically 54 of 67k parts in the 2026 sweep; rare but real."""
|
Empirically 54 of 67k parts in the 2026 sweep; rare but real.
|
||||||
|
Honours the explicit `wall_is_basement` flag when set (so a
|
||||||
|
SYSTEM-BUILT wall, which shares code 6, is not mis-flagged);
|
||||||
|
otherwise falls back to the gov-EPC API code-6 heuristic."""
|
||||||
|
if self.wall_is_basement is not None:
|
||||||
|
return self.wall_is_basement
|
||||||
return self.wall_construction == BASEMENT_WALL_CONSTRUCTION_CODE
|
return self.wall_construction == BASEMENT_WALL_CONSTRUCTION_CODE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -2172,16 +2172,16 @@ _ELMHURST_WALL_CODE_TO_SAP10: Dict[str, int] = {
|
||||||
"CA": 4, # Cavity
|
"CA": 4, # Cavity
|
||||||
"TF": 5, # Timber frame
|
"TF": 5, # Timber frame
|
||||||
"TI": 5, # Timber frame (Elmhurst's alt-wall code; same SAP10 mapping)
|
"TI": 5, # Timber frame (Elmhurst's alt-wall code; same SAP10 mapping)
|
||||||
"SY": 6, # System build
|
"SY": 6, # System build — canonical RdSAP10 WALL_SYSTEM_BUILT=6.
|
||||||
"B": 6, # Basement wall (cert 000565 Ext3+Ext4) — routes to the
|
"B": 6, # Basement wall (cert 000565 Ext3+Ext4). Numerically
|
||||||
# `BASEMENT_WALL_CONSTRUCTION_CODE=6` canonical signal so
|
# collides with "SY" System build on code 6, so the
|
||||||
# the cascade's `part.main_wall_is_basement` triggers the
|
# basement vs system-built distinction is carried by the
|
||||||
# RdSAP 10 §5.17 / Table 23 `u_basement_wall` override
|
# explicit `is_basement` / `wall_is_basement` flag (set
|
||||||
# (heat_transmission.py:640). Collides numerically with
|
# via `_elmhurst_wall_is_basement`) rather than the code:
|
||||||
# "SY" System build — the cascade's basement check
|
# only "B" triggers the cascade's `main_wall_is_basement`
|
||||||
# precedes `u_wall(construction=6)` so SY would be
|
# → RdSAP 10 §5.17 / Table 23 `u_basement_wall` override.
|
||||||
# silently mis-routed to u_basement_wall today; no cohort
|
# "SY" sets the flag False so it routes through the normal
|
||||||
# fixture exercises SY yet so the conflict is dormant.
|
# `u_wall(construction=6)` system-built table instead.
|
||||||
"CO": 7, # Cob
|
"CO": 7, # Cob
|
||||||
"PH": 8, # Park home
|
"PH": 8, # Park home
|
||||||
"CW": 9, # Curtain wall
|
"CW": 9, # Curtain wall
|
||||||
|
|
@ -2263,6 +2263,32 @@ def _elmhurst_wall_construction_int(coded: str) -> Optional[int]:
|
||||||
return _ELMHURST_WALL_CODE_TO_SAP10[code]
|
return _ELMHURST_WALL_CODE_TO_SAP10[code]
|
||||||
|
|
||||||
|
|
||||||
|
# Elmhurst wall codes that both resolve to SAP10 wall_construction=6 but
|
||||||
|
# carry opposite basement meaning: "B" Basement wall vs "SY" System build
|
||||||
|
# (see `_ELMHURST_WALL_CODE_TO_SAP10`). RdSAP10 code 6 is canonically
|
||||||
|
# WALL_SYSTEM_BUILT; the explicit basement flag lets the cascade route a
|
||||||
|
# genuine basement to RdSAP §5.17 `u_basement_wall` without mis-flagging
|
||||||
|
# a system-built wall.
|
||||||
|
_ELMHURST_BASEMENT_WALL_CODE: Final[str] = "B"
|
||||||
|
_ELMHURST_SYSTEM_BUILT_WALL_CODE: Final[str] = "SY"
|
||||||
|
|
||||||
|
|
||||||
|
def _elmhurst_wall_is_basement(coded: str) -> Optional[bool]:
|
||||||
|
"""Disambiguate the SAP10 code-6 collision from the Elmhurst wall_type
|
||||||
|
string. Returns True for "B Basement wall", False for "SY System
|
||||||
|
build", and None for every other code (so the SapBuildingPart /
|
||||||
|
SapAlternativeWall properties fall back to the gov-EPC API code-6
|
||||||
|
heuristic — unchanged for the API path). Empty lodging → None."""
|
||||||
|
code = _leading_code(coded)
|
||||||
|
if not code:
|
||||||
|
return None
|
||||||
|
if code == _ELMHURST_BASEMENT_WALL_CODE:
|
||||||
|
return True
|
||||||
|
if code == _ELMHURST_SYSTEM_BUILT_WALL_CODE:
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Elmhurst Party Wall Type codes — distinct category-set from the Wall
|
# Elmhurst Party Wall Type codes — distinct category-set from the Wall
|
||||||
# Type field; the codes describe construction class for `u_party_wall`
|
# Type field; the codes describe construction class for `u_party_wall`
|
||||||
# (Table 4 / RdSAP §S.3.2) rather than a specific SAP10 wall-type. Maps
|
# (Table 4 / RdSAP §S.3.2) rather than a specific SAP10 wall-type. Maps
|
||||||
|
|
@ -3385,6 +3411,7 @@ def _map_elmhurst_building_part(
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
construction_age_band=age_code,
|
construction_age_band=age_code,
|
||||||
wall_construction=_elmhurst_wall_construction_int(walls.wall_type),
|
wall_construction=_elmhurst_wall_construction_int(walls.wall_type),
|
||||||
|
wall_is_basement=_elmhurst_wall_is_basement(walls.wall_type),
|
||||||
wall_insulation_type=_elmhurst_wall_insulation_int(walls.insulation),
|
wall_insulation_type=_elmhurst_wall_insulation_int(walls.insulation),
|
||||||
wall_thickness_measured=not walls.thickness_unknown,
|
wall_thickness_measured=not walls.thickness_unknown,
|
||||||
party_wall_construction=_elmhurst_party_wall_construction_int(walls.party_wall_type),
|
party_wall_construction=_elmhurst_party_wall_construction_int(walls.party_wall_type),
|
||||||
|
|
@ -3463,6 +3490,7 @@ def _map_elmhurst_alternative_wall(
|
||||||
wall_thickness_measured="Y" if not a.thickness_unknown else "N",
|
wall_thickness_measured="Y" if not a.thickness_unknown else "N",
|
||||||
wall_insulation_thickness=None,
|
wall_insulation_thickness=None,
|
||||||
wall_thickness_mm=measured_thickness_mm,
|
wall_thickness_mm=measured_thickness_mm,
|
||||||
|
is_basement=_elmhurst_wall_is_basement(a.wall_type),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1222,6 +1222,58 @@ def test_sap_building_part_has_basement_detects_main_wall_and_alt_wall_codes() -
|
||||||
assert alt_is_basement.main_wall_is_basement is False # main is still wc=4
|
assert alt_is_basement.main_wall_is_basement is False # main is still wc=4
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_wall_is_basement_flag_disambiguates_system_built_from_basement() -> None:
|
||||||
|
"""RdSAP10 `wall_construction == 6` is canonically SYSTEM-BUILT
|
||||||
|
(`WALL_SYSTEM_BUILT`), but the gov-EPC basement heuristic hijacked it
|
||||||
|
(Elmhurst lodges both "SY System build" and "B Basement wall" as
|
||||||
|
code 6). The explicit `wall_is_basement` flag — set by the Elmhurst
|
||||||
|
mapper from the distinct "SY"/"B" codes — disambiguates:
|
||||||
|
- flag True → basement (drives §5.17 u_basement_wall)
|
||||||
|
- flag False → system-built (drives the u_wall code-6 table)
|
||||||
|
- flag None → fall back to the gov-EPC API code-6 heuristic
|
||||||
|
so the API path (which lodges basement as integer 6 with no flag) is
|
||||||
|
unchanged."""
|
||||||
|
from dataclasses import replace
|
||||||
|
# Arrange — three parts, all wall_construction=6, differing only in flag.
|
||||||
|
plain = make_building_part(
|
||||||
|
identifier=BuildingPartIdentifier.MAIN,
|
||||||
|
construction_age_band="G",
|
||||||
|
wall_construction=6, wall_insulation_type=4,
|
||||||
|
party_wall_construction=1, roof_construction=4,
|
||||||
|
floor_dimensions=[
|
||||||
|
make_floor_dimension(
|
||||||
|
total_floor_area_m2=80.0, room_height_m=2.5,
|
||||||
|
party_wall_length_m=0.0, heat_loss_perimeter_m=35.0, floor=0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
system_built = replace(plain, wall_is_basement=False)
|
||||||
|
basement = replace(plain, wall_is_basement=True)
|
||||||
|
api_code_6 = replace(plain, wall_is_basement=None)
|
||||||
|
|
||||||
|
# Act / Assert
|
||||||
|
assert system_built.main_wall_is_basement is False
|
||||||
|
assert basement.main_wall_is_basement is True
|
||||||
|
assert api_code_6.main_wall_is_basement is True # API heuristic preserved
|
||||||
|
|
||||||
|
# Alt-wall mirror — same Optional disambiguation on SapAlternativeWall.
|
||||||
|
alt_system_built = SapAlternativeWall(
|
||||||
|
wall_area=14.24, wall_dry_lined="N", wall_construction=6,
|
||||||
|
wall_insulation_type=4, wall_thickness_measured="Y", is_basement=False,
|
||||||
|
)
|
||||||
|
alt_basement = SapAlternativeWall(
|
||||||
|
wall_area=14.24, wall_dry_lined="N", wall_construction=6,
|
||||||
|
wall_insulation_type=4, wall_thickness_measured="Y", is_basement=True,
|
||||||
|
)
|
||||||
|
alt_api_code_6 = SapAlternativeWall(
|
||||||
|
wall_area=14.24, wall_dry_lined="N", wall_construction=6,
|
||||||
|
wall_insulation_type=4, wall_thickness_measured="Y",
|
||||||
|
)
|
||||||
|
assert alt_system_built.is_basement_wall is False
|
||||||
|
assert alt_basement.is_basement_wall is True
|
||||||
|
assert alt_api_code_6.is_basement_wall is True
|
||||||
|
|
||||||
|
|
||||||
def test_basement_alt_wall_uses_table_23_u_value_not_cascade() -> None:
|
def test_basement_alt_wall_uses_table_23_u_value_not_cascade() -> None:
|
||||||
"""RdSAP §5.17 / Table 23 governs basement-wall U-values: 0.7 for age
|
"""RdSAP §5.17 / Table 23 governs basement-wall U-values: 0.7 for age
|
||||||
A-F, 0.6 for G-H, 0.45 for I, 0.35 for J, ..., 0.26 for M. The
|
A-F, 0.6 for G-H, 0.45 for I, 0.35 for J, ..., 0.26 for M. The
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue