Model/domain/modelling/generators/heating_recommendation.py
Khalim Conn-Kowlessar b3badc9b10 test(modelling): HHRSH before/after cascade pins (001431) at 1e-4
The same absolute-target HHR overlay reproduces the common relodged after from
two different base systems (existing electric storage; "no system present"
electric) — proving the bundle is a true whole-system end-state. Closes one
named gap the pin surfaced: the relodged HHR cylinder lodges
cylinder_thermostat='Y', so HeatingOverlay + _fold_heating + the HHRSH overlay
gain cylinder_thermostat (ΔSAP 0.065 -> <1e-4). ADR-0024.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 19:09:23 +00:00

104 lines
4.3 KiB
Python

"""The heating Recommendation Generator.
Detects a dwelling whose heating system should be replaced and emits one
"Heating & Hot Water" Recommendation of competing whole-system bundles — the
Optimiser picks at most one (ADR-0024). Each bundle is a whole-system change:
main heating + controls + fuel + meter + the implied hot water, folded into one
Measure Option's `HeatingOverlay`. Hot water is never a separate competing
measure; the legacy heating-vs-HW split double-counted.
This slice covers the high-heat-retention storage (HHRSH) bundle; the ASHP and
boiler bundles land in later slices. Detection + pricing only — impact is
produced by scoring (ADR-0016).
"""
from typing import Optional
from datatypes.epc.domain.epc_property_data import EpcPropertyData, MainHeatingDetail
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
from domain.modelling.simulation import EpcSimulation, HeatingOverlay
from repositories.product.product_repository import ProductRepository
_HEATING_SURFACE = "Heating & Hot Water"
_HHR_STORAGE_MEASURE_TYPE = "high_heat_retention_storage_heaters"
# Electricity main-fuel code (Elmhurst → SAP10 Table 12).
_ELECTRICITY_FUEL = 30
# Table 4a SAP main-heating code for high-heat-retention storage heaters; an
# existing HHR system lodges this already, so it is not re-recommended.
_HHR_STORAGE_SAP_CODE = 409
# RdSAP main_heating_category for a heat pump (Table 4a) — an existing heat pump
# is never downgraded to storage heaters.
_HEAT_PUMP_CATEGORY = 4
# The HHRSH bundle's absolute end-state (ADR-0024): high-heat-retention storage
# heaters (Table 4a code 409) on a dual off-peak meter, with an off-peak
# electric immersion hot-water cylinder. Pinned against the relodged after-cert
# in the cascade tests; `mains_gas` and the heat emitter are unchanged by this
# measure, so they are not written.
_HHR_STORAGE_OVERLAY = HeatingOverlay(
main_fuel_type=_ELECTRICITY_FUEL,
sap_main_heating_code=_HHR_STORAGE_SAP_CODE,
main_heating_control=2404,
water_heating_code=903,
water_heating_fuel=_ELECTRICITY_FUEL,
cylinder_size=2,
cylinder_insulation_type=1,
cylinder_insulation_thickness_mm=120,
cylinder_thermostat="Y",
has_hot_water_cylinder=True,
meter_type="Dual",
)
def recommend_heating(
epc: EpcPropertyData, products: ProductRepository
) -> Optional[Recommendation]:
"""Return a "Heating & Hot Water" Recommendation of competing whole-system
bundles for the dwelling, else None when no bundle is eligible."""
options: list[MeasureOption] = []
hhr_option = _hhr_storage_option(epc, products)
if hhr_option is not None:
options.append(hhr_option)
if not options:
return None
return Recommendation(surface=_HEATING_SURFACE, options=tuple(options))
def _hhr_storage_option(
epc: EpcPropertyData, products: ProductRepository
) -> Optional[MeasureOption]:
"""The high-heat-retention storage bundle, offered for an electrically-heated
(or off-gas) dwelling that is not already HHR or a heat pump."""
if not _hhr_storage_eligible(epc):
return None
product = products.get(_HHR_STORAGE_MEASURE_TYPE)
return MeasureOption(
measure_type=_HHR_STORAGE_MEASURE_TYPE,
description=(
"Replace the heating with high heat retention storage heaters on an "
"off-peak tariff, with an off-peak electric hot-water cylinder"
),
overlay=EpcSimulation(heating=_HHR_STORAGE_OVERLAY),
cost=Cost(
total=product.unit_cost_per_m2, contingency_rate=product.contingency_rate
),
material_id=product.id,
)
def _hhr_storage_eligible(epc: EpcPropertyData) -> bool:
"""HHR storage suits an electrically-heated or off-gas dwelling, unless it is
already HHR or a heat pump (translated from legacy `HeatingRecommender.
is_high_heat_retention_valid`, which keyed on description strings)."""
main: MainHeatingDetail = epc.sap_heating.main_heating_details[0]
if main.sap_main_heating_code == _HHR_STORAGE_SAP_CODE:
return False
if main.main_heating_category == _HEAT_PUMP_CATEGORY:
return False
off_gas: bool = not epc.sap_energy_source.mains_gas
electric_main: bool = main.main_fuel_type == _ELECTRICITY_FUEL
return electric_main or off_gas