Slice 102f-prep.6: HP-gate §5 central-heating pump gains (Table 4f)

SAP 10.2 Table 4f (PDF p.169) — heat-pump packages (main heating
category 4) bundle the circulation pump's electricity into the
system COP, so worksheet line (70) "Pumps, fans" reports zero gain
for every month on HP certs. Cert 0380's worksheet confirms 0.0
through Jan-Dec.

`internal_gains_from_cert` previously called `central_heating_pump_w`
unconditionally and routed the 3/7/10 W (date-bucket) result through
the seasonal mask in `pumps_fans_monthly_w`. For HP certs that added
~7 W of spurious heating-season gains to (73)m → cold-month MIT
drifted +0.008°C above worksheet (92).

Gating the pump-W computation on `_CATEGORIES_WITHOUT_CENTRAL_HEATING
_PUMP = {4}` zeroes the gain for HP certs and leaves every other
category (gas, oil, electric storage, …) on the existing cascade.
Cohort impact:
  - Cert 0380 MIT 12-tuple now matches worksheet (92) at 1e-3 per
    month (worst Δ at Nov = -0.0009°C).
  - SAP residual closes from +0.155 → +0.059 vs worksheet 88.5104.
  - Closed certs (001479 / 0330 / 9501 — all boiler cohorts, cat 2
    or 1) are unaffected; Layer 4 1e-4 chain gates remain GREEN.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-27 13:52:49 +00:00
parent 2be7905637
commit 80e528e5aa
2 changed files with 39 additions and 18 deletions

View file

@ -678,26 +678,22 @@ def test_api_0380_heat_pump_no_pumps_fans_kwh_per_table_4f() -> None:
assert result.pumps_fans_kwh_per_yr == 0.0
def test_api_0380_mean_internal_temp_matches_worksheet_92_within_1e_2() -> None:
def test_api_0380_mean_internal_temp_matches_worksheet_92_within_1e_3() -> None:
# Arrange — SAP 10.2 Appendix N3.5 (PDF p.107) replaces Table 9c
# steps 3-4 for heat-pump packages with PCDB data: each month
# blends Th, T_unimodal, T_bimodal via Equation N5, where N24,9_m
# and N16,9_m come from Table N5 (PSR-interpolated) and the day
# allocation algorithm.
# blends Th, T_unimodal, T_bimodal via Equation N5.
#
# Cert 0380 (Mitsubishi PUZ-WM50VHA, PCDB 104568, PSR ≈ 1.43)
# lands on Table N5 row "1.2 or more" → annual totals (3, 38) →
# Jan(3, 28) + Dec(0, 10) extended days. Pre-fix the cascade ran
# pure-bimodal so Jan = 17.85 vs worksheet (92) Jan = 18.95
# (Δ -1.10) and Dec = 17.85 vs worksheet 18.16 (Δ -0.31).
# Jan(3, 28) + Dec(0, 10) extended days.
#
# Tolerance 1e-2 absorbs a separate cold-month residual of +0.008°C
# caused by `internal_gains_from_cert` injecting the central-heating
# pump's gain (~7 W heating-season) for HP certs even though SAP 10.2
# Table 4f says HP packages contribute zero pump/fan gains (line 70
# of cert 0380's worksheet is 0.0 for every month). That residual
# is the §5-gating concern of the next slice; this slice's contract
# is the §7 N3.5 extended-heating cascade alone.
# Pre-slice-102f-prep.6 the cold-month MIT drifted +0.008°C due to
# `internal_gains_from_cert` injecting the central-heating pump's
# heating-season gain (~7 W) on HP certs. SAP 10.2 Table 4f
# specifies zero pump/fan gains on HP packages (cert 0380's
# worksheet line 70 = 0.0 every month) — that gating drops the
# spurious gain and tightens the MIT cascade against worksheet
# (92) to 1e-3 per month.
doc = json.loads(_API_0380_JSON.read_text())
epc = EpcPropertyDataMapper.from_api_response(doc)
@ -712,7 +708,7 @@ def test_api_0380_mean_internal_temp_matches_worksheet_92_within_1e_2() -> None:
for m, (cascade, ws) in enumerate(zip(
inputs.mean_internal_temp_monthly_c, worksheet_mit_92
)):
assert abs(cascade - ws) < 1e-2, (
assert abs(cascade - ws) < 1e-3, (
f"month {m + 1}: cascade={cascade:.4f} vs worksheet={ws:.4f}"
)

View file

@ -615,6 +615,22 @@ def _pump_date_category_from_cert(epc: EpcPropertyData) -> PumpDateCategory:
return PumpDateCategory.UNKNOWN
# SAP 10.2 Table 4f categories that do NOT apply a central-heating pump
# gain in §5: the pump/fan electricity is already accounted for in the
# system COP / efficiency. Cert 0380's worksheet line (70) is 0.0 for
# every month, confirming category 4 (heat pumps).
_CATEGORIES_WITHOUT_CENTRAL_HEATING_PUMP: Final[frozenset[int]] = frozenset({4})
def _main_heating_category_from_cert(epc: EpcPropertyData) -> Optional[int]:
"""First main-heating detail's category, or None when the cert
lodges no main heating."""
details = epc.sap_heating.main_heating_details
if not details:
return None
return details[0].main_heating_category
def internal_gains_from_cert(
*,
epc: EpcPropertyData,
@ -668,9 +684,18 @@ def internal_gains_from_cert(
daylight_factor=c_daylight,
)
pump_w = central_heating_pump_w(
date_category=_pump_date_category_from_cert(epc)
)
# SAP 10.2 Table 4f: heat-pump packages (category 4) account for the
# circulation pump's electricity inside the system COP, so worksheet
# line (70) "Pumps, fans" is 0 for HP certs (cert 0380's worksheet
# confirms 0 every month). Bypass the pump-W computation rather than
# carrying it through `pumps_fans_monthly_w`'s seasonal mask.
main_category = _main_heating_category_from_cert(epc)
if main_category in _CATEGORIES_WITHOUT_CENTRAL_HEATING_PUMP:
pump_w = 0.0
else:
pump_w = central_heating_pump_w(
date_category=_pump_date_category_from_cert(epc)
)
# Liquid-fuel + warm-air + PIV + MV + HIU branches default to zero for
# the combi-gas-natural-vent population; future slices will detect them
# from epc.main_heating_details + epc.mechanical_ventilation.