mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Merge pull request #1359 from Hestia-Homes/fix/heat-pump-flow-temp-efficiency
fix(sap): heat-pump PSR-extension + water-heating 100% floor (Appendix N)
This commit is contained in:
commit
085bd29af4
5 changed files with 219 additions and 15 deletions
|
|
@ -6234,6 +6234,10 @@ _SAP_DESIGN_HEAT_LOSS_DELTA_T_K: Final[float] = 24.2
|
|||
_HP_SPACE_HEATING_IN_USE_FACTOR_N3_6: Final[float] = 0.95
|
||||
_HP_IN_USE_FACTOR_CRITERIA_MET: Final[float] = 0.95
|
||||
_HP_IN_USE_FACTOR_CRITERIA_FAIL: Final[float] = 0.60
|
||||
# SAP 10.2 Appendix N3.7 (PDF p.109): the heat-pump water-heating efficiency
|
||||
# (in-use factor × η_water) is "subject to a minimum efficiency of 100%" —
|
||||
# below that the direct-electric backup governs.
|
||||
_HP_WATER_HEATING_MIN_EFFICIENCY: Final[float] = 1.0
|
||||
|
||||
|
||||
def _heat_pump_cylinder_meets_pcdb_criteria(
|
||||
|
|
@ -6325,7 +6329,12 @@ def _heat_pump_apm_efficiencies(
|
|||
main_heating_efficiency = (
|
||||
_HP_SPACE_HEATING_IN_USE_FACTOR_N3_6 * eta_space_1_pct / 100.0
|
||||
)
|
||||
water_efficiency_pct = in_use_water * eta_water_3_pct / 100.0
|
||||
# N3.7: in-use factor × η_water, subject to a minimum efficiency of 100%
|
||||
# (the direct-electric backup floors the heat pump's water heating).
|
||||
water_efficiency_pct = max(
|
||||
in_use_water * eta_water_3_pct / 100.0,
|
||||
_HP_WATER_HEATING_MIN_EFFICIENCY,
|
||||
)
|
||||
return (main_heating_efficiency, water_efficiency_pct)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
|||
_apply_heat_network_hiu_default_store, # pyright: ignore[reportPrivateUsage]
|
||||
_cylinder_thermostat_present, # pyright: ignore[reportPrivateUsage]
|
||||
_has_suspended_timber_floor_per_spec, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_pump_apm_efficiencies, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_code_302_effective_factor, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_community_fuel_code, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_distribution_electricity, # pyright: ignore[reportPrivateUsage]
|
||||
|
|
@ -4841,6 +4842,57 @@ def test_hot_water_from_pcdb_heat_pump_bills_at_app_n_wh_high_rate() -> None:
|
|||
assert abs(rate_immersion - 0.0750) <= 1e-6
|
||||
|
||||
|
||||
def test_heat_pump_water_efficiency_is_floored_at_100pct_per_app_n3_7() -> None:
|
||||
# Arrange — SAP 10.2 Appendix N3.7 ("Thermal efficiency for water
|
||||
# heating – heat pumps", PDF p.109): "multiply the thermal efficiency
|
||||
# (ηwater) for water heating by the in-use factor in Table N8; subject
|
||||
# to a MINIMUM EFFICIENCY OF 100%." Our `_heat_pump_apm_efficiencies`
|
||||
# applied the in-use factor but omitted the floor, so an oversized heat
|
||||
# pump whose PSR-extended ηwater × 0.60 in-use fell below 100% billed
|
||||
# water heating at that sub-100% efficiency (over-counting HW fuel).
|
||||
#
|
||||
# Accredited anchor: golden fixture case 56 (PCDB 100061, the config of
|
||||
# cert 100110101713). At HLC 107.82 W/K the PSR is 3.107, above the
|
||||
# record's largest PSR 2.0, so the Appendix N2 extension takes ηwater,3
|
||||
# from 198.9% toward 100% at 2 x 2.0 = 4.0 → 128.55%; × the 0.60 in-use
|
||||
# factor (Open-EPC certs never lodge cylinder HX area → criteria fail)
|
||||
# = 77.13% < 100% → the worksheet (216) reads 100.0000. In-range PSR
|
||||
# (case 54, HLC large) keeps 0.60 × 198.9 = 119.34% (worksheet case 54
|
||||
# (216) = 112.5% for its 187.5% record — both above the floor, unchanged).
|
||||
from domain.sap10_calculator.tables.pcdb import heat_pump_record
|
||||
|
||||
record = heat_pump_record(100061)
|
||||
assert record is not None
|
||||
hp_main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=29, # electricity (heat pump)
|
||||
heat_emitter_type=1, # radiators
|
||||
emitter_temperature=0,
|
||||
main_heating_control=2210,
|
||||
main_heating_category=4,
|
||||
sap_main_heating_code=None,
|
||||
main_heating_index_number=100061,
|
||||
)
|
||||
epc = _typical_semi_detached_epc() # no specified cylinder → in-use 0.60
|
||||
|
||||
# Act — oversized PSR (extension region) vs an in-range PSR.
|
||||
_space_ext, water_ext = _heat_pump_apm_efficiencies(
|
||||
main=hp_main, hp_record=record,
|
||||
hlc_annual_avg_w_per_k=107.82, # PSR 3.107 > largest 2.0
|
||||
epc=epc,
|
||||
) or (None, None)
|
||||
_space_in, water_in = _heat_pump_apm_efficiencies(
|
||||
main=hp_main, hp_record=record,
|
||||
hlc_annual_avg_w_per_k=400.0, # PSR 0.837, in range
|
||||
epc=epc,
|
||||
) or (None, None)
|
||||
|
||||
# Assert — extended HP water efficiency is floored at 100% (1.0); the
|
||||
# in-range PSR keeps the un-floored 0.60 × 198.9% = 119.34%.
|
||||
assert water_ext is not None and abs(water_ext - 1.0) < 1e-9
|
||||
assert water_in is not None and abs(water_in - 0.60 * 198.9 / 100.0) < 1e-9
|
||||
|
||||
|
||||
def test_hot_water_immersion_off_peak_bills_at_table_13_blend() -> None:
|
||||
# Arrange — SAP 10.2 Table 12a (PDF p.191) "Immersion water heater"
|
||||
# row routes the WH column to Table 13 (PDF p.197). For an electric
|
||||
|
|
|
|||
|
|
@ -180,3 +180,88 @@ def test_interpolate_heat_pump_efficiency_at_cert_0380_psr_per_sap_app_n() -> No
|
|||
# ≈ 0.0035077 → eta_water_3 ≈ 285.0861
|
||||
assert abs(eta_space_1 - 234.5235) < 1e-3
|
||||
assert abs(eta_water_3 - 285.0861) < 1e-3
|
||||
|
||||
|
||||
def test_interpolate_extends_above_largest_psr_toward_100pct_per_app_n() -> None:
|
||||
"""SAP 10.2 Appendix N2 (PDF p.101, footnote 44/45) — PSR above the
|
||||
record's largest value extends the efficiency toward 100%, it is NOT
|
||||
clamped to the top-of-table value.
|
||||
|
||||
"in the case of a heat pump (ground, water or air source), where
|
||||
the PSR is greater than the largest value in the data 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."
|
||||
|
||||
Interpolation is reciprocal-linear (footnote 43). Accredited anchor:
|
||||
Elmhurst worksheet for cert 100110101713 / "golden fixture debugging"
|
||||
case 56 (PCDB 100061, ECODAN 8.5 kW, largest PSR row η_space,1=352.0).
|
||||
The dwelling HLC (39) = 107.8199 W/K and max output 8.106 kW give
|
||||
|
||||
PSR = 8.106 × 1000 / (107.8199 × 24.2) = 3.106650
|
||||
|
||||
which exceeds the record's largest PSR (2.0). The spec extension to
|
||||
100% at 2 × 2.0 = 4.0 yields, at t = (3.106650 − 2.0)/(4.0 − 2.0):
|
||||
|
||||
1/η = (1 − t)/352.0 + t/100.0 → η_space,1 = 147.011
|
||||
|
||||
so that (206) = 0.95 × 147.011 = 139.660 — matching the accredited
|
||||
worksheet exactly. The previous top-of-table clamp returned 352.0
|
||||
(→ 334.4%), over-rating the dwelling by +18 SAP.
|
||||
"""
|
||||
from domain.sap10_calculator.tables.pcdb.parser import (
|
||||
interpolate_heat_pump_efficiency_at_psr,
|
||||
)
|
||||
|
||||
record = heat_pump_record(100061)
|
||||
assert record is not None
|
||||
assert record.psr_groups[-1].psr == 2.0
|
||||
assert record.psr_groups[-1].eta_space_1_pct == 352.0
|
||||
|
||||
eta_space_1, _eta_water_3 = interpolate_heat_pump_efficiency_at_psr(
|
||||
record.psr_groups, target_psr=3.106649864134083,
|
||||
)
|
||||
|
||||
assert abs(eta_space_1 - 147.011) < 1e-2
|
||||
assert abs(0.95 * eta_space_1 - 139.6604) < 1e-2
|
||||
|
||||
|
||||
def test_interpolate_above_twice_largest_psr_is_100pct_per_app_n() -> None:
|
||||
"""SAP 10.2 Appendix N2 — beyond twice the largest PSR the efficiency
|
||||
is exactly 100% (the upper terminus of the extension), for both space
|
||||
and water heating PSR-dependent results."""
|
||||
from domain.sap10_calculator.tables.pcdb.parser import (
|
||||
interpolate_heat_pump_efficiency_at_psr,
|
||||
)
|
||||
|
||||
record = heat_pump_record(100061)
|
||||
assert record is not None
|
||||
|
||||
eta_space_1, eta_water_3 = interpolate_heat_pump_efficiency_at_psr(
|
||||
record.psr_groups, target_psr=9.0, # > 2 × 2.0
|
||||
)
|
||||
|
||||
assert eta_space_1 == 100.0
|
||||
assert eta_water_3 == 100.0
|
||||
|
||||
|
||||
def test_interpolate_below_smallest_psr_is_100pct_per_app_n() -> None:
|
||||
"""SAP 10.2 Appendix N2 (PDF p.101) — "For all heat pumps, an
|
||||
efficiency of 100% should be used if the PSR is less than the smallest
|
||||
value in the database record." (Not clamped to the smallest row.)"""
|
||||
from domain.sap10_calculator.tables.pcdb.parser import (
|
||||
interpolate_heat_pump_efficiency_at_psr,
|
||||
)
|
||||
|
||||
record = heat_pump_record(100061)
|
||||
assert record is not None
|
||||
assert record.psr_groups[0].psr == 0.2
|
||||
|
||||
eta_space_1, eta_water_3 = interpolate_heat_pump_efficiency_at_psr(
|
||||
record.psr_groups, target_psr=0.1, # < 0.2
|
||||
)
|
||||
|
||||
assert eta_space_1 == 100.0
|
||||
assert eta_water_3 == 100.0
|
||||
|
|
|
|||
|
|
@ -193,7 +193,10 @@ _CORPUS = Path(
|
|||
# within-0.5 71.6% -> 72.5%, MAE 0.819 -> 0.815. Surfaced by Khalim's Elmhurst
|
||||
# stress worksheet (simulated case 46): closed its last ventilation residual
|
||||
# (our Jan ACH 9.14 -> 9.0748 exact; SAP 29 -> 30 = accredited Elmhurst).
|
||||
_MIN_WITHIN_HALF_SAP = 0.74
|
||||
# 0.74 -> 0.742 via the heat-pump water-heating 100% floor (App N3.7): cert
|
||||
# 100110101713 moves inside +-0.5 (|err| 4.97 -> 0.49). See the _MAX_SAP_MAE
|
||||
# log below for the paired space-heating PSR-extension + water-floor slices.
|
||||
_MIN_WITHIN_HALF_SAP = 0.742
|
||||
# 0.793 -> 0.789 via the §12 Unknown-meter + dual-electric-immersion off-peak
|
||||
# trigger (RdSAP 10 PDF p.62): Apartment 241 (main 691 + 903 dual immersion)
|
||||
# -5.38 -> -1.05. Worksheet-validated on "simulated case 48" (Elmhurst SAP 57,
|
||||
|
|
@ -248,7 +251,22 @@ _MIN_WITHIN_HALF_SAP = 0.74
|
|||
# an identical dwelling rates SAP 87 with "Connected to Dwelling = Yes" (credit
|
||||
# -£167) vs SAP 74 with "No" (credit £0). Enum decoded empirically: 0 = no PV,
|
||||
# 1 = not connected, 2 = connected (the gov-API does not expose it elsewhere).
|
||||
_MAX_SAP_MAE = 0.740
|
||||
# Then 0.740 -> 0.726 via the heat-pump PSR-extension fix (SAP 10.2 Appendix N2,
|
||||
# PDF p.101 footnotes 44/45): an air/ground/water source heat pump whose plant
|
||||
# size ratio exceeds the PCDB record's largest PSR is no longer clamped to the
|
||||
# top-of-table COP — its efficiency is reciprocal-interpolated toward 100% at
|
||||
# twice the largest PSR (and 100% below the smallest PSR). 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. Only two
|
||||
# certs move (both oversized-PSR heat pumps): 100110101713 +18.32 -> -4.97 and
|
||||
# 4510053280 -0.61; within-0.5 holds at 74.1%.
|
||||
# Then 0.726 -> 0.722 (within-0.5 74.1% -> 74.2%) via the heat-pump water-
|
||||
# heating 100% floor (SAP 10.2 Appendix N3.7, PDF p.109: in-use x eta_water
|
||||
# subject to a minimum efficiency of 100%). Only 100110101713 moves: its
|
||||
# oversized-PSR water eff 0.60 x 128.55% = 77.13% is floored to 100% (=
|
||||
# accredited Elmhurst (216)), taking the cert 68.03 -> 72.51 (|err| 4.97 ->
|
||||
# 0.49, now inside +-0.5). In-range heat pumps keep their > 100% water COP.
|
||||
_MAX_SAP_MAE = 0.722
|
||||
_MAX_CO2_MAE_TONNES = 0.09 # t CO2 / yr vs co2_emissions_current
|
||||
_MAX_PE_PER_M2_MAE = 3.5 # kWh / m2 / yr vs energy_consumption_current
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue