From 6b64104dc50e1a62b142a6ede602d36cde11b756 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Jul 2026 12:54:46 +0000 Subject: [PATCH] =?UTF-8?q?An=20Energy-Savings=20scenario=20prices=20packa?= =?UTF-8?q?ges=20at=20the=20live=20fuel=20rates=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 --- harness/console.py | 4 +- .../test_modelling_goal_objectives.py | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/harness/console.py b/harness/console.py index a9aa222b5..06c16d81e 100644 --- a/harness/console.py +++ b/harness/console.py @@ -39,6 +39,7 @@ from orchestration.modelling_orchestrator import ( _candidate_recommendations, # pyright: ignore[reportPrivateUsage] ) from orchestration.property_baseline_orchestrator import PropertyBaselineOrchestrator +from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository from repositories.fuel_rates.fuel_rates_static_file_repository import ( FuelRatesStaticFileRepository, ) @@ -182,6 +183,7 @@ def run_modelling( considered_measures: Optional[frozenset[MeasureType]] = None, products: Optional[ProductRepository] = None, scenario: Optional[Scenario] = None, + fuel_rates: Optional[FuelRatesRepository] = None, print_table: bool = True, ) -> Plan: """Run ONLY the Modelling stage over ``epc`` with no database — skipping @@ -240,7 +242,7 @@ def run_modelling( ModellingOrchestrator( unit_of_work=lambda: unit, calculator=Sap10Calculator(), - fuel_rates=FuelRatesStaticFileRepository(), + fuel_rates=fuel_rates or FuelRatesStaticFileRepository(), ).run( property_ids=[_PROPERTY_ID], scenario_ids=[scenario_id], diff --git a/tests/orchestration/test_modelling_goal_objectives.py b/tests/orchestration/test_modelling_goal_objectives.py index 962c94e24..ac7492085 100644 --- a/tests/orchestration/test_modelling_goal_objectives.py +++ b/tests/orchestration/test_modelling_goal_objectives.py @@ -9,11 +9,19 @@ carbon-optimal packages diverge at a £16,000 budget. from __future__ import annotations +import dataclasses + from datatypes.epc.domain.epc_property_data import EpcPropertyData +from domain.fuel_rates.fuel import Fuel +from domain.fuel_rates.fuel_rates import FuelRate, FuelRates from domain.modelling.measure_type import MeasureType from domain.modelling.plan import Plan from domain.modelling.scenario import Scenario from harness.console import run_modelling +from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository +from repositories.fuel_rates.fuel_rates_static_file_repository import ( + FuelRatesStaticFileRepository, +) from tests.domain.modelling._elmhurst_recommendation import ( parse_recommendation_summary, ) @@ -57,3 +65,48 @@ def test_reducing_co2_scenario_buys_carbon_not_sap() -> None: ) selected = {measure.measure_type for measure in carbon_led.measures} assert MeasureType.GAS_BOILER_UPGRADE not in selected + + +class _FixedFuelRates(FuelRatesRepository): + def __init__(self, rates: FuelRates) -> None: + self._rates = rates + + def get_current(self) -> FuelRates: + return self._rates + + +def _cheap_electricity_snapshot() -> FuelRates: + """The committed snapshot with electricity at 1p/kWh — a world where any + electric heating out-bills gas, while SAP's internal price book (which the + calculator rates against) is unmoved.""" + base = FuelRatesStaticFileRepository().get_current() + rates = dict(base.rates) + rates[Fuel.ELECTRICITY] = FuelRate( + unit_rate_p_per_kwh=1.0, + standing_charge_p_per_day=rates[Fuel.ELECTRICITY].standing_charge_p_per_day, + ) + return dataclasses.replace(base, rates=rates) + + +def test_energy_savings_scenario_prices_packages_at_the_live_fuel_rates() -> None: + # Arrange — SAP is itself a cost metric, but it prices energy from its + # internal tariff book. The Energy Savings goal must price at the *live* + # Fuel Rates snapshot: with 1p/kWh electricity, electric heating slashes + # the bill even though SAP still scores the gas boiler package higher. + epc = _solid_brick_dwelling() + + # Act + plan: Plan = run_modelling( + epc, + scenario=_scenario("Energy Savings", budget=16000.0), + fuel_rates=_FixedFuelRates(_cheap_electricity_snapshot()), + print_table=False, + ) + + # Assert — the bill objective abandons the boiler for electric heating. + selected = {measure.measure_type for measure in plan.measures} + assert MeasureType.GAS_BOILER_UPGRADE not in selected + assert selected & { + MeasureType.AIR_SOURCE_HEAT_PUMP, + MeasureType.HIGH_HEAT_RETENTION_STORAGE_HEATERS, + }