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.