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