From 7648032d730c5ad575e5b425f272358d54db8c98 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 4 Jun 2026 16:15:56 +0000 Subject: [PATCH] feat(modelling): wire solid-wall insulation into the candidate pool Slice 2e. recommend_solid_wall joins the orchestrator's fabric generator pool (restrictions default unrestricted until slice 3 sources them); the harness catalogue + contingencies (26%) gain external_wall_insulation / internal_wall_insulation. run_modelling on an uninsulated solid-brick dwelling (baseline SAP 36.6) now selects internal wall insulation into the optimised package; the catalogue-completeness guard covers both new measure types. Golden cohort 57/57 still error-free; IWI now fires on a real cohort cert. Co-Authored-By: Claude Opus 4.8 --- domain/modelling/contingencies.py | 2 ++ harness/sample_catalogue.json | 4 +++- orchestration/modelling_orchestrator.py | 2 ++ tests/harness/test_console.py | 23 ++++++++++++++++++++++- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/domain/modelling/contingencies.py b/domain/modelling/contingencies.py index d1e21357..a184534d 100644 --- a/domain/modelling/contingencies.py +++ b/domain/modelling/contingencies.py @@ -11,6 +11,8 @@ _CONTINGENCY_RATES: dict[str, float] = { "suspended_floor_insulation": 0.20, "solid_floor_insulation": 0.26, "mechanical_ventilation": 0.26, + "external_wall_insulation": 0.26, + "internal_wall_insulation": 0.26, } diff --git a/harness/sample_catalogue.json b/harness/sample_catalogue.json index f3cb49c2..4d2851a2 100644 --- a/harness/sample_catalogue.json +++ b/harness/sample_catalogue.json @@ -3,5 +3,7 @@ "loft_insulation": { "unit_cost_per_m2": 12.0 }, "suspended_floor_insulation": { "unit_cost_per_m2": 25.0 }, "solid_floor_insulation": { "unit_cost_per_m2": 45.0 }, - "mechanical_ventilation": { "unit_cost_per_m2": 450.0 } + "mechanical_ventilation": { "unit_cost_per_m2": 450.0 }, + "external_wall_insulation": { "unit_cost_per_m2": 100.0 }, + "internal_wall_insulation": { "unit_cost_per_m2": 90.0 } } diff --git a/orchestration/modelling_orchestrator.py b/orchestration/modelling_orchestrator.py index e7a336e1..6b8d4cdc 100644 --- a/orchestration/modelling_orchestrator.py +++ b/orchestration/modelling_orchestrator.py @@ -27,6 +27,7 @@ from domain.modelling.scoring.scoring import ( marginals_from_scores, ) from domain.modelling.generators.wall_recommendation import recommend_cavity_wall +from domain.modelling.generators.solid_wall_recommendation import recommend_solid_wall from domain.sap10_calculator.calculator import SapCalculator from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository from repositories.product.product_repository import ProductRepository @@ -194,6 +195,7 @@ def _candidate_recommendations( """Run every fabric Recommendation Generator; keep the ones that apply.""" generators = ( recommend_cavity_wall, + recommend_solid_wall, recommend_loft_insulation, recommend_floor_insulation, ) diff --git a/tests/harness/test_console.py b/tests/harness/test_console.py index 1de1a345..73109d40 100644 --- a/tests/harness/test_console.py +++ b/tests/harness/test_console.py @@ -10,14 +10,19 @@ from datatypes.epc.domain.epc import Epc from datatypes.epc.domain.epc_property_data import EpcPropertyData 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 ( + parse_recommendation_summary, +) from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import ( build_epc as _build_uninsulated_cavity_and_floor_epc, ) -# Every Measure Type the four fabric generators can emit; the harness catalogue +# Every Measure Type the fabric generators can emit; the harness catalogue # must price all of them or an offline run raises mid-pipeline. _GENERATOR_MEASURE_TYPES = ( "cavity_wall_insulation", + "external_wall_insulation", + "internal_wall_insulation", "loft_insulation", "suspended_floor_insulation", "solid_floor_insulation", @@ -65,6 +70,22 @@ def test_run_modelling_inspects_a_plan_without_baseline_or_lodged_performance() assert len(plan.measures) >= 1 +def test_run_modelling_recommends_solid_wall_insulation_for_solid_brick() -> None: + # Arrange — an uninsulated solid-brick dwelling (cert 001431 before), + # which has no cavity to fill, so any wall measure must be solid-wall. + epc: EpcPropertyData = parse_recommendation_summary( + "solid_brick_ewi_001431_before.pdf" + ) + + # Act — Modelling only, no database. + plan = run_modelling(epc, goal_band="C", print_table=False) + + # Assert — the solid-wall generator is wired into the candidate pool, so a + # solid-wall Option reaches the optimised package. + measure_types = {measure.measure_type for measure in plan.measures} + assert 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)