From b55ab3727f4e2fad9cfab9c9675ae526f07a01c1 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 5 Jun 2026 19:22:50 +0000 Subject: [PATCH] feat(modelling): wire the HHR storage bundle into the candidate pool recommend_heating joins the free candidate pool in _candidate_recommendations; the HHR storage bundle reaches the optimised package for an electric/off-gas dwelling. Catalogue + contingency (legacy 0.10) gain high_heat_retention_storage_heaters; report.py _triggers_for explains the heating trigger (electric/off-gas main); the harness _GENERATOR_MEASURE_TYPES forcing test covers it. ASHP + boiler bundles still to come. ADR-0024. Co-Authored-By: Claude Opus 4.8 --- domain/modelling/contingencies.py | 1 + harness/report.py | 10 ++++++++++ harness/sample_catalogue.json | 3 ++- orchestration/modelling_orchestrator.py | 2 ++ tests/harness/test_console.py | 23 +++++++++++++++++++++++ 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/domain/modelling/contingencies.py b/domain/modelling/contingencies.py index 2854d231..8d011ffb 100644 --- a/domain/modelling/contingencies.py +++ b/domain/modelling/contingencies.py @@ -18,6 +18,7 @@ _CONTINGENCY_RATES: dict[str, float] = { "double_glazing": 0.15, "secondary_glazing": 0.15, "low_energy_lighting": 0.26, + "high_heat_retention_storage_heaters": 0.10, } diff --git a/harness/report.py b/harness/report.py index 443f27a5..f9459bc4 100644 --- a/harness/report.py +++ b/harness/report.py @@ -134,6 +134,16 @@ def _triggers_for(epc: EpcPropertyData, measure_type: str) -> dict[str, Any]: epc.low_energy_fixed_lighting_bulbs_count ), } + if measure_type == "high_heat_retention_storage_heaters": + # heating_recommendation.py offers HHR storage to an electrically-heated + # or off-gas dwelling (translated from legacy is_high_heat_retention_valid). + return { + "main_fuel_type": epc.sap_heating.main_heating_details[0].main_fuel_type, + "sap_main_heating_code": ( + epc.sap_heating.main_heating_details[0].sap_main_heating_code + ), + "mains_gas": epc.sap_energy_source.mains_gas, + } return {} diff --git a/harness/sample_catalogue.json b/harness/sample_catalogue.json index c9ea14ab..fb34e28b 100644 --- a/harness/sample_catalogue.json +++ b/harness/sample_catalogue.json @@ -10,5 +10,6 @@ "internal_wall_insulation": { "unit_cost_per_m2": 90.0 }, "double_glazing": { "unit_cost_per_m2": 600.0 }, "secondary_glazing": { "unit_cost_per_m2": 510.0 }, - "low_energy_lighting": { "unit_cost_per_m2": 8.0 } + "low_energy_lighting": { "unit_cost_per_m2": 8.0 }, + "high_heat_retention_storage_heaters": { "unit_cost_per_m2": 3500.0 } } diff --git a/orchestration/modelling_orchestrator.py b/orchestration/modelling_orchestrator.py index f193b771..df375f65 100644 --- a/orchestration/modelling_orchestrator.py +++ b/orchestration/modelling_orchestrator.py @@ -30,6 +30,7 @@ from domain.modelling.generators.wall_recommendation import recommend_cavity_wal from domain.modelling.generators.solid_wall_recommendation import recommend_solid_wall from domain.modelling.generators.glazing_recommendation import recommend_glazing from domain.modelling.generators.lighting_recommendation import recommend_lighting +from domain.modelling.generators.heating_recommendation import recommend_heating from domain.geospatial.planning_restrictions import PlanningRestrictions from domain.sap10_calculator.calculator import SapCalculator from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository @@ -209,6 +210,7 @@ def _candidate_recommendations( recommend_floor_insulation(effective_epc, products), recommend_glazing(effective_epc, products, planning_restrictions), recommend_lighting(effective_epc, products), + recommend_heating(effective_epc, products), ) return [recommendation for recommendation in found if recommendation is not None] diff --git a/tests/harness/test_console.py b/tests/harness/test_console.py index b8cac0f9..2f1d6bd5 100644 --- a/tests/harness/test_console.py +++ b/tests/harness/test_console.py @@ -34,6 +34,7 @@ _GENERATOR_MEASURE_TYPES = ( "double_glazing", "secondary_glazing", "low_energy_lighting", + "high_heat_retention_storage_heaters", ) @@ -179,6 +180,28 @@ def test_run_modelling_recommends_low_energy_lighting_for_non_led_bulbs() -> Non assert "double_glazing" not in measure_types +def _electric_storage_lit_epc() -> EpcPropertyData: + epc: EpcPropertyData = _build_uninsulated_cavity_and_floor_epc() + main = epc.sap_heating.main_heating_details[0] + main.main_fuel_type = 30 + main.sap_main_heating_code = 402 + main.main_heating_control = 2401 + return epc + + +def test_run_modelling_recommends_hhr_storage_for_an_electric_dwelling() -> None: + # Arrange — an electrically-heated dwelling on old storage heaters; the + # heating generator is wired into the candidate pool (ADR-0024). + epc: EpcPropertyData = _electric_storage_lit_epc() + + # Act — Modelling only, no database. + plan = run_modelling(epc, goal_band="C", print_table=False) + + # Assert — the HHR storage bundle reaches the optimised package. + measure_types = {measure.measure_type for measure in plan.measures} + assert "high_heat_retention_storage_heaters" in measure_types + + def test_sample_catalogue_prices_every_generator_measure_type() -> None: # Arrange — the default offline catalogue. products: ProductJsonRepository = ProductJsonRepository(DEFAULT_CATALOGUE)