fix(pcdb): extend heat-pump efficiency toward 100% beyond table PSR range 🟩

interpolate_heat_pump_efficiency_at_psr clamped to the smallest/largest PSR
row when the dwelling's plant size ratio fell outside the record's range.
That is the SAP 10.2 Appendix N rule for *combined heat-pump-and-boiler*
packages, not for a plain air/ground/water source heat pump.

Per Appendix N2 (PDF p.101, footnotes 44/45) a source heat pump whose PSR
exceeds the record's largest value takes a reciprocal-linear interpolation
between the largest-PSR efficiency and 100% at twice that PSR (100% beyond),
and 100% when the PSR is below the record's smallest value. Both the space-
and water-heating PSR-dependent efficiencies extend this way.

Effect: an oversized heat pump in a small dwelling is no longer credited the
full top-of-table COP. Accredited Elmhurst worksheet for cert 100110101713
(golden fixture case 56, PCDB 100061, PSR 3.107 over largest 2.0): (206)
334.4% -> 139.66% = Elmhurst exact. Corpus (RdSAP-21.0.1, n=1000) MAE
0.7397 -> 0.7258, within-0.5 0.7410 held; only two certs move (both
oversized-PSR heat pumps), 100110101713 +18.32 -> -4.97.

Exhaust-air and combined heat-pump-and-boiler packages have different
boundary rules (straight-to-100% / clamp-to-edge) but are not distinguished
by the current PCDB parse; the air/ground/water rule is applied uniformly,
a documented limitation noted in the function docstring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-29 15:13:16 +00:00
parent 3d93c7b7d5
commit 51d8f65aac

View file

@ -263,6 +263,14 @@ _HP_PSR_GROUP_OFFSET_PSR: Final[int] = 0
_HP_PSR_GROUP_OFFSET_ETA_SPACE_1: Final[int] = 2
_HP_PSR_GROUP_OFFSET_ETA_WATER_3: Final[int] = 6
# SAP 10.2 Appendix N2 (PDF p.101, footnotes 44/45): out-of-range PSR
# extension for air/ground/water source heat pumps. Above the record's
# largest PSR the efficiency is reciprocal-interpolated toward 100% at
# `_EXTENSION_PSR_MULTIPLE` × the largest PSR; below the smallest PSR, and
# beyond that multiple, the efficiency is the terminal 100%.
_EXTENSION_TERMINAL_EFFICIENCY_PCT: Final[float] = 100.0
_EXTENSION_PSR_MULTIPLE: Final[float] = 2.0
def _parse_psr_groups(raw: tuple[str, ...]) -> tuple[PsrEfficiencyGroup, ...]:
"""Decode the variable-length PSR-dependent block of a format-465
@ -317,28 +325,60 @@ def interpolate_heat_pump_efficiency_at_psr(
(not their reciprocals taken from PCDB), so the η_*_pct values must
be strictly positive every PCDB row in the cohort satisfies this.
Per spec PDF p.100 lines 7039-7072: clamp to the smallest PSR in
the record when `target_psr` is below it, and to the largest when
above ("if the PSR is greater than the largest PSR in the database
record then the heat pump space and water heating fractions for the
largest PSR should be used, and if the PSR is less than the
smallest PSR in the database record then the heat pump space and
water heating fractions for the smallest PSR should be used").
Out-of-range PSR (spec PDF p.101, footnotes 44/45 air/ground/water
source heat pumps):
- Below the smallest PSR in the record: "an efficiency of 100%
should be used if the PSR is less than the smallest value in the
database record."
- Above the largest PSR in the record: "an efficiency may be
obtained from linear interpolation between that at the largest
PSR in the data record and efficiency 100% at PSR two times the
largest PSR in the data record. If the PSR is greater than two
times the largest PSR in the data record an efficiency of 100%
should be used." The interpolation is reciprocal-linear too
(footnote 43), with 100% as the upper anchor.
Both space- and water-heating PSR-dependent efficiencies extend the
same way. (Exhaust-air heat pumps and combined heat-pump-and-boiler
packages instead use 100% directly above the largest PSR, and combined
packages clamp to the edge rows; neither is distinguished by the
current PCDB parse, so the air/ground/water rule is applied uniformly
a documented limitation. The dominant RdSAP cohort is air source.)
Cohort fixture: cert 3336-2825-9400-0512-8292 (Mitsubishi PUZ-WM50VHA,
PCDB 104568) PSR 1.40151 brackets PCDB rows PSR 1.2 (η_space_1
= 253.9) and PSR 1.5 (η_space_1 = 229.2). Linear (pre-slice):
237.31; reciprocal (spec-faithful): 236.74 matches worksheet
(206)/(210) at 1e-4 once the 0.95 in-use factor is applied.
Out-of-range anchor: PCDB 100061 (golden fixture case 56), largest PSR
2.0 (η_space_1=352.0). At dwelling PSR 3.10665 the extension to 100%
at PSR 4.0 gives η_space_1 = 147.011 (206) = 139.660, matching the
accredited Elmhurst worksheet (vs the old clamp's 352.0 → 334.4%).
"""
if not psr_groups:
raise ValueError("PSR groups required for interpolation")
if target_psr <= psr_groups[0].psr:
first = psr_groups[0]
return (first.eta_space_1_pct, first.eta_water_3_pct)
if target_psr >= psr_groups[-1].psr:
if target_psr < psr_groups[0].psr:
return (_EXTENSION_TERMINAL_EFFICIENCY_PCT, _EXTENSION_TERMINAL_EFFICIENCY_PCT)
if target_psr > psr_groups[-1].psr:
last = psr_groups[-1]
return (last.eta_space_1_pct, last.eta_water_3_pct)
upper_psr = _EXTENSION_PSR_MULTIPLE * last.psr
if target_psr >= upper_psr:
return (
_EXTENSION_TERMINAL_EFFICIENCY_PCT,
_EXTENSION_TERMINAL_EFFICIENCY_PCT,
)
t = (target_psr - last.psr) / (upper_psr - last.psr)
eta_space_1 = 1.0 / (
(1.0 - t) / last.eta_space_1_pct
+ t / _EXTENSION_TERMINAL_EFFICIENCY_PCT
)
eta_water_3 = 1.0 / (
(1.0 - t) / last.eta_water_3_pct
+ t / _EXTENSION_TERMINAL_EFFICIENCY_PCT
)
return (eta_space_1, eta_water_3)
for low_group, high_group in zip(psr_groups, psr_groups[1:]):
if low_group.psr <= target_psr <= high_group.psr:
span = high_group.psr - low_group.psr