mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(modelling): thread Property planning restrictions to the solid-wall gate
Slice 3b+3d (ADR-0019/0020). Property gains a planning_restrictions attribute (default unrestricted); the ModellingOrchestrator threads it from the Property through _plan_for -> _scored_candidate_groups -> _candidate_recommendations into recommend_solid_wall, replacing the unrestricted default. run_modelling exposes a planning_restrictions param so the offline harness can inspect restricted properties. Integration test: a listed solid-brick dwelling that gets IWI when unrestricted now yields no wall insulation. 145 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dab2e759bf
commit
c5182627ba
4 changed files with 49 additions and 10 deletions
|
|
@ -4,6 +4,7 @@ from dataclasses import dataclass
|
|||
from typing import Literal, Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.property.site_notes import SiteNotes
|
||||
|
||||
SourcePath = Literal["site_notes", "epc_with_overlay"]
|
||||
|
|
@ -40,6 +41,9 @@ class Property:
|
|||
# The current open-market value (a Property Valuation) — externally sourced
|
||||
# and mostly absent; feeds the Plan's Valuation Uplift £ forms (ADR-0018).
|
||||
current_market_value: Optional[float] = None
|
||||
# Planning protections resolved from the geospatial layer (ADR-0020); gate
|
||||
# wall insulation (ADR-0019). Defaults to unrestricted when unknown.
|
||||
planning_restrictions: PlanningRestrictions = PlanningRestrictions()
|
||||
|
||||
@property
|
||||
def source_path(self) -> SourcePath:
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from typing import Any, Optional
|
|||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.geospatial.coordinates import Coordinates
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.modelling.plan import Plan
|
||||
from domain.modelling.scenario import Scenario
|
||||
from domain.property.property import Property, PropertyIdentity
|
||||
|
|
@ -167,6 +168,7 @@ def run_modelling(
|
|||
goal_band: str = "C",
|
||||
catalogue_path: Path = DEFAULT_CATALOGUE,
|
||||
current_market_value: Optional[float] = None,
|
||||
planning_restrictions: PlanningRestrictions = PlanningRestrictions(),
|
||||
print_table: bool = True,
|
||||
) -> Plan:
|
||||
"""Run ONLY the Modelling stage over ``epc`` with no database — skipping
|
||||
|
|
@ -186,6 +188,7 @@ def run_modelling(
|
|||
),
|
||||
epc=epc,
|
||||
current_market_value=current_market_value,
|
||||
planning_restrictions=planning_restrictions,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from domain.modelling.scoring.scoring import (
|
|||
)
|
||||
from domain.modelling.generators.wall_recommendation import recommend_cavity_wall
|
||||
from domain.modelling.generators.solid_wall_recommendation import recommend_solid_wall
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.sap10_calculator.calculator import SapCalculator
|
||||
from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
|
@ -104,6 +105,7 @@ class ModellingOrchestrator:
|
|||
uow.product,
|
||||
scenario,
|
||||
current_market_value=prop.current_market_value,
|
||||
planning_restrictions=prop.planning_restrictions,
|
||||
)
|
||||
uow.plan.save(
|
||||
plan,
|
||||
|
|
@ -123,11 +125,12 @@ class ModellingOrchestrator:
|
|||
scenario: Scenario,
|
||||
*,
|
||||
current_market_value: Optional[float],
|
||||
planning_restrictions: PlanningRestrictions,
|
||||
) -> Plan:
|
||||
"""Generate → score → optimise → re-score/repair → attribute → bill →
|
||||
assemble the Plan for one Property + Scenario."""
|
||||
groups: list[list[ScoredOption]] = _scored_candidate_groups(
|
||||
scorer, effective_epc, products
|
||||
scorer, effective_epc, products, planning_restrictions
|
||||
)
|
||||
# Forced Measure Dependencies (ventilation) are excluded from the pool
|
||||
# but injected into the package before the re-score (ADR-0016).
|
||||
|
|
@ -190,16 +193,19 @@ def _bill_for(bill_derivation: BillDerivation, score: Score) -> Bill:
|
|||
|
||||
|
||||
def _candidate_recommendations(
|
||||
effective_epc: EpcPropertyData, products: ProductRepository
|
||||
effective_epc: EpcPropertyData,
|
||||
products: ProductRepository,
|
||||
planning_restrictions: PlanningRestrictions,
|
||||
) -> list[Recommendation]:
|
||||
"""Run every fabric Recommendation Generator; keep the ones that apply."""
|
||||
generators = (
|
||||
recommend_cavity_wall,
|
||||
recommend_solid_wall,
|
||||
recommend_loft_insulation,
|
||||
recommend_floor_insulation,
|
||||
"""Run every fabric Recommendation Generator; keep the ones that apply.
|
||||
Solid-wall insulation is additionally gated by the Property's planning
|
||||
protections (ADR-0019)."""
|
||||
found = (
|
||||
recommend_cavity_wall(effective_epc, products),
|
||||
recommend_solid_wall(effective_epc, products, planning_restrictions),
|
||||
recommend_loft_insulation(effective_epc, products),
|
||||
recommend_floor_insulation(effective_epc, products),
|
||||
)
|
||||
found = (generator(effective_epc, products) for generator in generators)
|
||||
return [recommendation for recommendation in found if recommendation is not None]
|
||||
|
||||
|
||||
|
|
@ -219,11 +225,14 @@ def _scored_candidate_groups(
|
|||
scorer: PackageScorer,
|
||||
effective_epc: EpcPropertyData,
|
||||
products: ProductRepository,
|
||||
planning_restrictions: PlanningRestrictions,
|
||||
) -> list[list[ScoredOption]]:
|
||||
"""One group per Recommendation: each Option scored independently against
|
||||
the baseline (role-1 warm-start signal, ADR-0016)."""
|
||||
groups: list[list[ScoredOption]] = []
|
||||
for recommendation in _candidate_recommendations(effective_epc, products):
|
||||
for recommendation in _candidate_recommendations(
|
||||
effective_epc, products, planning_restrictions
|
||||
):
|
||||
options = list(recommendation.options)
|
||||
impacts: list[MeasureImpact] = independent_option_impacts(
|
||||
scorer, effective_epc, options
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import pytest
|
|||
|
||||
from datatypes.epc.domain.epc import Epc
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from harness.console import DEFAULT_CATALOGUE, run_modelling, run_one
|
||||
from repositories.product.product_json_repository import ProductJsonRepository
|
||||
from tests.domain.modelling._elmhurst_recommendation import (
|
||||
|
|
@ -86,6 +87,28 @@ def test_run_modelling_recommends_solid_wall_insulation_for_solid_brick() -> Non
|
|||
assert measure_types & {"external_wall_insulation", "internal_wall_insulation"}
|
||||
|
||||
|
||||
def test_run_modelling_listed_building_yields_no_wall_insulation() -> None:
|
||||
# Arrange — the same uninsulated solid-brick dwelling that gets IWI when
|
||||
# unrestricted; listing it protects the fabric, blocking both EWI and IWI.
|
||||
epc: EpcPropertyData = parse_recommendation_summary(
|
||||
"solid_brick_ewi_001431_before.pdf"
|
||||
)
|
||||
|
||||
# Act — thread a listed-building restriction through to the generator.
|
||||
plan = run_modelling(
|
||||
epc,
|
||||
goal_band="C",
|
||||
print_table=False,
|
||||
planning_restrictions=PlanningRestrictions(is_listed=True),
|
||||
)
|
||||
|
||||
# Assert — no wall-insulation measure survives the restriction.
|
||||
measure_types = {measure.measure_type for measure in plan.measures}
|
||||
assert not (
|
||||
measure_types & {"external_wall_insulation", "internal_wall_insulation"}
|
||||
)
|
||||
|
||||
|
||||
def test_sample_catalogue_prices_every_generator_measure_type() -> None:
|
||||
# Arrange — the default offline catalogue.
|
||||
products: ProductJsonRepository = ProductJsonRepository(DEFAULT_CATALOGUE)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue