Map a landlord wall-type override to a wall Simulation Overlay 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-16 17:14:54 +00:00
parent 80b86d4790
commit db1e283b07
3 changed files with 136 additions and 0 deletions

View file

@ -0,0 +1,60 @@
"""Map a Landlord-Override `WallType` value to a wall Simulation Overlay (ADR-0032).
A `WallType` value is one full EPC wall description *material* (cavity, solid
brick, ) combined with *insulation state* (as built / with internal insulation
/ filled cavity / ). The calculator scores the wall from the RdSAP
`wall_construction` (material) and `wall_insulation_type` (state) **int codes**,
never the description string, so the overlay decomposes the value into those two
codes and emits an `EpcSimulation` targeting the override's building part. The
result folds onto the lodged EPC via `apply_simulations`, exactly as a wall
Measure's overlay does. Unresolvable material/state → None (no overlay).
"""
from __future__ import annotations
from typing import Optional
from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
# RdSAP `wall_construction` codes by material prefix (domain/sap10_ml/rdsap_uvalues.py).
_MATERIAL_CONSTRUCTION: dict[str, int] = {
"Cavity wall": 4,
"Solid brick": 3,
}
# RdSAP `wall_insulation_type` codes by insulation-state suffix
# (domain/sap10_ml/rdsap_uvalues.py): external 1, filled-cavity 2, internal 3,
# as-built/uninsulated 4, cavity+external 6, cavity+internal 7.
_STATE_INSULATION: dict[str, int] = {
"as built, no insulation (assumed)": 4,
"with internal insulation": 3,
"with external insulation": 1,
"filled cavity": 2,
"filled cavity and internal insulation": 7,
"filled cavity and external insulation": 6,
}
def wall_overlay_for(
wall_type_value: str, building_part: int
) -> Optional[EpcSimulation]:
material, _, state = wall_type_value.partition(", ")
construction = _MATERIAL_CONSTRUCTION.get(material)
insulation = _STATE_INSULATION.get(state)
if construction is None or insulation is None:
return None
identifier = (
BuildingPartIdentifier.MAIN
if building_part == 0
else BuildingPartIdentifier.extension(building_part)
)
return EpcSimulation(
building_parts={
identifier: BuildingPartOverlay(
wall_construction=construction,
wall_insulation_type=insulation,
)
}
)

View file

@ -25,6 +25,10 @@ class BuildingPartOverlay:
A `None` field means "leave the baseline value unchanged".
"""
# The wall material (RdSAP `wall_construction` code). Left `None` by Measures
# — insulating a wall doesn't change its material — but set by a Landlord
# Override that corrects the construction itself (ADR-0032).
wall_construction: Optional[int] = None
wall_insulation_type: Optional[int] = None
# Added solid-wall insulation depth (mm) — drives the calculator's Table 6
# bucket / §5.8 documentary U-value for EWI (`wall_insulation_type=1`) and

View file

@ -0,0 +1,72 @@
"""The Landlord-Override `WallType` → wall Simulation Overlay mapping (ADR-0032).
A `WallType` value decomposes into the RdSAP `wall_construction` (material) and
`wall_insulation_type` (state) int codes the calculator reads; the overlay
targets the override's building part. Unresolvable values produce no overlay.
"""
from __future__ import annotations
import pytest
from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier
from domain.epc.wall_type_overlay import wall_overlay_for
def test_solid_brick_with_internal_insulation_overlays_main_wall() -> None:
# Act
simulation = wall_overlay_for("Solid brick, with internal insulation", 0)
# Assert — solid brick (wall_construction 3) + internal insulation (type 3)
# on the main building part.
assert simulation is not None
overlay = simulation.building_parts[BuildingPartIdentifier.MAIN]
assert overlay.wall_construction == 3
assert overlay.wall_insulation_type == 3
@pytest.mark.parametrize(
("wall_type_value", "construction", "insulation"),
[
("Cavity wall, as built, no insulation (assumed)", 4, 4),
("Cavity wall, with internal insulation", 4, 3),
("Cavity wall, with external insulation", 4, 1),
("Cavity wall, filled cavity", 4, 2),
("Cavity wall, filled cavity and internal insulation", 4, 7),
("Cavity wall, filled cavity and external insulation", 4, 6),
("Solid brick, as built, no insulation (assumed)", 3, 4),
("Solid brick, with external insulation", 3, 1),
],
)
def test_material_and_state_decompose_to_their_gov_codes(
wall_type_value: str, construction: int, insulation: int
) -> None:
# Act
simulation = wall_overlay_for(wall_type_value, 0)
# Assert
assert simulation is not None
overlay = simulation.building_parts[BuildingPartIdentifier.MAIN]
assert overlay.wall_construction == construction
assert overlay.wall_insulation_type == insulation
def test_overlay_targets_the_extension_building_part() -> None:
# Act — building_part 1 is the first extension.
simulation = wall_overlay_for("Solid brick, with internal insulation", 1)
# Assert
assert simulation is not None
assert BuildingPartIdentifier.EXTENSION_1 in simulation.building_parts
@pytest.mark.parametrize(
"wall_type_value",
["Unknown", "Granite or whin, as built, no insulation (assumed)", ""],
)
def test_unresolvable_wall_type_produces_no_overlay(wall_type_value: str) -> None:
# Act
simulation = wall_overlay_for(wall_type_value, 0)
# Assert
assert simulation is None