The ASHP bundle sizes the heat pump to the dwelling's design heat loss 🟥

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-03 10:26:35 +00:00
parent 039df7ae87
commit df38dda7b0
2 changed files with 32 additions and 3 deletions

View file

@ -335,17 +335,23 @@ def recommend_heating(
products: ProductRepository, products: ProductRepository,
restrictions: PlanningRestrictions = PlanningRestrictions(), restrictions: PlanningRestrictions = PlanningRestrictions(),
considered_measures: Optional[frozenset[MeasureType]] = None, considered_measures: Optional[frozenset[MeasureType]] = None,
*,
design_heat_loss_kw: Optional[float] = None,
) -> Optional[Recommendation]: ) -> Optional[Recommendation]:
"""Return a "Heating & Hot Water" Recommendation of competing whole-system """Return a "Heating & Hot Water" Recommendation of competing whole-system
bundles for the dwelling, else None when no bundle is eligible. ASHP is 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] = [] options: list[MeasureOption] = []
hhr_option = _hhr_storage_option(epc, products) hhr_option = _hhr_storage_option(epc, products)
if hhr_option is not None: if hhr_option is not None:
options.append(hhr_option) 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: if ashp_option is not None:
options.append(ashp_option) options.append(ashp_option)
@ -730,9 +736,11 @@ def _ashp_option(
epc: EpcPropertyData, epc: EpcPropertyData,
products: ProductRepository, products: ProductRepository,
restrictions: PlanningRestrictions, restrictions: PlanningRestrictions,
design_heat_loss_kw: Optional[float] = None,
) -> Optional[MeasureOption]: ) -> Optional[MeasureOption]:
"""The air-source heat-pump bundle, offered for any non-flat house/bungalow """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): if not _ashp_eligible(epc, restrictions):
return None return None
# Cost is composed per-dwelling from the rate sheet (ADR-0025), not the # Cost is composed per-dwelling from the rate sheet (ADR-0025), not the

View file

@ -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 — capped at the 12 kW rung, not left undersized at 5 kW.
assert pcdb_id == 110281 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