S0380.183: community-heating HW bills at heat-network rate (§10b) — closes CH2/CH4 fully

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-02 18:29:57 +00:00 committed by Jun-te Kim
parent 035303e9f8
commit ef668ef7e9
2 changed files with 33 additions and 16 deletions

View file

@ -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),
)

View file

@ -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: