From df38dda7b034a673872e2dc5f40593f470477cee Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 3 Jul 2026 10:26:35 +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=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../generators/heating_recommendation.py | 14 ++++++++++--- .../modelling/test_heating_recommendation.py | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/domain/modelling/generators/heating_recommendation.py b/domain/modelling/generators/heating_recommendation.py index f4f7375dc..4fc0b0736 100644 --- a/domain/modelling/generators/heating_recommendation.py +++ b/domain/modelling/generators/heating_recommendation.py @@ -335,17 +335,23 @@ def recommend_heating( products: ProductRepository, restrictions: PlanningRestrictions = PlanningRestrictions(), considered_measures: Optional[frozenset[MeasureType]] = None, + *, + design_heat_loss_kw: Optional[float] = None, ) -> Optional[Recommendation]: """Return a "Heating & Hot Water" Recommendation of competing whole-system bundles for the dwelling, else None when no bundle is eligible. ASHP is - additionally gated by the Property's planning protections (ADR-0024).""" + additionally gated by the Property's planning protections (ADR-0024). + + ``design_heat_loss_kw`` (the calculator's SAP design heat loss) sizes the + ASHP to the dwelling so SAP's Appendix-N PSR efficiency reads near its peak + (ADR-0049); when omitted, sizing falls back to the floor-area proxy.""" options: list[MeasureOption] = [] hhr_option = _hhr_storage_option(epc, products) if hhr_option is not None: options.append(hhr_option) - ashp_option = _ashp_option(epc, products, restrictions) + ashp_option = _ashp_option(epc, products, restrictions, design_heat_loss_kw) if ashp_option is not None: options.append(ashp_option) @@ -730,9 +736,11 @@ def _ashp_option( epc: EpcPropertyData, products: ProductRepository, restrictions: PlanningRestrictions, + design_heat_loss_kw: Optional[float] = None, ) -> Optional[MeasureOption]: """The air-source heat-pump bundle, offered for any non-flat house/bungalow - that is not listed/heritage and not already a heat pump.""" + that is not listed/heritage and not already a heat pump. The pump is sized to + ``design_heat_loss_kw`` (ADR-0049), falling back to the floor-area proxy.""" if not _ashp_eligible(epc, restrictions): return None # Cost is composed per-dwelling from the rate sheet (ADR-0025), not the diff --git a/tests/domain/modelling/test_heating_recommendation.py b/tests/domain/modelling/test_heating_recommendation.py index 51d41f035..e01a55c17 100644 --- a/tests/domain/modelling/test_heating_recommendation.py +++ b/tests/domain/modelling/test_heating_recommendation.py @@ -791,3 +791,24 @@ def test_ashp_sizing_caps_at_the_largest_pump_for_a_high_heat_loss_dwelling() -> # Assert — capped at the 12 kW rung, not left undersized at 5 kW. assert pcdb_id == 110281 + + +def test_ashp_bundle_sizes_the_pump_to_the_design_heat_loss() -> None: + # A high-heat-loss house (17.5 kW) must get a pump sized to it — the capped + # 12 kW aroTHERM plus (PCDB 110281) — so SAP's Appendix-N PSR efficiency reads + # near its peak, not the fixed 5 kW unit (110257) whose PSR collapses. + # Arrange + baseline: EpcPropertyData = _gas_boiler_house() + + # Act + recommendation: Recommendation | None = recommend_heating( + baseline, _StubProducts(), design_heat_loss_kw=17.5 + ) + + # Assert — the ASHP overlay carries the sized 12 kW record, not the fixed 5 kW. + assert recommendation is not None + ashp = next( + o for o in recommendation.options if o.measure_type == "air_source_heat_pump" + ) + assert ashp.overlay.heating is not None + assert ashp.overlay.heating.main_heating_index_number == 110281