mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
ASHP sizing targets the Appendix-N efficiency peak (realistic installed pump) 🟩
Sizing selects the aroTHERM plus rung nearest PSR ~0.8 (0.8 x design heat loss) — the Appendix-N efficiency peak, reproducing the pump a real installer fits — rather than the MCS PSR>=1.0 capacity target, which oversizes. Validated against the relodged Elmhurst ASHP cert (delta 0 held at the 5 kW Vaillant); the two self-snapshot pins re-pin to their correctly-sized larger pumps, and the orchestrator/harness thread the calculator's design heat loss so production sizes to the dwelling. Manual SapResult test stubs carry the new design_heat_loss_kw field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
cb91954bb4
commit
bf240e3315
8 changed files with 77 additions and 27 deletions
|
|
@ -153,17 +153,25 @@ _ASHP_SIZING_LADDER: tuple[tuple[float, int], ...] = (
|
|||
(7.93, 110273), # aroTHERM plus 10 kW
|
||||
(11.48, 110281), # aroTHERM plus 12 kW
|
||||
)
|
||||
# SAP 10.2 Appendix-N heat-pump efficiency peaks around PSR 0.8 (output / design
|
||||
# heat loss); sizing to it reproduces real installer choices (accredited-cert
|
||||
# validated) rather than the MCS capacity target (PSR 1.0), which oversizes.
|
||||
_EFFICIENCY_PEAK_PSR: float = 0.8
|
||||
|
||||
|
||||
def select_ashp_pcdb_id(design_heat_loss_kw: float) -> int:
|
||||
"""The PCDB heat-pump record to install for a dwelling with
|
||||
`design_heat_loss_kw`: the smallest ladder rung whose rated output meets the
|
||||
load (PSR >= 1.0), capped at the largest rung when the load exceeds the
|
||||
domestic aroTHERM plus range (ADR-0049)."""
|
||||
for max_output_kw, pcdb_id in _ASHP_SIZING_LADDER:
|
||||
if max_output_kw >= design_heat_loss_kw:
|
||||
return pcdb_id
|
||||
return _ASHP_SIZING_LADDER[-1][1]
|
||||
`design_heat_loss_kw` (ADR-0049): the ladder rung whose rated output sits
|
||||
nearest the SAP 10.2 Appendix-N efficiency peak — PSR ~= 0.8, i.e. a rated
|
||||
output of ``0.8 x design_heat_loss`` — which reproduces the pump a real
|
||||
installer fits (validated against the relodged Elmhurst ASHP cert) and keeps
|
||||
the heat pump clear of the low-PSR efficiency collapse. A load beyond the
|
||||
domestic aroTHERM plus range naturally selects the largest rung."""
|
||||
target_output_kw: float = _EFFICIENCY_PEAK_PSR * design_heat_loss_kw
|
||||
return min(
|
||||
_ASHP_SIZING_LADDER,
|
||||
key=lambda rung: abs(rung[0] - target_output_kw),
|
||||
)[1]
|
||||
|
||||
|
||||
# --- Gas boiler upgrade (Heating/HW expansion): replace an existing wet boiler
|
||||
|
|
|
|||
|
|
@ -273,10 +273,13 @@ def candidate_recommendations(
|
|||
if solar_insights is not None and "solarPotential" in solar_insights
|
||||
else None
|
||||
)
|
||||
# The SAP design heat loss sizes the ASHP to the dwelling (ADR-0049).
|
||||
design_heat_loss_kw: float = Sap10Calculator().calculate(epc).design_heat_loss_kw
|
||||
return _candidate_recommendations(
|
||||
epc,
|
||||
products or ProductJsonRepository(catalogue_path),
|
||||
planning_restrictions,
|
||||
solar_potential,
|
||||
considered_measures,
|
||||
design_heat_loss_kw,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -332,7 +332,13 @@ def _candidate_recommendations(
|
|||
MeasureType.SYSTEM_TUNE_UP,
|
||||
MeasureType.SYSTEM_TUNE_UP_ZONED,
|
||||
),
|
||||
lambda: recommend_heating(effective_epc, products, planning_restrictions, considered_measures),
|
||||
lambda: recommend_heating(
|
||||
effective_epc,
|
||||
products,
|
||||
planning_restrictions,
|
||||
considered_measures,
|
||||
design_heat_loss_kw=design_heat_loss_kw,
|
||||
),
|
||||
),
|
||||
(
|
||||
admitted(MeasureType.SECONDARY_HEATING_REMOVAL),
|
||||
|
|
@ -385,9 +391,20 @@ def _scored_candidate_groups(
|
|||
) -> list[list[ScoredOption]]:
|
||||
"""One group per Recommendation: each Option scored independently against
|
||||
the baseline (role-1 warm-start signal, ADR-0016)."""
|
||||
# The SAP design heat loss sizes the ASHP to the dwelling (ADR-0049); read it
|
||||
# off a baseline score, which the group scoring computes anyway.
|
||||
baseline_result = scorer.score(effective_epc, []).sap_result
|
||||
design_heat_loss_kw: Optional[float] = (
|
||||
baseline_result.design_heat_loss_kw if baseline_result is not None else None
|
||||
)
|
||||
groups: list[list[ScoredOption]] = []
|
||||
for recommendation in _candidate_recommendations(
|
||||
effective_epc, products, planning_restrictions, solar_potential, considered_measures
|
||||
effective_epc,
|
||||
products,
|
||||
planning_restrictions,
|
||||
solar_potential,
|
||||
considered_measures,
|
||||
design_heat_loss_kw,
|
||||
):
|
||||
options = list(recommendation.options)
|
||||
impacts: list[MeasureImpact] = independent_option_impacts(
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ def _sap_result(
|
|||
space_heating_kwh_per_yr=0.0,
|
||||
space_cooling_kwh_per_yr=0.0,
|
||||
fabric_energy_efficiency_kwh_per_m2_yr=0.0,
|
||||
design_heat_loss_kw=0.0,
|
||||
main_heating_fuel_kwh_per_yr=main_heating_fuel_kwh_per_yr,
|
||||
main_2_heating_fuel_kwh_per_yr=main_2_heating_fuel_kwh_per_yr,
|
||||
secondary_heating_fuel_kwh_per_yr=secondary_heating_fuel_kwh_per_yr,
|
||||
|
|
|
|||
|
|
@ -664,7 +664,11 @@ def test_ashp_overlay_scores_the_vaillant_end_state_from_a_gas_boiler() -> None:
|
|||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"ashp_from_gas_boiler_001431_before.pdf"
|
||||
)
|
||||
recommendation: Recommendation | None = recommend_heating(before, _AnyProduct())
|
||||
recommendation: Recommendation | None = recommend_heating(
|
||||
before,
|
||||
_AnyProduct(),
|
||||
design_heat_loss_kw=Sap10Calculator().calculate(before).design_heat_loss_kw,
|
||||
)
|
||||
assert recommendation is not None
|
||||
option = next(
|
||||
o for o in recommendation.options if o.measure_type == "air_source_heat_pump"
|
||||
|
|
@ -678,12 +682,15 @@ def test_ashp_overlay_scores_the_vaillant_end_state_from_a_gas_boiler() -> None:
|
|||
# CO2/PE are the postcode DEMAND cascade now that `Sap10Calculator.
|
||||
# calculate` computes EPC emissions/PE on local weather (SAP 10.2
|
||||
# Appendix U p.124); SAP is unchanged (UK-average rating cascade).
|
||||
# Re-pinned after ADR-0049 sizes the pump to this dwelling's 9.69 kW design
|
||||
# heat loss (the 10 kW aroTHERM plus, 110273) instead of the fixed 5 kW unit,
|
||||
# which was undersized here — the correctly-sized pump raises the end-state SAP.
|
||||
_assert_overlay_scores(
|
||||
before,
|
||||
option.overlay,
|
||||
sap=51.99820176096402,
|
||||
co2=1065.7593506066496,
|
||||
pe=10995.781557709413,
|
||||
sap=69.6498827017577,
|
||||
co2=698.896585317197,
|
||||
pe=7271.044741124457,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -697,7 +704,11 @@ def test_ashp_overlay_scores_the_vaillant_end_state_from_a_gas_boiler_instant_hw
|
|||
before: EpcPropertyData = parse_recommendation_summary(
|
||||
"ashp_from_gas_boiler_instant_hw_001431_before.pdf"
|
||||
)
|
||||
recommendation: Recommendation | None = recommend_heating(before, _AnyProduct())
|
||||
recommendation: Recommendation | None = recommend_heating(
|
||||
before,
|
||||
_AnyProduct(),
|
||||
design_heat_loss_kw=Sap10Calculator().calculate(before).design_heat_loss_kw,
|
||||
)
|
||||
assert recommendation is not None
|
||||
option = next(
|
||||
o for o in recommendation.options if o.measure_type == "air_source_heat_pump"
|
||||
|
|
@ -709,12 +720,15 @@ def test_ashp_overlay_scores_the_vaillant_end_state_from_a_gas_boiler_instant_hw
|
|||
# see `test_gas_boiler_instant_hw_before_baselines`.
|
||||
# CO2/PE are the postcode DEMAND cascade now (see the boiler-1 pin above);
|
||||
# SAP is unchanged (UK-average rating cascade).
|
||||
# Re-pinned after ADR-0049 sizes the pump to this dwelling's 12.87 kW design
|
||||
# heat loss (the capped 12 kW aroTHERM plus, 110281) instead of the fixed 5 kW
|
||||
# unit, which was badly undersized here — the sized pump raises the SAP.
|
||||
_assert_overlay_scores(
|
||||
before,
|
||||
option.overlay,
|
||||
sap=39.00740809309464,
|
||||
co2=1845.8588018295509,
|
||||
pe=18944.42568846759,
|
||||
sap=72.22779402266684,
|
||||
co2=870.0854694592007,
|
||||
pe=9025.363011043328,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -734,7 +748,11 @@ def test_ashp_overlay_reproduces_the_relodged_after_from_a_system_boiler_with_cy
|
|||
after: EpcPropertyData = parse_recommendation_summary(
|
||||
"ashp_from_system_boiler_with_cylinder_001431_after.pdf"
|
||||
)
|
||||
recommendation: Recommendation | None = recommend_heating(before, _AnyProduct())
|
||||
recommendation: Recommendation | None = recommend_heating(
|
||||
before,
|
||||
_AnyProduct(),
|
||||
design_heat_loss_kw=Sap10Calculator().calculate(before).design_heat_loss_kw,
|
||||
)
|
||||
assert recommendation is not None
|
||||
option = next(
|
||||
o for o in recommendation.options if o.measure_type == "air_source_heat_pump"
|
||||
|
|
|
|||
|
|
@ -203,9 +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 — a ~4 kW design heat loss sizes to the 5 kW aroTHERM plus (110257).
|
||||
# Act — a 5.5 kW design heat loss sizes to the 5 kW aroTHERM plus (110257):
|
||||
# PSR-0.8 target ~4.4 kW, nearest the 4.37 kW rung.
|
||||
recommendation: Recommendation | None = recommend_heating(
|
||||
baseline, _StubProducts(), design_heat_loss_kw=4.0
|
||||
baseline, _StubProducts(), design_heat_loss_kw=5.5
|
||||
)
|
||||
|
||||
# Assert — the ASHP bundle carries the absolute heat-pump end-state.
|
||||
|
|
@ -771,16 +772,16 @@ def test_boiler_upgrade_leaves_adequate_controls_unchanged() -> None:
|
|||
assert overlay.main_heating_control is None
|
||||
|
||||
|
||||
def test_ashp_sizing_selects_the_smallest_pump_meeting_the_design_heat_loss() -> None:
|
||||
# A 6 kW design heat loss needs a pump whose rated output covers it: the 7 kW
|
||||
# aroTHERM plus (PCDB 110265, ~6.40 kW), the smallest ladder rung meeting the
|
||||
# load — not the fixed 5 kW unit (4.37 kW) that would sit below the load and
|
||||
# collapse SAP's Appendix-N PSR efficiency.
|
||||
def test_ashp_sizing_targets_the_appendix_n_efficiency_peak() -> None:
|
||||
# A 6 kW design heat loss sizes to the SAP Appendix-N efficiency peak
|
||||
# (PSR ~= 0.8, so ~4.8 kW rated output): the 5 kW aroTHERM plus (PCDB 110257,
|
||||
# ~4.37 kW) — what a real installer fits — not the oversized 7 kW unit, and
|
||||
# well clear of the low-PSR efficiency collapse.
|
||||
# Act
|
||||
pcdb_id = select_ashp_pcdb_id(design_heat_loss_kw=6.0)
|
||||
|
||||
# Assert — the 7 kW aroTHERM plus rung.
|
||||
assert pcdb_id == 110265
|
||||
# Assert — the 5 kW aroTHERM plus rung.
|
||||
assert pcdb_id == 110257
|
||||
|
||||
|
||||
def test_ashp_sizing_caps_at_the_largest_pump_for_a_high_heat_loss_dwelling() -> None:
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ def _sap_result(
|
|||
space_heating_kwh_per_yr=0.0,
|
||||
space_cooling_kwh_per_yr=0.0,
|
||||
fabric_energy_efficiency_kwh_per_m2_yr=0.0,
|
||||
design_heat_loss_kw=0.0,
|
||||
main_heating_fuel_kwh_per_yr=0.0,
|
||||
main_2_heating_fuel_kwh_per_yr=0.0,
|
||||
secondary_heating_fuel_kwh_per_yr=0.0,
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ def _sap_result_with_lighting() -> SapResult:
|
|||
space_heating_kwh_per_yr=0.0,
|
||||
space_cooling_kwh_per_yr=0.0,
|
||||
fabric_energy_efficiency_kwh_per_m2_yr=0.0,
|
||||
design_heat_loss_kw=0.0,
|
||||
main_heating_fuel_kwh_per_yr=0.0,
|
||||
main_2_heating_fuel_kwh_per_yr=0.0,
|
||||
secondary_heating_fuel_kwh_per_yr=0.0,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue