From ef668ef7e92525ee0fc1ad8be8a0b14cb84b41c2 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 2 Jun 2026 18:29:57 +0000 Subject: [PATCH] =?UTF-8?q?S0380.183:=20community-heating=20HW=20bills=20a?= =?UTF-8?q?t=20heat-network=20rate=20(=C2=A710b)=20=E2=80=94=20closes=20CH?= =?UTF-8?q?2/CH4=20fully?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SAP 10.2 §10b: hot water for a community-heating dwelling bills at the heat-network rate, not the cert-lodged fuel. Elmhurst §15.0 lodges `water_heating_fuel_type = "Mains gas"` (3.48 p/kWh) as a placeholder on community certs; the worksheet (342) Water-heating cost = (310) × the S0380.171 CHP heat-fraction blend — the SAME rate as space heating (340). Per-line walk of the CH2 block 10b: (340) space = 11837.83 × 0.037955 = 449.3047 (cascade EXACT) (342) water = 3854.12 × 0.037955 = 146.2830 (cascade billed 3854.12 × 0.0348 = 134.12 → −£12.16, the whole residual) (350) lighting + (351) standing → (355) 754.1502. `_hot_water_fuel_cost_gbp_per_kwh`'s `inherit_main_for_community_heating` path already routes HW cost through `_fuel_cost_gbp_per_kwh(main)` (the CHP blend), but its gate `_is_community_heating_hw_from_main` excluded code 302. S0380.182 wired the 302 CO2/PE credit via `_heat_network_code_302_effective_factor`, which intercepts the HW CO2/PE helpers ABOVE this predicate's branch — so extending the predicate to include 302 now affects ONLY the cost path. Closures: CH2 (CHP/Gas) SAP +0.5277→−0.0000, cost −£12.16→−£0.00 — FULLY EXACT CH4 (CHP/Oil) SAP +0.5277→−0.0000, cost −£12.16→−£0.00 — FULLY EXACT CH6 (CHP/Coal) SAP −7.49→−8.02, cost +£172.68→+£184.84 — its HW now also bills the blend, compounding the DLF=1.0 quirk (cascade DLF=1.45); same separate CH6 DLF front. Corpus now 39 variants EXACT on all four metrics (CH2/CH4 join). Open: CH3 CO2/PE (code-304 community-HP COP), CH6 all-metric (DLF=1.0 manual override the Summary doesn't carry). 2225 pass + 1 skip + 0 fail (tolerances 1e-4 all metrics); pyright net-zero 32→32. Co-Authored-By: Claude Opus 4.8 --- .../tests/test_heating_systems_corpus.py | 18 +++++++++-- .../sap10_calculator/rdsap/cert_to_inputs.py | 31 +++++++++++-------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/backend/documents_parser/tests/test_heating_systems_corpus.py b/backend/documents_parser/tests/test_heating_systems_corpus.py index f008d0ee..6ec58ca7 100644 --- a/backend/documents_parser/tests/test_heating_systems_corpus.py +++ b/backend/documents_parser/tests/test_heating_systems_corpus.py @@ -716,11 +716,23 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = ( # / cost −£12.16 is the heat-network cost/standing residual exposed # by S0380.175 (cost-side, untouched by this CO2/PE slice). CH3 # unchanged (code 304 community-HP COP front). + # + # Slice S0380.183 closed the CH2/CH4 HW cost residual: per SAP 10.2 + # §10b the community-heating HW bills at the heat-network rate, not + # the Elmhurst §15.0 "Mains gas" placeholder. Worksheet (342) = + # (310) × the S0380.171 CHP heat-fraction blend (= the same rate as + # space heating (340)), not (310) × 3.48 p/kWh gas. Extended + # `_is_community_heating_hw_from_main` to include code 302 — the + # S0380.182 CO2/PE interception sits above this predicate's branch, + # so it now affects only the cost path. CH2 + CH4 are FULLY EXACT + # on all four metrics. CH6 SAP −7.49→−8.02 / cost +£172.68→+£184.84 + # (its HW now also bills the blend, compounding the DLF=1.0 quirk — + # same root, still the separate CH6 DLF front). _CorpusExpectation(variant='community heating 1', block='11b', expected_sap_resid=+0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000), - _CorpusExpectation(variant='community heating 2', block='11b', expected_sap_resid=+0.5277, expected_cost_resid_gbp=-12.1598, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=+0.0000), + _CorpusExpectation(variant='community heating 2', block='11b', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=+0.0000), _CorpusExpectation(variant='community heating 3', block='11b', expected_sap_resid=+0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-75.3228, expected_pe_resid_kwh=-249.3161), - _CorpusExpectation(variant='community heating 4', block='11b', expected_sap_resid=+0.5277, expected_cost_resid_gbp=-12.1598, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=-0.0000), - _CorpusExpectation(variant='community heating 6', block='11b', expected_sap_resid=-7.4942, expected_cost_resid_gbp=+172.6778, expected_co2_resid_kg=+2411.5399, expected_pe_resid_kwh=+5023.4766), + _CorpusExpectation(variant='community heating 4', block='11b', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=-0.0000), + _CorpusExpectation(variant='community heating 6', block='11b', expected_sap_resid=-8.0219, expected_cost_resid_gbp=+184.8376, expected_co2_resid_kg=+2411.5399, expected_pe_resid_kwh=+5023.4766), ) diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 7aa5ce36..2dd9fc04 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -1496,24 +1496,26 @@ def _water_heating_fuel_code(epc: EpcPropertyData) -> Optional[int]: def _is_community_heating_hw_from_main(epc: EpcPropertyData) -> bool: """True iff the cert's WHC routes HW from the main heating system - (codes 901 / 902 / 914) AND the main is a single-source heat - network with a registered heat-source efficiency - (`_HEAT_NETWORK_HEAT_SOURCE_EFFICIENCY` — currently SAP code 301 - boilers and 304 HP). + (codes 901 / 902 / 914) AND the main is a heat network the cascade + can cost/emission-rate: a registered single-source heat-source + efficiency (`_HEAT_NETWORK_HEAT_SOURCE_EFFICIENCY` — SAP code 301 + boilers / 304 HP) OR code 302 (CHP and boilers). Elmhurst Summary §15.0 lodges `water_heating_fuel_type = "Mains gas"` on community-heating certs regardless of the actual heat-network source — without this guard the HW cost / CO2 / PE bills via the Mains-gas Table 12 code (3.48 p/kWh / 0.21 / 1.13) instead of the - heat-network code (4.24 p/kWh / Table 12 code 41 / 51). + heat-network rate. - SAP code 302 (CHP+boilers) is excluded because the 35%/65% split - requires the displaced-electricity credit line per spec block 13b - (464)/(466) on the HW side — same constraint as `_main_heating_ - co2_factor_kg_per_kwh` (S0380.172). Routing HW through main for - SAP 302 without the credit cascade would regress CO2 / PE; both - the SH and HW paths converge in a single follow-up slice that - wires the CHP credit + boiler-side factor split. + SAP code 302 (CHP+boilers) was previously excluded because the + 35%/65% split needs the displaced-electricity credit line (spec + block 12b/13b (364)/(366)/(464)/(466)). S0380.182 wired that credit + via `_heat_network_code_302_effective_factor`, which intercepts the + HW CO2/PE helpers ABOVE this predicate's branch — so including 302 + here now affects only the COST path, routing HW cost through + `_fuel_cost_gbp_per_kwh(main)` = the S0380.171 CHP heat-fraction + blend (the same rate as space heating, worksheet (342) = (310) × + blend). Closes the CH2/CH4 HW cost residual (S0380.183). """ if epc.sap_heating.water_heating_code not in _WATER_INHERIT_FROM_MAIN_CODES: return False @@ -1521,7 +1523,10 @@ def _is_community_heating_hw_from_main(epc: EpcPropertyData) -> bool: if not _is_heat_network_main(main): return False code = main.sap_main_heating_code if main is not None else None - return isinstance(code, int) and code in _HEAT_NETWORK_HEAT_SOURCE_EFFICIENCY + return isinstance(code, int) and ( + code in _HEAT_NETWORK_HEAT_SOURCE_EFFICIENCY + or code == _SAP_CODE_COMMUNITY_CHP_AND_BOILERS + ) def _main_heating_efficiency(epc: EpcPropertyData) -> float: