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: