S0380.182: community-heating CHP+boilers CO2/PE credit (§12b/13b) — closes CH2/CH4 CO2+PE

SAP 10.2 worksheet block 12b (CO2) / 13b (PE) for community heating
"CHP and boilers" (SAP code 302). Per unit of network heat fuel
H = (307)+(310) the effective generation factor is:

  chp×100/(362)×f_fuel − chp×(361)/(362)×f_disp + (1−chp)×100/(367)×f_fuel

  (363)/(463) CHP fuel      = chp_frac × 100/heat_eff × f_fuel
  (364)/(464) less credit   = −chp_frac × elec_eff/heat_eff × f_disp
  (368)/(468) boiler fuel   = (1−chp_frac) × 100/boiler_eff × f_fuel

f_fuel = Table 12 heat-network fuel factor (the CHP unit and the back-up
boilers burn the same community fuel — verified vs CH2 gas / CH4 oil /
CH6 coal worksheets (363)/(368)); f_disp = Table 12f (PDF p.196) credit
for the CHP-generated electricity. RdSAP 10 §C (p.58) defaults: heat eff
50% (362), electrical eff 25% (361), boiler eff 80% (367); CHP heat frac
0.35 per-cert via community_heating_chp_fraction.

New `_heat_network_code_302_effective_factor` + Table 12f flexible
constants (0.420 CO2 / 2.369 PE) + RdSAP §C efficiency constants, wired
into all four factor helpers (main + HW, CO2 + PE) ahead of the existing
single-fuel / 1-over-heat-source-eff path. The worksheet (368)/(468)
boiler emissions DISPLAY rounded/mis-aligned in the PDF, but the
(373)/(473)/(386)/(486) totals reconcile only with the boiler at the
full Table 12 factor — verified EXACT.

Two spec citations applied:
- Table 12f flexible-operation default for RdSAP community CHP is an
  Elmhurst engine choice (Table 12f notes make "standard" the default);
  mirrored per [[feedback-software-no-special-handling]] and documented
  in SAP_CALCULATOR.md §8.3.
- Table 12 heat-network oil/biodiesel CO2 (codes 53/56) corrected
  0.298 → 0.335 per Table 12 (p.189) "assumes 'gas oil'"; the code-302
  oil cascade (CH4) was the first to exercise it. PE 1.180 was already
  correct. No other variant uses these codes (no regression).

Closures (CO2 + PE only — the CHP credit does not touch cost/SAP):
  CH2 (CHP/Gas)  CO2 −1411.49→+0.0000, PE +1331.23→+0.0000  EXACT
  CH4 (CHP/Oil)  CO2 −4378.24→−0.0000, PE  +319.81→−0.0000  EXACT
  CH6 (CHP/Coal) CO2/PE re-pinned (+2411.54 / +5023.48) — its worksheet
                 lodges a manual DLF=1.0 the Summary doesn't carry, so
                 cascade DLF=1.45 over-scales H; same root as the CH6
                 SAP −7.49 / cost +£172 (separate DLF front).

CH2/CH4 are now CO2+PE-exact but still carry the heat-network cost/SAP
residual (+0.5277 SAP / −£12.16 cost, exposed by S0380.175 — cost-side,
untouched here). CH3 unchanged (code 304 community-HP COP front).

Corpus state: 37 variants EXACT on all four metrics (incl. CH1);
remaining residuals are CH2/CH4 cost+SAP, CH3 CO2+PE (HP COP), CH6
all-metric (DLF quirk). 2223 pass + 1 skip + 0 fail (tolerances 1e-4 all
metrics per S0380.181); pyright net-zero 43→43.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-02 18:22:51 +00:00 committed by Jun-te Kim
parent 3bbb9aa1a2
commit 035303e9f8
5 changed files with 241 additions and 5 deletions

View file

@ -695,11 +695,32 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
# for the next slice. Elmhurst DISPLAYS the (372) energy column as
# 0.01 × (307) (space only) but computes emissions on 0.01 ×
# (307+310) per the §C3.2 text — verified EXACT line-by-line.
#
# Slice S0380.182 wired the SAP 10.2 §12b/13b community-heating
# "CHP and boilers" (SAP code 302) CO2/PE cascade: per unit of
# network heat fuel H = (307)+(310), the effective generation factor
# = chp_frac × 100/(362) × f_fuel chp_frac × (361)/(362) × f_disp
# + (1chp_frac) × 100/(367) × f_fuel, where f_fuel is the Table 12
# heat-network fuel factor (CHP + back-up boilers burn the same
# community fuel) and f_disp is the Table 12f credit factor for the
# CHP-generated electricity (Elmhurst uses "flexible operation"
# 0.420 CO2 / 2.369 PE). RdSAP 10 §C (p.58) defaults: heat eff 50% /
# electrical eff 25% / boiler eff 80%; CHP frac 0.35 per-cert. Also
# fixed Table 12 heat-network-oil CO2 (codes 53/56 0.298→0.335 per
# Table 12 p.189 — the code-302 oil cascade was the first to use it).
# CH2 (gas) + CH4 (oil) CO2 + PE now EXACT (<1e-4). CH6 (coal) CO2/PE
# shift sign: its worksheet lodges a manual DLF=1.0 (two adjoining
# dwellings) the Summary doesn't carry, so the cascade's DLF=1.45
# over-scales H — pin + the CH6 SAP 7.49 / cost +£172 are the same
# DLF quirk (separate front, likely pin-forever). CH2/CH4 SAP +0.5277
# / 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).
_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=-1411.4867, expected_pe_resid_kwh=+1331.2330),
_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 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=-4378.2449, expected_pe_resid_kwh=+319.8065),
_CorpusExpectation(variant='community heating 6', block='11b', expected_sap_resid=-7.4942, expected_cost_resid_gbp=+172.6778, expected_co2_resid_kg=-2916.0676, expected_pe_resid_kwh=+7689.7925),
_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),
)

View file

@ -495,3 +495,34 @@ shape (Table 12 annual where spec literal says monthly), so the gate
is implemented under the same `dual-rate → annual on top of monthly`
discipline. If a second §12.4.4-eligible cert worksheet diverges from
this rule it should be raised against this row before re-tuning.
### 8.3 Community-heating CHP uses Table 12f "flexible operation" by default
**Slice S0380.182.** For RdSAP-defaulted community heating with CHP
(SAP code 302) that is **not** in the PCDB, the displaced-electricity
credit (worksheet (364)/(366) CO2 and (464)/(466) PE) needs a Table 12f
(PDF p.196) "fuel factor for electricity generated by CHP". Table 12f
offers three regimes per CHP vintage:
| Regime | CO2 kg/kWh | PE | Note |
|---|---|---|---|
| export only | 0.394 | 2.345 | |
| **flexible operation** | **0.420** | **2.369** | needs assessor evidence |
| standard | 0.348 | 2.149 | "all other operating regimes" |
Table 12f's own notes make **standard** the default ("Standard ... should
be used for all other operating regimes of gas CHP plants") and require
submitted evidence for **flexible**. Yet the BRE-approved Elmhurst rdSAP
engine emits **0.420 / 2.369 (flexible)** for these RdSAP-defaulted
community-CHP certs — verified line-by-line against the CH2 (gas) / CH4
(oil) / CH6 (coal) corpus worksheets (364)/(366)/(464)/(466), all of
which carry 0.4200 CO2 and 2.3690 PE regardless of the community fuel.
RdSAP 10 §C (p.58) is silent on the Table 12f regime, so this is an
engine default not derivable from the spec text.
Per [[feedback-software-no-special-handling]] / [[feedback-worksheet-not-api-reference]]
we mirror the engine: `_TABLE_12F_CHP_FLEXIBLE_{CO2,PE}` in
`cert_to_inputs`. CH2 + CH4 close to <1e-4 on both CO2 and PE with this
factor; "standard" (0.348/2.149) would leave a residual. If a future
PCDB-listed or evidence-backed CHP cert diverges, raise it against this
row before re-tuning.

View file

@ -984,6 +984,97 @@ def _heat_network_distribution_electricity(
return (energy_kwh, co2_factor, pe_factor)
# SAP 10.2 Table 12 fuel code 302's worksheet path. Community heating
# "CHP and boilers" (Table 4a code 302).
_SAP_CODE_COMMUNITY_CHP_AND_BOILERS: Final[int] = 302
# SAP 10.2 Table 12f (PDF p.196) — fuel factors for the electricity
# GENERATED BY CHP (the displaced-grid credit, worksheet (364)/(464)).
# The BRE-approved Elmhurst rdSAP engine applies the "flexible
# operation" row (0.420 kg CO2/kWh, 2.369 PE) to RdSAP-defaulted
# community CHP that is not in the PCDB — verified line-by-line against
# the CH2/CH4/CH6 corpus worksheets (363)..(366) / (463)..(466). Table
# 12f's own notes make "standard" (0.348 / 2.149) the default and
# require assessor evidence for "flexible"; we mirror the certified
# engine per [[feedback-software-no-special-handling]] (documented as a
# spec divergence in SAP_CALCULATOR.md §8).
_TABLE_12F_CHP_FLEXIBLE_CO2_KG_PER_KWH: Final[float] = 0.420
_TABLE_12F_CHP_FLEXIBLE_PE_KWH_PER_KWH: Final[float] = 2.369
# RdSAP 10 §C (PDF p.58) heat-network CHP defaults when the network is
# not in the PCDB: CHP overall efficiency 75% with heat-to-power ratio
# 2.0 → heat efficiency 50% (worksheet (362)) + electrical efficiency
# 25% (worksheet (361)); back-up boiler efficiency 80% (worksheet
# (367)). The CHP heat fraction (0.35 default) is per-cert via
# `community_heating_chp_fraction`.
_HEAT_NETWORK_CHP_HEAT_EFFICIENCY: Final[float] = 0.50
_HEAT_NETWORK_CHP_ELECTRICAL_EFFICIENCY: Final[float] = 0.25
_HEAT_NETWORK_CHP_BOILER_EFFICIENCY: Final[float] = 0.80
def _heat_network_code_302_effective_factor(
main: Optional[MainHeatingDetail],
*,
primary_energy: bool,
) -> Optional[float]:
"""SAP 10.2 worksheet block 12b (CO2) / 13b (PE) for community
heating "CHP and boilers" (SAP code 302) the effective per-kWh
factor to apply to the network heat fuel [(307) + (310)].
Per unit of network heat fuel H = (307) + (310), the worksheet sums:
CHP fuel (363)/(463) = chp_frac × 100/(362) × f_fuel
less credit (364)/(464) = chp_frac × (361)/(362) × f_disp
boiler fuel (368)/(468) = (1chp_frac) × 100/(367) × f_fuel
where f_fuel is the Table 12 heat-network fuel factor (the CHP unit
and the back-up boilers burn the same community fuel verified vs
CH2 gas / CH4 oil / CH6 coal worksheets) and f_disp is the Table 12f
credit factor for the electricity the CHP generates. RdSAP 10 §C
defaults: (362) heat eff 50%, (361) electrical eff 25%, (367) boiler
eff 80%.
Returns the blended factor for code-302 mains with the CHP-split
fields populated; None otherwise so callers fall through to the
existing single-fuel / heat-source-efficiency-scaling path.
NB the worksheet PDF DISPLAYS the (368)/(468) boiler emissions
rounded/mis-aligned, but the (373)/(473)/(386)/(486) totals
reconcile only with the boiler at the FULL Table 12 fuel factor
verified EXACT.
"""
if (
main is None
or main.sap_main_heating_code != _SAP_CODE_COMMUNITY_CHP_AND_BOILERS
):
return None
chp_fraction = main.community_heating_chp_fraction
boiler_fuel_code = main.community_heating_boiler_fuel_type
if chp_fraction is None or boiler_fuel_code is None:
return None
if primary_energy:
fuel_factor = primary_energy_factor(boiler_fuel_code)
displaced_factor = _TABLE_12F_CHP_FLEXIBLE_PE_KWH_PER_KWH
else:
fuel_factor = co2_factor_kg_per_kwh(boiler_fuel_code)
displaced_factor = _TABLE_12F_CHP_FLEXIBLE_CO2_KG_PER_KWH
boiler_fraction = 1.0 - chp_fraction
return (
chp_fraction
* (1.0 / _HEAT_NETWORK_CHP_HEAT_EFFICIENCY)
* fuel_factor
- chp_fraction
* (
_HEAT_NETWORK_CHP_ELECTRICAL_EFFICIENCY
/ _HEAT_NETWORK_CHP_HEAT_EFFICIENCY
)
* displaced_factor
+ boiler_fraction
* (1.0 / _HEAT_NETWORK_CHP_BOILER_EFFICIENCY)
* fuel_factor
)
@dataclass(frozen=True)
class PriceTable:
"""Seam between the spec-correct SAP 10.2/10.3 Table 12 prices and
@ -2606,6 +2697,13 @@ def _main_heating_co2_factor_kg_per_kwh(
- zero-fuel cases (sum monthly_kwh == 0 effective factor None;
annual factor is the safe degenerate value)
"""
# SAP 10.2 §12b — community-heating CHP+boilers (code 302): the
# blended CHP-credit + boiler generation CO2 factor (S0380.182).
code_302_co2 = _heat_network_code_302_effective_factor(
main, primary_energy=False,
)
if code_302_co2 is not None:
return code_302_co2
if not _is_electric_main(main):
# Heat-network mains (SAP codes 301 / 304) are non-electric per
# `_is_electric_main` but require a heat-source-efficiency scaling
@ -2671,6 +2769,13 @@ def _main_heating_primary_factor(
Fallback to annual `primary_energy_factor` for non-electric mains
and the same edge cases as the CO2 helper (no Table 12a row,
unknown dual-rate codes, zero-fuel)."""
# SAP 10.2 §13b — community-heating CHP+boilers (code 302): the
# blended CHP-credit + boiler generation PE factor (S0380.182).
code_302_pe = _heat_network_code_302_effective_factor(
main, primary_energy=True,
)
if code_302_pe is not None:
return code_302_pe
fuel = _main_fuel_code(main)
if not _is_electric_main(main):
# PE-side mirror of `_main_heating_co2_factor_kg_per_kwh`
@ -2929,6 +3034,16 @@ def _hot_water_co2_factor_kg_per_kwh(
monthly HW fuel kWh the calculator uses an annual-flat HW
efficiency so the SHAPE of fuel monthly is identical to demand
monthly, and `_effective_monthly_co2_factor` is shape-only)."""
# SAP 10.2 §12b — community-heating CHP+boilers (code 302) HW from
# main: the same blended CHP-credit + boiler generation CO2 factor
# as SH (S0380.182). Gated on WHC ∈ {901, 902, 914} so immersion-
# heated DHW on a CHP network keeps the lodged electric factor.
if epc.sap_heating.water_heating_code in _WATER_INHERIT_FROM_MAIN_CODES:
code_302_co2 = _heat_network_code_302_effective_factor(
_water_heating_main(epc), primary_energy=False,
)
if code_302_co2 is not None:
return code_302_co2
# Community heating + WHC ∈ {901, 902, 914}: HW heat is delivered
# through the heat-network main, so HW CO2 must read the same
# Table 12 heat-network code factor as SH, scaled by 1/heat_source_
@ -2977,6 +3092,15 @@ def _hot_water_primary_factor(
exactly to match the Elmhurst worksheet's (278) annual factor.
The 41-variant heating-systems corpus closes its HW PE residual
+25/+48 0 with this gate."""
# SAP 10.2 §13b — community-heating CHP+boilers (code 302) HW from
# main: same blended CHP-credit + boiler generation PE factor as SH
# (S0380.182). Gated on WHC ∈ {901, 902, 914}.
if epc.sap_heating.water_heating_code in _WATER_INHERIT_FROM_MAIN_CODES:
code_302_pe = _heat_network_code_302_effective_factor(
_water_heating_main(epc), primary_energy=True,
)
if code_302_pe is not None:
return code_302_pe
# Mirror of `_hot_water_co2_factor_kg_per_kwh` community-heating
# branch (S0380.173): WHC ∈ {901, 902, 914} on a heat-network main
# routes HW PE through the same Table 12 heat-network code as SH,

View file

@ -192,8 +192,14 @@ CO2_KG_PER_KWH: Final[dict[int, float]] = {
30: 0.136, 31: 0.136, 32: 0.136, 33: 0.136, 34: 0.136, 35: 0.136,
38: 0.136, 40: 0.136, 39: 0.136, 60: 0.136, 36: 0.136,
# Heat networks
51: 0.210, 52: 0.241, 53: 0.298, 54: 0.375, 55: 0.269,
56: 0.298, 57: 0.036, 58: 0.018,
# Heat-network oil (code 53 "assumes 'gas oil'") and mineral-oil/
# biodiesel boilers (code 56) carry 0.335 kg CO2/kWh per SAP 10.2
# Table 12 (p.189) — NOT the individual-appliance heating-oil factor
# (code 4 = 0.298). (Fixed in S0380.182 when the code-302 CHP CO2
# cascade first exercised heat-network oil; PE 1.180 was already
# correct.)
51: 0.210, 52: 0.241, 53: 0.335, 54: 0.375, 55: 0.269,
56: 0.335, 57: 0.036, 58: 0.018,
41: 0.136, 42: 0.015, 43: 0.029, 44: 0.024,
45: 0.015, 46: 0.011, 47: 0.011, 48: 0.136, 49: 0.136,
50: 0.0,

View file

@ -41,6 +41,7 @@ from domain.sap10_calculator.exceptions import (
from domain.sap10_calculator.rdsap.cert_to_inputs import (
SAP_10_2_SPEC_PRICES,
_has_suspended_timber_floor_per_spec, # pyright: ignore[reportPrivateUsage]
_heat_network_code_302_effective_factor, # pyright: ignore[reportPrivateUsage]
_heat_network_distribution_electricity, # pyright: ignore[reportPrivateUsage]
_heat_network_dlf, # pyright: ignore[reportPrivateUsage]
_is_electric_main, # pyright: ignore[reportPrivateUsage]
@ -209,6 +210,59 @@ def test_heat_network_distribution_electricity_per_sap_10_2_appendix_c_3_2() ->
assert abs(pe_factor - expected_pe_factor) <= 1e-9
def test_heat_network_code_302_chp_effective_factor_per_sap_10_2_block_12b_13b() -> None:
# Arrange — community heating "CHP and boilers" (SAP code 302) on
# the RdSAP 10 §C (PDF p.58) defaults: CHP heat frac 0.35, heat eff
# 50% / electrical eff 25%, boiler eff 80%. CH2-style gas network
# (community_heating_boiler_fuel_type = 51 → Table 12 gas 0.210 CO2
# / 1.130 PE). SAP 10.2 §12b/13b effective generation factor:
# chp×100/(362)×f chp×(361)/(362)×f_disp + (1chp)×100/(367)×f
# with f_disp = Table 12f flexible operation (0.420 CO2 / 2.369 PE).
main = MainHeatingDetail(
has_fghrs=False,
main_fuel_type=20,
heat_emitter_type=1,
emitter_temperature=1,
main_heating_control=2306,
main_heating_category=6,
sap_main_heating_code=302,
community_heating_chp_fraction=0.35,
community_heating_boiler_fuel_type=51,
)
# Act
co2 = _heat_network_code_302_effective_factor(main, primary_energy=False)
pe = _heat_network_code_302_effective_factor(main, primary_energy=True)
# Assert — gas: 0.35×2.0×0.210 0.35×0.5×0.420 + 0.65×1.25×0.210
# = 0.147 0.0735 + 0.170625 = 0.244125 (matches the
# CH2 worksheet (386) generation factor); PE mirror with 1.130 /
# 2.369 = 1.29455.
assert co2 is not None
assert pe is not None
assert abs(co2 - 0.244125) <= 1e-9
assert abs(pe - 1.29455) <= 1e-9
def test_heat_network_code_302_effective_factor_none_for_non_302_main() -> None:
# Arrange — a code-301 heat-network boiler main (no CHP split). The
# §12b/13b CHP+boilers blend applies only to code 302; code 301
# routes through the 1/heat-source-eff scaling path instead.
main = MainHeatingDetail(
has_fghrs=False,
main_fuel_type=20,
heat_emitter_type=1,
emitter_temperature=1,
main_heating_control=2306,
main_heating_category=6,
sap_main_heating_code=301,
)
# Act / Assert
assert _heat_network_code_302_effective_factor(main, primary_energy=False) is None
assert _heat_network_code_302_effective_factor(main, primary_energy=True) is None
def test_heat_network_distribution_electricity_none_for_individual_main() -> None:
# Arrange — an individually-heated gas-boiler main (category 2, no
# heat-network SAP code). §C3.2 pumping electricity applies only to