From 80e528e5aa41185a1fa7cc3f59dc16af63148d0a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 27 May 2026 13:52:49 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20102f-prep.6:=20HP-gate=20=C2=A75=20cent?= =?UTF-8?q?ral-heating=20pump=20gains=20(Table=204f)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../tests/test_summary_pdf_mapper_chain.py | 26 +++++++--------- .../worksheet/internal_gains.py | 31 +++++++++++++++++-- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py index bc7d5961..38bf18a3 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -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}" ) diff --git a/domain/sap10_calculator/worksheet/internal_gains.py b/domain/sap10_calculator/worksheet/internal_gains.py index 2f6771a0..90071b1c 100644 --- a/domain/sap10_calculator/worksheet/internal_gains.py +++ b/domain/sap10_calculator/worksheet/internal_gains.py @@ -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.