From a9173bd4928979cef2e9a8fbb540b6fc410371c4 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Jul 2026 11:44:31 +0000 Subject: [PATCH] =?UTF-8?q?A=20Fabric=20First=20Scenario=20spends=20the=20?= =?UTF-8?q?budget=20on=20the=20envelope=20before=20heating=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../test_modelling_fabric_first.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/orchestration/test_modelling_fabric_first.py diff --git a/tests/orchestration/test_modelling_fabric_first.py b/tests/orchestration/test_modelling_fabric_first.py new file mode 100644 index 000000000..86d5e7746 --- /dev/null +++ b/tests/orchestration/test_modelling_fabric_first.py @@ -0,0 +1,60 @@ +"""The ModellingOrchestrator honours a Fabric First Scenario: when +``scenario.fabric_first`` is set, the Optimiser treats the building envelope +with the full budget before heating / renewables are considered, so a budget +the plain optimiser would spend on a heating system is spent on fabric +instead. End-to-end through ``run_modelling`` (no database) with the real +calculator, against the uninsulated solid-brick 001431 dwelling whose plain +plan is heating-led. +""" + +from __future__ import annotations + +from datatypes.epc.domain.epc_property_data import EpcPropertyData +from domain.modelling.measure_type import FABRIC_MEASURE_TYPES, MeasureType +from domain.modelling.plan import Plan +from domain.modelling.scenario import Scenario +from harness.console import run_modelling +from tests.domain.modelling._elmhurst_recommendation import ( + parse_recommendation_summary, +) + +_HEATING_MEASURES: frozenset[MeasureType] = frozenset( + { + MeasureType.GAS_BOILER_UPGRADE, + MeasureType.AIR_SOURCE_HEAT_PUMP, + MeasureType.HIGH_HEAT_RETENTION_STORAGE_HEATERS, + MeasureType.SOLAR_PV, + } +) + + +def _fabric_first_scenario(*, budget: float) -> Scenario: + return Scenario( + id=999, + goal="Increasing EPC", + goal_value="C", + budget=budget, + is_default=True, + fabric_first=True, + ) + + +def test_fabric_first_scenario_spends_the_budget_on_fabric_before_heating() -> None: + # Arrange — uninsulated solid brick: at a £4000 budget the plain optimiser + # buys the £3200 gas boiler (the cheapest route toward band C). Fabric + # first must commit the envelope work first, after which the boiler no + # longer fits the leftover budget. + epc: EpcPropertyData = parse_recommendation_summary( + "solid_brick_ewi_001431_before.pdf" + ) + + # Act + plan: Plan = run_modelling( + epc, scenario=_fabric_first_scenario(budget=4000.0), print_table=False + ) + + # Assert — the plan is fabric-led: at least one envelope measure, and none + # of the budget leaked to a heating system or renewables. + selected = {measure.measure_type for measure in plan.measures} + assert selected & FABRIC_MEASURE_TYPES + assert not selected & _HEATING_MEASURES