From 2dd637a6571da5ae0523c08f6da1d1b395fa3221 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 3 Jul 2026 10:28:08 +0000 Subject: [PATCH] =?UTF-8?q?The=20ASHP=20bundle=20sizes=20the=20heat=20pump?= =?UTF-8?q?=20to=20the=20dwelling's=20design=20heat=20loss=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _ashp_option now selects the aroTHERM plus PCDB record matching the threaded design heat loss (floor-area proxy fallback), so the overlay's efficiency anchor is sized to the dwelling instead of a fixed 5 kW unit. Co-Authored-By: Claude Opus 4.8 --- .../modelling/generators/heating_recommendation.py | 13 ++++++++++++- .../domain/modelling/test_heating_recommendation.py | 6 ++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/domain/modelling/generators/heating_recommendation.py b/domain/modelling/generators/heating_recommendation.py index 4fc0b0736..cdadf50bf 100644 --- a/domain/modelling/generators/heating_recommendation.py +++ b/domain/modelling/generators/heating_recommendation.py @@ -12,6 +12,7 @@ boiler bundles land in later slices. Detection + pricing only — impact is produced by scoring (ADR-0016). """ +from dataclasses import replace from typing import Optional from datatypes.epc.domain.epc_property_data import EpcPropertyData, MainHeatingDetail @@ -743,6 +744,16 @@ def _ashp_option( ``design_heat_loss_kw`` (ADR-0049), falling back to the floor-area proxy.""" if not _ashp_eligible(epc, restrictions): return None + # Size the pump to the dwelling: the calculator's design heat loss when the + # caller threaded it, else the floor-area proxy (ADR-0049). + heat_loss_kw: float = ( + design_heat_loss_kw + if design_heat_loss_kw is not None + else epc.total_floor_area_m2 * _KW_PER_M2 + ) + sized_overlay: HeatingOverlay = replace( + _ASHP_OVERLAY, main_heating_index_number=select_ashp_pcdb_id(heat_loss_kw) + ) # Cost is composed per-dwelling from the rate sheet (ADR-0025), not the # single catalogue scalar; the catalogue row is read only for its id, so an # absent ASHP row must not suppress the bundle — it just carries no id. @@ -754,7 +765,7 @@ def _ashp_option( "Replace the heating with an air-source heat pump, time-and-" "temperature-zone controls and a heat-pump hot-water cylinder" ), - overlay=EpcSimulation(heating=_ASHP_OVERLAY), + overlay=EpcSimulation(heating=sized_overlay), cost=cost, material_id=product.id if product is not None else None, ) diff --git a/tests/domain/modelling/test_heating_recommendation.py b/tests/domain/modelling/test_heating_recommendation.py index e01a55c17..2477150ef 100644 --- a/tests/domain/modelling/test_heating_recommendation.py +++ b/tests/domain/modelling/test_heating_recommendation.py @@ -203,8 +203,10 @@ def test_gas_boiler_house_yields_an_ashp_bundle() -> None: # bungalow regardless of current system or efficiency (ADR-0024). baseline: EpcPropertyData = _gas_boiler_house() - # Act - recommendation: Recommendation | None = recommend_heating(baseline, _StubProducts()) + # Act — a ~4 kW design heat loss sizes to the 5 kW aroTHERM plus (110257). + recommendation: Recommendation | None = recommend_heating( + baseline, _StubProducts(), design_heat_loss_kw=4.0 + ) # Assert — the ASHP bundle carries the absolute heat-pump end-state. assert recommendation is not None