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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-05 19:22:50 +00:00
parent b3badc9b10
commit b55ab3727f
5 changed files with 38 additions and 1 deletions

View file

@ -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,
}

View file

@ -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 {}

View file

@ -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 }
}

View file

@ -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]

View file

@ -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)