mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +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
|
||||
|
||||
|
||||
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:
|
||||
# Arrange — "SY System build" and "B Basement wall" both map to SAP10
|
||||
# wall_construction=6 (canonical WALL_SYSTEM_BUILT). The explicit
|
||||
|
|
|
|||
|
|
@ -2314,6 +2314,40 @@ def _elmhurst_dwelling_type(
|
|||
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]:
|
||||
"""Map an Elmhurst wall_type string ('CA Cavity') to the SAP10
|
||||
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
|
||||
else None
|
||||
),
|
||||
roof_construction=_elmhurst_roof_construction_int(roof.roof_type),
|
||||
roof_construction_type=_strip_code(roof.roof_type),
|
||||
roof_insulation_location=_strip_code(roof.insulation),
|
||||
roof_insulation_thickness=_resolve_sloping_ceiling_thickness(
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ def build_epc() -> EpcPropertyData:
|
|||
"""
|
||||
main = SapBuildingPart(
|
||||
identifier=BuildingPartIdentifier.MAIN,
|
||||
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||
roof_construction=4,
|
||||
construction_age_band="B",
|
||||
wall_construction=_WC_CAVITY,
|
||||
wall_insulation_type=4,
|
||||
|
|
@ -98,6 +100,8 @@ def build_epc() -> EpcPropertyData:
|
|||
)
|
||||
extension_1 = SapBuildingPart(
|
||||
identifier=BuildingPartIdentifier.EXTENSION_1,
|
||||
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||
roof_construction=4,
|
||||
construction_age_band="B",
|
||||
wall_construction=_WC_CAVITY,
|
||||
wall_insulation_type=4,
|
||||
|
|
@ -130,6 +134,8 @@ def build_epc() -> EpcPropertyData:
|
|||
)
|
||||
extension_2 = SapBuildingPart(
|
||||
identifier=BuildingPartIdentifier.EXTENSION_2,
|
||||
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||
roof_construction=3,
|
||||
construction_age_band="B",
|
||||
wall_construction=_WC_CAVITY,
|
||||
wall_insulation_type=4,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ def build_epc() -> EpcPropertyData:
|
|||
"""
|
||||
main = SapBuildingPart(
|
||||
identifier=BuildingPartIdentifier.MAIN,
|
||||
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||
roof_construction=4,
|
||||
construction_age_band="B",
|
||||
wall_construction=_WC_CAVITY,
|
||||
wall_insulation_type=4,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ def build_epc() -> EpcPropertyData:
|
|||
"""
|
||||
main = SapBuildingPart(
|
||||
identifier=BuildingPartIdentifier.MAIN,
|
||||
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||
roof_construction=4,
|
||||
construction_age_band="B",
|
||||
wall_construction=_WC_CAVITY,
|
||||
wall_insulation_type=4,
|
||||
|
|
@ -133,6 +135,8 @@ def build_epc() -> EpcPropertyData:
|
|||
)
|
||||
extension = SapBuildingPart(
|
||||
identifier=BuildingPartIdentifier.EXTENSION_1,
|
||||
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||
roof_construction=4,
|
||||
construction_age_band="B",
|
||||
wall_construction=_WC_CAVITY,
|
||||
wall_insulation_type=4,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ def build_epc() -> EpcPropertyData:
|
|||
"""
|
||||
main = SapBuildingPart(
|
||||
identifier=BuildingPartIdentifier.MAIN,
|
||||
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||
roof_construction=4,
|
||||
construction_age_band="B",
|
||||
wall_construction=_WC_CAVITY,
|
||||
wall_insulation_type=4, # "A As Built"
|
||||
|
|
@ -130,6 +132,8 @@ def build_epc() -> EpcPropertyData:
|
|||
)
|
||||
extension = SapBuildingPart(
|
||||
identifier=BuildingPartIdentifier.EXTENSION_1,
|
||||
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||
roof_construction=4,
|
||||
construction_age_band="B",
|
||||
wall_construction=_WC_CAVITY,
|
||||
wall_insulation_type=4,
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ def build_epc() -> EpcPropertyData:
|
|||
"""
|
||||
main = SapBuildingPart(
|
||||
identifier=BuildingPartIdentifier.MAIN,
|
||||
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||
roof_construction=4,
|
||||
construction_age_band="B",
|
||||
wall_construction=_WC_CAVITY,
|
||||
wall_insulation_type=4,
|
||||
|
|
@ -97,6 +99,8 @@ def build_epc() -> EpcPropertyData:
|
|||
)
|
||||
extension = SapBuildingPart(
|
||||
identifier=BuildingPartIdentifier.EXTENSION_1,
|
||||
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||
roof_construction=4,
|
||||
construction_age_band="B",
|
||||
wall_construction=_WC_CAVITY,
|
||||
wall_insulation_type=4,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ def build_epc() -> EpcPropertyData:
|
|||
"""
|
||||
main = SapBuildingPart(
|
||||
identifier=BuildingPartIdentifier.MAIN,
|
||||
# API parity: roof_construction int mirrors the gov-EPC mapper
|
||||
roof_construction=3,
|
||||
construction_age_band="A",
|
||||
wall_construction=_WC_CAVITY,
|
||||
wall_insulation_type=4,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue