mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(modelling): planning-restriction gate on solid-wall insulation
Slice 2c. recommend_solid_wall takes a PlanningRestrictions value object (defaults unrestricted): a conservation area removes the EWI Option (external appearance), a listed or heritage building removes both EWI and IWI (protected fabric) -> None when nothing survives (ADR-0019). Plus a guard that a cavity wall yields no solid-wall Recommendation (it is handled by recommend_cavity _wall). PlanningRestrictions will be sourced onto the Property from the geospatial layer in slice 3 (ADR-0020). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ac78771258
commit
51ea4993a0
2 changed files with 88 additions and 4 deletions
|
|
@ -15,6 +15,7 @@ generator. Detection + pricing only; impact is produced later by scoring
|
|||
(ADR-0016).
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
|
|
@ -29,6 +30,27 @@ from repositories.product.product_repository import ProductRepository
|
|||
_EXTERNAL_MEASURE_TYPE: Final[str] = "external_wall_insulation"
|
||||
_INTERNAL_MEASURE_TYPE: Final[str] = "internal_wall_insulation"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlanningRestrictions:
|
||||
"""A Property's planning protections that gate wall insulation (ADR-0019).
|
||||
A conservation area blocks EWI only (external appearance); a listed or
|
||||
heritage building blocks both EWI and IWI (protected fabric). Sourced from
|
||||
the geospatial layer onto the Property in a later slice (ADR-0020); defaults
|
||||
to unrestricted."""
|
||||
|
||||
in_conservation_area: bool = False
|
||||
is_listed: bool = False
|
||||
is_heritage: bool = False
|
||||
|
||||
@property
|
||||
def blocks_external(self) -> bool:
|
||||
return self.in_conservation_area or self.is_listed or self.is_heritage
|
||||
|
||||
@property
|
||||
def blocks_internal(self) -> bool:
|
||||
return self.is_listed or self.is_heritage
|
||||
|
||||
# RdSAP `wall_construction` codes (consistent across paths for 1-5).
|
||||
_WALL_SOLID_BRICK: Final[int] = 3
|
||||
_WALL_TIMBER_FRAME: Final[int] = 5
|
||||
|
|
@ -87,11 +109,22 @@ def _solid_wall_option(
|
|||
)
|
||||
|
||||
|
||||
def _allowed(measure_type: str, restrictions: PlanningRestrictions) -> bool:
|
||||
"""Whether a planning-gated Option survives (ADR-0019): EWI is removed by any
|
||||
restriction; IWI only by a listed/heritage protection."""
|
||||
if measure_type == _EXTERNAL_MEASURE_TYPE:
|
||||
return not restrictions.blocks_external
|
||||
return not restrictions.blocks_internal
|
||||
|
||||
|
||||
def recommend_solid_wall(
|
||||
epc: EpcPropertyData, products: ProductRepository
|
||||
epc: EpcPropertyData,
|
||||
products: ProductRepository,
|
||||
restrictions: PlanningRestrictions = PlanningRestrictions(),
|
||||
) -> Optional[Recommendation]:
|
||||
"""Return a solid-wall insulation Recommendation for an uninsulated, suitable
|
||||
main wall — its constructable EWI/IWI Options — else None."""
|
||||
main wall — its constructable EWI/IWI Options, minus any the Property's
|
||||
planning protections forbid — else None."""
|
||||
main = next(
|
||||
part
|
||||
for part in epc.sap_building_parts
|
||||
|
|
@ -108,8 +141,15 @@ def recommend_solid_wall(
|
|||
if not measure_types:
|
||||
return None
|
||||
|
||||
options = tuple(
|
||||
_solid_wall_option(epc, products, measure_type)
|
||||
allowed = tuple(
|
||||
measure_type
|
||||
for measure_type in measure_types
|
||||
if _allowed(measure_type, restrictions)
|
||||
)
|
||||
if not allowed:
|
||||
return None
|
||||
|
||||
options = tuple(
|
||||
_solid_wall_option(epc, products, measure_type) for measure_type in allowed
|
||||
)
|
||||
return Recommendation(surface="Main wall", options=options)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from domain.modelling.generators.roof_recommendation import recommend_loft_insul
|
|||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
from domain.modelling.generators.wall_recommendation import recommend_cavity_wall
|
||||
from domain.modelling.generators.solid_wall_recommendation import (
|
||||
PlanningRestrictions,
|
||||
recommend_solid_wall,
|
||||
)
|
||||
from domain.modelling.recommendation import MeasureOption
|
||||
|
|
@ -194,6 +195,49 @@ def test_timber_frame_generator_offers_iwi_only_pinning_its_after() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_conservation_area_drops_ewi_but_keeps_iwi() -> None:
|
||||
# Arrange — a conservation area blocks the external-appearance change only.
|
||||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"solid_brick_ewi_001431_before.pdf"
|
||||
)
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_solid_wall(
|
||||
before, _AnyProduct(), PlanningRestrictions(in_conservation_area=True)
|
||||
)
|
||||
|
||||
# Assert — IWI survives, EWI is gone.
|
||||
assert recommendation is not None
|
||||
assert {option.measure_type for option in recommendation.options} == {
|
||||
"internal_wall_insulation"
|
||||
}
|
||||
|
||||
|
||||
def test_listed_building_blocks_all_solid_wall_insulation() -> None:
|
||||
# Arrange — listed/heritage protect the fabric, so both EWI and IWI go.
|
||||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"solid_brick_ewi_001431_before.pdf"
|
||||
)
|
||||
|
||||
# Act / Assert
|
||||
assert (
|
||||
recommend_solid_wall(
|
||||
before, _AnyProduct(), PlanningRestrictions(is_listed=True)
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_cavity_wall_gets_no_solid_wall_recommendation() -> None:
|
||||
# Arrange — a cavity wall is handled by recommend_cavity_wall, never here.
|
||||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"cavity_wall_001431_before.pdf"
|
||||
)
|
||||
|
||||
# Act / Assert
|
||||
assert recommend_solid_wall(before, _AnyProduct()) is None
|
||||
|
||||
|
||||
def test_loft_overlay_reproduces_the_relodged_after() -> None:
|
||||
# Arrange
|
||||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue