mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
mapper: Elmhurst path populates roof_construction (int) for cross-mapper parity
The gov-EPC API mapper sets BOTH roof_construction (int) and roof_construction_type (str, derived via _API_ROOF_CONSTRUCTION_TO_STR), but the Elmhurst mapper set only the string — leaving roof_construction None on every site-notes cert. The SAP cascade reads the STRING (so SAP cross-mapper parity always held), but consumers of the int (e.g. domain/sap10_ml/transform.py ML aggregates `main_dwelling_roof_ construction`) silently saw None on the Elmhurst path. New `_elmhurst_roof_construction_int` maps the Elmhurst roof-type code to the same SAP10 int the API lodges (F→1, PN→3, PA→4, PS→8, S/A→7), harvested from the committed Summary fixtures. Unlike the wall map it returns None (not a strict-raise) for unmapped codes: the int is not cascade-load-bearing, so an unknown roof must not block the cert (vaulted 5 / thatched 6 / NR omitted until a fixture surfaces them). The 6 hand-built U985 reference fixtures gain the matching roof_construction int (4/4/3 etc.) so test_from_elmhurst_site_notes_ matches_hand_built_* still asserts structural parity. SAP output is unchanged (cascade reads the string). §4 suite green (2407 passed); the two pre-existing stone-§5.6 sap10_ml failures are unrelated/out of scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3684a142ac
commit
f326e4eb53
8 changed files with 83 additions and 0 deletions
|
|
@ -4361,6 +4361,32 @@ def test_elmhurst_foam_cylinder_insulation_still_maps_to_factory_code_1() -> Non
|
||||||
assert code == 1
|
assert code == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_elmhurst_roof_construction_int_matches_api_codes() -> None:
|
||||||
|
# Arrange — cross-mapper structural parity: the gov-EPC API mapper
|
||||||
|
# populates BOTH roof_construction (int) and roof_construction_type
|
||||||
|
# (str derived via `_API_ROOF_CONSTRUCTION_TO_STR`), but the Elmhurst
|
||||||
|
# mapper set only the string, leaving the int None. The SAP cascade
|
||||||
|
# reads the string (so SAP parity held), but consumers of the int
|
||||||
|
# (e.g. domain/sap10_ml ML aggregates) saw None on every site-notes
|
||||||
|
# cert. `_elmhurst_roof_construction_int` closes the gap, mapping the
|
||||||
|
# Elmhurst roof code to the same SAP10 int the API lodges. Unmapped
|
||||||
|
# codes return None (not a raise) — the int is not cascade-load-
|
||||||
|
# bearing, so an unknown roof must not block the cert.
|
||||||
|
from datatypes.epc.domain.mapper import _elmhurst_roof_construction_int # pyright: ignore[reportPrivateUsage]
|
||||||
|
|
||||||
|
# Act / Assert — each Elmhurst roof code → the gov-EPC API int.
|
||||||
|
assert _elmhurst_roof_construction_int("F Flat") == 1
|
||||||
|
assert _elmhurst_roof_construction_int("PN Pitched (slates/tiles), no access") == 3
|
||||||
|
assert _elmhurst_roof_construction_int("PA Pitched (slates/tiles), access to loft") == 4
|
||||||
|
assert _elmhurst_roof_construction_int("PS Pitched, sloping ceiling") == 8
|
||||||
|
assert _elmhurst_roof_construction_int("S Same dwelling above") == 7
|
||||||
|
assert _elmhurst_roof_construction_int("A Another dwelling above") == 7
|
||||||
|
# Absent / unmapped → None (no raise; not cascade-load-bearing).
|
||||||
|
assert _elmhurst_roof_construction_int(None) is None
|
||||||
|
assert _elmhurst_roof_construction_int("") is None
|
||||||
|
assert _elmhurst_roof_construction_int("NR Non-residential space above") is None
|
||||||
|
|
||||||
|
|
||||||
def test_elmhurst_wall_is_basement_disambiguates_system_built_from_basement() -> None:
|
def test_elmhurst_wall_is_basement_disambiguates_system_built_from_basement() -> None:
|
||||||
# Arrange — "SY System build" and "B Basement wall" both map to SAP10
|
# Arrange — "SY System build" and "B Basement wall" both map to SAP10
|
||||||
# wall_construction=6 (canonical WALL_SYSTEM_BUILT). The explicit
|
# wall_construction=6 (canonical WALL_SYSTEM_BUILT). The explicit
|
||||||
|
|
|
||||||
|
|
@ -2314,6 +2314,40 @@ def _elmhurst_dwelling_type(
|
||||||
return f"{position} flat"
|
return f"{position} flat"
|
||||||
|
|
||||||
|
|
||||||
|
# Elmhurst roof-type codes → SAP10 roof_construction integer, matching the
|
||||||
|
# gov-EPC API codes in `_API_ROOF_CONSTRUCTION_TO_STR` so the two
|
||||||
|
# front-ends populate the same field. Harvested from the committed
|
||||||
|
# Elmhurst Summary fixtures (corpus + cohort): F/PN/PA/PS/S/A. Vaulted (5)
|
||||||
|
# and thatched (6) are omitted until a fixture surfaces their Elmhurst
|
||||||
|
# codes. NR ("Non-residential space above") is intentionally left
|
||||||
|
# unmapped — the gov enum's code 7 is specifically "dwelling above".
|
||||||
|
_ELMHURST_ROOF_CODE_TO_SAP10: Dict[str, int] = {
|
||||||
|
"F": 1, # Flat
|
||||||
|
"PN": 3, # Pitched (slates/tiles), no access (to loft)
|
||||||
|
"PA": 4, # Pitched (slates/tiles), access to loft
|
||||||
|
"PS": 8, # Pitched, sloping ceiling
|
||||||
|
"S": 7, # Same dwelling above
|
||||||
|
"A": 7, # Another dwelling above
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _elmhurst_roof_construction_int(coded: Optional[str]) -> Optional[int]:
|
||||||
|
"""Map an Elmhurst roof_type string ('PA Pitched (slates/tiles),
|
||||||
|
access to loft') to the SAP10 `roof_construction` integer the gov-EPC
|
||||||
|
API lodges (4), so the site-notes and API front-ends populate the
|
||||||
|
same field (cross-mapper structural parity).
|
||||||
|
|
||||||
|
Returns None for an absent or unmapped code — and, unlike
|
||||||
|
`_elmhurst_wall_construction_int`, does NOT raise. `roof_construction`
|
||||||
|
(int) is not read by the SAP cascade (which reads the string
|
||||||
|
`roof_construction_type`, populated on both paths), so an unmapped
|
||||||
|
roof code stays None — the pre-existing Elmhurst behaviour — rather
|
||||||
|
than blocking the cert."""
|
||||||
|
if not coded:
|
||||||
|
return None
|
||||||
|
return _ELMHURST_ROOF_CODE_TO_SAP10.get(_leading_code(coded))
|
||||||
|
|
||||||
|
|
||||||
def _elmhurst_wall_construction_int(coded: str) -> Optional[int]:
|
def _elmhurst_wall_construction_int(coded: str) -> Optional[int]:
|
||||||
"""Map an Elmhurst wall_type string ('CA Cavity') to the SAP10
|
"""Map an Elmhurst wall_type string ('CA Cavity') to the SAP10
|
||||||
integer code (4). Returns None when the lodging is absent (empty
|
integer code (4). Returns None when the lodging is absent (empty
|
||||||
|
|
@ -3494,6 +3528,7 @@ def _map_elmhurst_building_part(
|
||||||
if walls.insulation_thickness_mm is not None
|
if walls.insulation_thickness_mm is not None
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
roof_construction=_elmhurst_roof_construction_int(roof.roof_type),
|
||||||
roof_construction_type=_strip_code(roof.roof_type),
|
roof_construction_type=_strip_code(roof.roof_type),
|
||||||
roof_insulation_location=_strip_code(roof.insulation),
|
roof_insulation_location=_strip_code(roof.insulation),
|
||||||
roof_insulation_thickness=_resolve_sloping_ceiling_thickness(
|
roof_insulation_thickness=_resolve_sloping_ceiling_thickness(
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ def build_epc() -> EpcPropertyData:
|
||||||
"""
|
"""
|
||||||
main = SapBuildingPart(
|
main = SapBuildingPart(
|
||||||
identifier=BuildingPartIdentifier.MAIN,
|
identifier=BuildingPartIdentifier.MAIN,
|
||||||
|
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||||
|
roof_construction=4,
|
||||||
construction_age_band="B",
|
construction_age_band="B",
|
||||||
wall_construction=_WC_CAVITY,
|
wall_construction=_WC_CAVITY,
|
||||||
wall_insulation_type=4,
|
wall_insulation_type=4,
|
||||||
|
|
@ -98,6 +100,8 @@ def build_epc() -> EpcPropertyData:
|
||||||
)
|
)
|
||||||
extension_1 = SapBuildingPart(
|
extension_1 = SapBuildingPart(
|
||||||
identifier=BuildingPartIdentifier.EXTENSION_1,
|
identifier=BuildingPartIdentifier.EXTENSION_1,
|
||||||
|
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||||
|
roof_construction=4,
|
||||||
construction_age_band="B",
|
construction_age_band="B",
|
||||||
wall_construction=_WC_CAVITY,
|
wall_construction=_WC_CAVITY,
|
||||||
wall_insulation_type=4,
|
wall_insulation_type=4,
|
||||||
|
|
@ -130,6 +134,8 @@ def build_epc() -> EpcPropertyData:
|
||||||
)
|
)
|
||||||
extension_2 = SapBuildingPart(
|
extension_2 = SapBuildingPart(
|
||||||
identifier=BuildingPartIdentifier.EXTENSION_2,
|
identifier=BuildingPartIdentifier.EXTENSION_2,
|
||||||
|
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||||
|
roof_construction=3,
|
||||||
construction_age_band="B",
|
construction_age_band="B",
|
||||||
wall_construction=_WC_CAVITY,
|
wall_construction=_WC_CAVITY,
|
||||||
wall_insulation_type=4,
|
wall_insulation_type=4,
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ def build_epc() -> EpcPropertyData:
|
||||||
"""
|
"""
|
||||||
main = SapBuildingPart(
|
main = SapBuildingPart(
|
||||||
identifier=BuildingPartIdentifier.MAIN,
|
identifier=BuildingPartIdentifier.MAIN,
|
||||||
|
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||||
|
roof_construction=4,
|
||||||
construction_age_band="B",
|
construction_age_band="B",
|
||||||
wall_construction=_WC_CAVITY,
|
wall_construction=_WC_CAVITY,
|
||||||
wall_insulation_type=4,
|
wall_insulation_type=4,
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ def build_epc() -> EpcPropertyData:
|
||||||
"""
|
"""
|
||||||
main = SapBuildingPart(
|
main = SapBuildingPart(
|
||||||
identifier=BuildingPartIdentifier.MAIN,
|
identifier=BuildingPartIdentifier.MAIN,
|
||||||
|
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||||
|
roof_construction=4,
|
||||||
construction_age_band="B",
|
construction_age_band="B",
|
||||||
wall_construction=_WC_CAVITY,
|
wall_construction=_WC_CAVITY,
|
||||||
wall_insulation_type=4,
|
wall_insulation_type=4,
|
||||||
|
|
@ -133,6 +135,8 @@ def build_epc() -> EpcPropertyData:
|
||||||
)
|
)
|
||||||
extension = SapBuildingPart(
|
extension = SapBuildingPart(
|
||||||
identifier=BuildingPartIdentifier.EXTENSION_1,
|
identifier=BuildingPartIdentifier.EXTENSION_1,
|
||||||
|
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||||
|
roof_construction=4,
|
||||||
construction_age_band="B",
|
construction_age_band="B",
|
||||||
wall_construction=_WC_CAVITY,
|
wall_construction=_WC_CAVITY,
|
||||||
wall_insulation_type=4,
|
wall_insulation_type=4,
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ def build_epc() -> EpcPropertyData:
|
||||||
"""
|
"""
|
||||||
main = SapBuildingPart(
|
main = SapBuildingPart(
|
||||||
identifier=BuildingPartIdentifier.MAIN,
|
identifier=BuildingPartIdentifier.MAIN,
|
||||||
|
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||||
|
roof_construction=4,
|
||||||
construction_age_band="B",
|
construction_age_band="B",
|
||||||
wall_construction=_WC_CAVITY,
|
wall_construction=_WC_CAVITY,
|
||||||
wall_insulation_type=4, # "A As Built"
|
wall_insulation_type=4, # "A As Built"
|
||||||
|
|
@ -130,6 +132,8 @@ def build_epc() -> EpcPropertyData:
|
||||||
)
|
)
|
||||||
extension = SapBuildingPart(
|
extension = SapBuildingPart(
|
||||||
identifier=BuildingPartIdentifier.EXTENSION_1,
|
identifier=BuildingPartIdentifier.EXTENSION_1,
|
||||||
|
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||||
|
roof_construction=4,
|
||||||
construction_age_band="B",
|
construction_age_band="B",
|
||||||
wall_construction=_WC_CAVITY,
|
wall_construction=_WC_CAVITY,
|
||||||
wall_insulation_type=4,
|
wall_insulation_type=4,
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ def build_epc() -> EpcPropertyData:
|
||||||
"""
|
"""
|
||||||
main = SapBuildingPart(
|
main = SapBuildingPart(
|
||||||
identifier=BuildingPartIdentifier.MAIN,
|
identifier=BuildingPartIdentifier.MAIN,
|
||||||
|
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||||
|
roof_construction=4,
|
||||||
construction_age_band="B",
|
construction_age_band="B",
|
||||||
wall_construction=_WC_CAVITY,
|
wall_construction=_WC_CAVITY,
|
||||||
wall_insulation_type=4,
|
wall_insulation_type=4,
|
||||||
|
|
@ -97,6 +99,8 @@ def build_epc() -> EpcPropertyData:
|
||||||
)
|
)
|
||||||
extension = SapBuildingPart(
|
extension = SapBuildingPart(
|
||||||
identifier=BuildingPartIdentifier.EXTENSION_1,
|
identifier=BuildingPartIdentifier.EXTENSION_1,
|
||||||
|
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||||
|
roof_construction=4,
|
||||||
construction_age_band="B",
|
construction_age_band="B",
|
||||||
wall_construction=_WC_CAVITY,
|
wall_construction=_WC_CAVITY,
|
||||||
wall_insulation_type=4,
|
wall_insulation_type=4,
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ def build_epc() -> EpcPropertyData:
|
||||||
"""
|
"""
|
||||||
main = SapBuildingPart(
|
main = SapBuildingPart(
|
||||||
identifier=BuildingPartIdentifier.MAIN,
|
identifier=BuildingPartIdentifier.MAIN,
|
||||||
|
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||||
|
roof_construction=3,
|
||||||
construction_age_band="A",
|
construction_age_band="A",
|
||||||
wall_construction=_WC_CAVITY,
|
wall_construction=_WC_CAVITY,
|
||||||
wall_insulation_type=4,
|
wall_insulation_type=4,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue