Model/domain/modelling/generators/secondary_heating_recommendation.py
Khalim Conn-Kowlessar ae7959f57c feat(modelling): secondary-heating-removal generator + MeasureType (ADR-0028)
recommend_secondary_heating_removal offers one standalone Option that clears the
lodged secondary system. Eligibility is purely physical (offer iff
sap_heating.secondary_heating_type is set) — no effectiveness gate, since a
lodged secondary is a fixed emitter per RdSAP (portables are ignored), and the
electric-storage §A.2.2 no-op is the Optimiser's call (ADR-0028 decisions 1-2).
Priced at a flat per-dwelling decommission cost, not room-scaled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 13:35:14 +00:00

57 lines
2.6 KiB
Python

"""The Secondary Heating Removal Recommendation Generator (ADR-0028).
Offers to strip a dwelling's lodged secondary heating system so the main system
serves 100% of space heating. A **standalone, co-selectable** Recommendation —
not an Option in the Heating & Hot Water rec — because removing a secondary
heater is independent of (and combinable with) a tune-up or boiler upgrade.
Eligibility is purely physical: offered **iff a secondary is lodged**
(`sap_heating.secondary_heating_type` is set). RdSAP only records a secondary
when a *fixed* emitter is present (portable plug-in heaters are ignored), so a
lodged secondary is by definition a fixed unit worth removing. There is no
effectiveness gate — on an electric-storage main RdSAP §A.2.2 forces a default
secondary back, making removal a no-op, but that is the Optimiser's call (it
owns the economics), not eligibility's. Detection + pricing only; impact is
produced later by scoring (ADR-0016).
Priced at a flat per-dwelling decommission cost (one electrician visit to
disconnect a fixed/hard-wired heater + localised making-good), not scaled by
room count — the EPC lodges one secondary system with no heater count (ADR-0028).
"""
from typing import Final, Optional
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.modelling.measure_type import MeasureType
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
from domain.modelling.simulation import EpcSimulation, SecondaryHeatingOverlay
from repositories.product.product_repository import ProductRepository
_SECONDARY_HEATING_REMOVAL_MEASURE_TYPE: Final[MeasureType] = (
MeasureType.SECONDARY_HEATING_REMOVAL
)
def recommend_secondary_heating_removal(
epc: EpcPropertyData, products: ProductRepository
) -> Optional[Recommendation]:
"""Return a Secondary Heating Removal Recommendation — its single Option
clears the lodged secondary system — else None when no secondary is lodged
(nothing physical to remove)."""
if epc.sap_heating.secondary_heating_type is None:
return None
product = products.get(_SECONDARY_HEATING_REMOVAL_MEASURE_TYPE)
overlay = EpcSimulation(secondary_heating=SecondaryHeatingOverlay())
cost = Cost(
total=product.unit_cost_per_m2,
contingency_rate=product.contingency_rate,
)
option = MeasureOption(
measure_type=_SECONDARY_HEATING_REMOVAL_MEASURE_TYPE,
description="Remove the secondary heating system",
overlay=overlay,
cost=cost,
material_id=product.id,
)
return Recommendation(surface="Secondary Heating", options=(option,))