Model/domain/sap10_calculator/docs/SAP_CALCULATOR.md
Khalim Conn-Kowlessar 8e86de2257 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>
2026-06-02 18:23:17 +00:00

528 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SAP 10.2 / RdSAP 10 calculator — module overview
Deterministic, bit-faithful replication of the RdSAP10 calculation engine.
Validated against the 6 Elmhurst U985 worksheet PDFs at **abs=1e-4 on
every line ref** for both the Rating cascade (UK-average climate, used
for the published SAP rating + EI rating) and the Demand cascade
(postcode climate via PCDB Table 172, used for the EPC's published
Current Carbon, Current Primary Energy, and Fuel Bill).
**Current state: 941/941 pins green** (rating + demand section cascade
pins via `test_section_cascade_pins.py`, plus e2e SapResult + monthly
infiltration ACH pins via `test_e2e_elmhurst_sap_score.py`).
This document is the public API + architecture reference. For fixture
authoring see [`domain/sap10_calculator/README.md`](../../domain/sap10_calculator/README.md).
---
## 1. Public API
Three entry points, all in `domain.sap10_calculator.rdsap.cert_to_inputs`:
```python
from domain.sap10_calculator.rdsap.cert_to_inputs import (
cert_to_inputs, # SAP rating + EI rating (UK-avg climate)
cert_to_demand_inputs, # Current Carbon + Current PE (postcode climate)
local_climate_for_cert, # postcode → PostcodeClimate (None on miss)
)
from domain.sap10_calculator.calculator import calculate_sap_from_inputs, SapResult
```
### 1.1 Rating cascade — `cert_to_inputs(epc)`
Produces a `CalculatorInputs` aggregate with UK-average climate. Feed it
to `calculate_sap_from_inputs(inputs)` to get a `SapResult`:
```python
inputs = cert_to_inputs(epc)
result = calculate_sap_from_inputs(inputs)
result.sap_score # int — published SAP rating (1-100+)
result.sap_score_continuous # float — un-rounded
result.ecf # Energy Cost Factor
result.total_fuel_cost_gbp # Rating-cascade cost (NOT the EPC's Fuel Bill)
```
Per SAP10.2 Appendix U (p.124) only the SAP rating and EI rating use
UK-average weather. Everything else (emissions, primary energy, fuel
bill) the EPC publishes comes from the demand cascade below.
### 1.2 Demand cascade — `cert_to_demand_inputs(epc)`
Same physics, postcode-district climate from PCDB Table 172:
```python
inputs = cert_to_demand_inputs(epc)
result = calculate_sap_from_inputs(inputs)
result.co2_kg_per_yr # EPC's "Current Carbon" (tonnes/year ÷ 1000)
result.primary_energy_kwh_per_yr # EPC's "Current Primary Energy"
```
Falls back to UK-average climate when `epc.postcode` is missing or the
district is not in Table 172 (rural postcodes → no PCDB match).
### 1.3 Section helpers — `<section>_section_from_cert(epc, postcode_climate=...)`
Each U985 worksheet section has a typed dataclass + a `_section_from_cert`
helper. Use these for explicit line-ref pinning or to compose your own
flow. The `postcode_climate` kwarg selects rating (None) vs demand
(PostcodeClimate) cascade.
| Helper | Returns | Pins |
|---|---|---|
| `dimensions_from_cert(epc)` | `Dimensions` | §1 (1)..(5) |
| `ventilation_from_cert(epc, postcode_climate=...)` | `VentilationResult` | §2 (6a)..(25)m |
| `heat_transmission_section_from_cert(epc)` | `HeatTransmission` | §3 (26)..(37) |
| `water_heating_section_from_cert(epc)` | `WaterHeatingResult` | §4 (42)..(65)m |
| `internal_gains_section_from_cert(epc)` | `InternalGainsResult` | §5 (66)..(73) |
| `solar_gains_section_from_cert(epc, postcode_climate=...)` | `SolarGainsResult` | §6 (74)..(83) |
| `mean_internal_temperature_section_from_cert(epc, postcode_climate=...)` | `MeanInternalTemperatureResult` | §7 (85)..(94) |
| `space_heating_section_from_cert(epc, postcode_climate=...)` | `SpaceHeatingResult` | §8 (95)..(99) |
| `space_cooling_section_from_cert(epc, postcode_climate=...)` | `SpaceCoolingResult` | §8c (100)..(108) |
| `fabric_energy_efficiency_from_cert(epc)` | `float` | §8f (109) |
| `energy_requirements_section_from_cert(epc, postcode_climate=...)` | `EnergyRequirementsResult` | §9a (201)..(221) |
| `fuel_cost_section_from_cert(epc, postcode_climate=...)` | `FuelCostResult` | §10a (240)..(255) |
| `sap_rating_section_from_cert(epc)` | `SapRatingSection` | §11a (256)..(258) — UK-avg only |
| `environmental_section_from_cert(epc, postcode_climate=...)` | `EnvironmentalSection` | §12 (261)..(274) |
| `primary_energy_section_from_cert(epc, postcode_climate=...)` | `PrimaryEnergySection` | §13a (275)..(286) |
---
## 2. The simulator use case
The calculator is built for "what-if" analysis — modify cert inputs (e.g.
upgrade wall insulation), re-run, observe the delta. The shape:
```python
import dataclasses
from domain.sap10_calculator.rdsap.cert_to_inputs import (
cert_to_inputs, local_climate_for_cert,
environmental_section_from_cert, primary_energy_section_from_cert,
)
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
def dwelling_outputs(epc):
"""The 4 EPC-facing outputs for any cert.
SAP and EI ratings use UK-average climate per Appendix U; Current
Carbon and Current Primary Energy use postcode climate from PCDB
Table 172."""
pc = local_climate_for_cert(epc)
rating = calculate_sap_from_inputs(cert_to_inputs(epc))
env_rating = environmental_section_from_cert(epc) # UK-avg
env_demand = environmental_section_from_cert(epc, postcode_climate=pc)
pe_demand = primary_energy_section_from_cert(epc, postcode_climate=pc)
return {
"sap_rating": rating.sap_score, # UK-avg
"ei_rating": env_rating.ei_rating_integer if env_rating else None, # UK-avg
"current_carbon_kg": env_demand.total_co2_kg_per_yr if env_demand else None, # postcode
"current_pe_kwh": pe_demand.total_pe_kwh_per_yr if pe_demand else None, # postcode
}
# Baseline
baseline = dwelling_outputs(epc)
# Counterfactual — fill the cavity
upgraded_walls = [
dataclasses.replace(w, insulation_thickness_mm=50, wall_insulation_type=2)
for w in epc.walls
]
modified_epc = dataclasses.replace(epc, walls=upgraded_walls)
upgraded = dwelling_outputs(modified_epc)
print({k: upgraded[k] - baseline[k] for k in baseline}) # impact
```
Absolute values match the EPC; deltas reflect the modelled retrofit.
---
## 3. Architecture
Two cascades stacked on a shared physics core:
```
cert: EpcPropertyData
┌──────────────────────────┼──────────────────────────┐
│ │
cert_to_inputs(epc) cert_to_demand_inputs(epc)
(UK-avg climate, region 0) (postcode climate via PCDB Table 172)
│ │
▼ ▼
CalculatorInputs (rating) CalculatorInputs (demand)
│ │
▼ ▼
calculate_sap_from_inputs(inputs) calculate_sap_from_inputs(inputs)
│ │
▼ ▼
SapResult (rating) SapResult (demand)
• sap_score • co2_kg_per_yr (EPC value)
• sap_score_continuous • primary_energy_kwh_per_yr
• ecf • space_heating_kwh_per_yr
• total_fuel_cost_gbp • main_heating_fuel_kwh_per_yr
• (more, all at postcode climate)
```
Climate is the only difference between the two cascades. Internally, the
climate is plumbed through as either an `int` region index (0..21) or a
`PostcodeClimate` instance (PCDB Table 172). Four functions in
`domain.sap10_calculator.climate.appendix_u` dispatch on `isinstance`:
`external_temperature_c`, `wind_speed_m_per_s`,
`horizontal_solar_irradiance_w_per_m2`, plus `_latitude_deg` in
`worksheet/solar_gains.py`.
### Per-end-use CO2 and PE factors
For the demand cascade's CO2 (§12) and PE (§13a) line refs:
- Gas end-uses (main heating, water heating with a gas boiler) use the
annual Table 12 / Table 32 (RdSAP10) factor — gas factors don't vary
monthly.
- Electricity end-uses (secondary heater, pumps/fans, lighting, electric
shower, secondary heating with electric resistance) use the
Σ(kWh_m × Table 12d_m) / Σ kWh_m **effective annual** factor — a
Days-weighted average of the monthly factor by the per-end-use
monthly kWh distribution. Same shape for PE (Table 12e).
This is the slice-32 / slice-33 mechanism. See `_effective_monthly_factor`
in `cert_to_inputs.py` for the helper and the per-end-use factor fields
on `CalculatorInputs`.
---
## 4. File map
```
domain/sap10_calculator/
├── calculator.py # Top-level orchestrator (CalculatorInputs → SapResult)
├── README.md # Fixture authoring cookbook
├── rdsap/
│ └── cert_to_inputs.py # EpcPropertyData → CalculatorInputs (both cascades)
├── worksheet/ # Per-section physics modules (§1..§13a)
│ ├── dimensions.py # §1
│ ├── ventilation.py # §2
│ ├── heat_transmission.py # §3
│ ├── water_heating.py # §4
│ ├── internal_gains.py # §5
│ ├── solar_gains.py # §6
│ ├── mean_internal_temperature.py # §7
│ ├── space_heating.py # §8
│ ├── space_cooling.py # §8c
│ ├── fabric_energy_efficiency.py # §8f
│ ├── energy_requirements.py # §9a
│ ├── fuel_cost.py # §10a
│ ├── rating.py # §11a + §14 EI rating equations
│ ├── utilisation_factor.py # Table 9a η helper
│ └── tests/
│ ├── _elmhurst_worksheet_NNNNNN.py # 6 conformance fixtures
│ ├── _elmhurst_fixtures.py # ALL_FIXTURES registry
│ ├── test_section_cascade_pins.py # THE conformance suite
│ └── test_e2e_elmhurst_sap_score.py # Top-level SapResult pins
├── climate/
│ └── appendix_u.py # Tables U1/U2/U3 (UK-avg + 22 regions)
└── tables/
├── table_12.py # Fuel prices, CO2 factors, PE factors (annual + Table 12d/12e monthly)
├── table_12a.py # Off-peak high-rate fractions
├── table_32.py # RdSAP10 fuel prices (Table 32)
└── pcdb/
├── postcode_weather.py # PCDB Table 172 (postcode-district weather)
├── parser.py # PCDB row parsers
└── (other PCDB tables)
domain/sap10_calculator/docs/specs/
├── sap-10-2-full-specification-2025-03-14.pdf # SAP 10.2 spec
├── RdSAP 10 Specification 10-06-2025.pdf # RdSAP 10 spec
├── pcdb10.dat # PCDB raw data (Table 172 + others)
├── SAP_CALCULATOR.md # this file
└── pcdb_table_*.jsonl # PCDB extracts per table
```
---
## 5. Validation
### The 6 Elmhurst U985 fixtures
Each fixture is a real-cert ground-truth captured from Elmhurst Energy's
RdSAP tool. The pair of PDFs (`Summary_NNNNNN.pdf` cert + `U985-0001-
NNNNNN.pdf` worksheet) gives us:
- A full `EpcPropertyData` encoding (the `Summary` → fixture's `build_epc()`)
- Every populated worksheet line ref `(1a)..(286)` to 4 d.p. (the
`U985-...` PDF → fixture's `LINE_*` / `DEMAND_LINE_*` constants)
The fixtures span the cert-shape variations we've seen in the wild:
1-2 extensions, room-in-roof present/absent, electric shower present,
party-wall code variations, suspended timber floor quirks, etc.
| Fixture | TFA | Notes |
|---|---|---|
| 000474 | 56.79 | Main + 2 extensions, gas combi |
| 000477 | 77.58 | RR main-only, gas combi |
| 000480 | 84.41 | Main + 1 extension + RR |
| 000487 | 81.57 | RR + extension + alt wall, **electric shower** |
| 000490 | 66.06 | Main + 1 extension |
| 000516 | 90.54 | Main only, gas combi |
### Pin scoreboard
```
RATING CASCADE (UK-avg climate)
§1 12/12 §2 96/96 §3 24/24 §4 54/54 §5 54/54 §6 12/12
§7 60/60 §8 36/36 §8c 42/42 §8f 6/6 §9a 72/72 §10a 192/192
§11a 24/24 §12 84/84
rating Σ = 768/768
DEMAND CASCADE (postcode climate)
D§12 54/54 D§13a 36/36
demand Σ = 90/90
E2E SapResult pins
sap_score, ecf, fuel_cost, co2, kwh fields 66/66
monthly_infiltration_ach 6/6
e2e Σ = 72/72
GRAND TOTAL = 930/930
```
### How to run
```bash
# Full SAP calculator suite (cascade pins + e2e + helpers)
python -m pytest domain/sap10_calculator/ --no-cov
# Cascade pins only (the conformance suite)
python -m pytest \
domain/sap10_calculator/worksheet/tests/test_section_cascade_pins.py \
domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \
--no-cov --no-header --tb=no -q
```
### Hard rules
These are non-negotiable per `[[feedback-zero-error-strict]]` /
`[[feedback-e2e-validation-philosophy]]`:
- `abs=1e-4` on every pin. **No `rel=…` tolerances, no widening, no xfail.**
- A failing pin is a real calculator bug or fixture defect — diagnose
before relaxing.
- Audit the fixture against the PDF **first** when a cascade pin fails
(many lodgements have been incomplete).
- `_round_half_up` at §15 RdSAP boundaries — never Python's banker's
`round()`.
- Cascade pins walk the real cert→inputs cascade end-to-end. Don't
isolate sections using PDF values as inputs.
---
## 6. Adding a new conformance fixture
See [`domain/sap10_calculator/README.md#adding-a-new-elmhurst-conformance-fixture`](../../domain/sap10_calculator/README.md#adding-a-new-elmhurst-conformance-fixture)
for the step-by-step cookbook. Summary:
1. Drop a fixture module at `worksheet/tests/_elmhurst_worksheet_NNNNNN.py`
2. Mirror the `Summary_NNNNNN.pdf` into `build_epc()`
3. Capture every populated worksheet line as `LINE_*` (Block 1, rating
cascade) + `DEMAND_LINE_*` (Block 2, demand cascade) constants
4. Register in `_elmhurst_fixtures.py`
5. Pins should all pass; if they don't, audit the fixture before
blaming the calculator.
---
## 7. Spec references at hand
```
SAP 10.2 (14-03-2025):
§7 Mean internal temperature p.28-32
§13 SAP rating equations p.38-39
§14 EI rating + Primary Energy p.43-44
Appendix J §2a Nbath p.81
Appendix J §8 electric shower p.82
Table J4 (shower flow/power) p.83
Table J5 (behavioural fbeh) p.83
Table 3a/3b/3c (HW combi loss) p.160-162
Table 9a/9b/9c (heating + utilisation) p.183-185
Table 12 (price/CO2/PEF annual) p.191
Table 12a (off-peak high-rate) p.191-192
Table 12d (monthly CO2 for electricity) p.194
Table 12e (monthly PE for electricity) p.195
Appendix U §U1/U2/U3 (region tables) p.124-127
Appendix U paragraph 1 (rating vs demand) p.124
RdSAP 10 (10-06-2025):
§3.1 precision rule p.16
§3.6 wall area p.19
§3.7.1 window area p.20
§3.8 roof area (max-floor) p.20
§3.9 RR simplified p.21
§3.10 RR detailed p.21
Table 4 (RR gable walls) p.22
§5.12 + Table 19 floor U p.46
§5.13 + Table 20 exposed floor p.47
§5.17 + Table 23 basement p.48
§5.18 curtain wall p.48
Table 24 (window U) p.50
§9.2 + Table 27 living area p.52
§15 rounding rules p.66
§19.2 RdSAP10 CO2/PE = SAP10.2 Table 12 p.94
Table 32 (fuel prices, CO2, PEF) p.95
Table 11 (secondary fraction) p.188
Table 12a (standing/off-peak) p.191
PCDB10:
Table 105 (gas/oil boilers) domain/sap10_calculator/docs/specs/pcdb_table_105_...
Table 172 (postcode-district weather) domain/sap10_calculator/tables/pcdb/data/pcdb10.dat
```
---
## 8. Elmhurst-mirrored spec divergences
The calculator's contract is **bit-faithful replication of the BRE-approved
Elmhurst rdSAP engine**, not literal compliance with the SAP 10.2 spec
text. The two coincide >99% of the time, but in a few places the
worksheet PDFs from Elmhurst lodge a value that the spec text — read in
isolation — would call wrong. We mirror the engine in those cases and
document the divergence here.
Trigger to ADD a row: cascade matches spec literal interpretation, but
worksheet PDF disagrees, AND the worksheet PDF value is reproducible
across multiple Elmhurst-lodged certs (i.e. it's the engine's behaviour,
not a one-off lodging defect). Per
[[feedback-software-no-special-handling]] / [[feedback-spec-floor-skepticism]]
verify both the worksheet PDF and the cascade output before adding.
### 8.1 HW PE/CO2 factors on dual-rate tariffs use Table 12 annual, not Table 12e/12d monthly
**Slice:** S0380.163.
**Code:**
[`_hot_water_primary_factor`](../rdsap/cert_to_inputs.py),
[`_hot_water_co2_factor_kg_per_kwh`](../rdsap/cert_to_inputs.py).
**Test:** `test_electric_water_heating_factors_use_annual_table_12_on_dual_rate_tariff`.
SAP 10.2 Table 12 footnote (t) (PDF p.189) reads:
> *PE factors for grid electricity vary by month. The average figure
> given in this table is therefore not used directly. Instead the
> monthly factors given in Table 12e should be used in the SAP
> worksheet.*
(Footnote (s) says the same for CO2 / Table 12d.) Read literally this
applies to every electric end-use including dual-rate HW. The cascade
originally followed the literal reading: Σ(HW_m × F_m_12e) / ΣHW_m =
~1.521 PE for 18-hour HW on a winter-skewed demand profile.
The Elmhurst worksheet ((278) "Water heating (low-rate cost)") uses
1.5010 PE / 0.136 CO2 — the Table 12 ANNUAL row — on every dual-rate
tariff cert in the 41-variant controlled-variable corpus. The engine
applies monthly Table 12e for lighting (1.5338 winter-weighted) and
secondary heating (1.5715) on the same certs, but flat Table 12 for the
"low-rate cost" line items (SH main 1 + HW). It's an Elmhurst
implementation choice, not a documented spec exception.
**Cascade rule (post-S0380.163):**
| Tariff | HW PE / CO2 factor source |
|---|---|
| STANDARD | Table 12e / 12d monthly, weighted by HW demand seasonality (per spec literal) |
| 7-hour / 10-hour / 18-hour / 24-hour | Table 12 annual flat (1.501 PE / 0.136 CO2) |
The SH main factor (`_main_heating_primary_factor`) already
matches Elmhurst by accident: for dual-rate tariffs the
`_table_12a_system_for_main` lookup returns None for storage heaters /
electric direct-acting / electric boilers without PCDB → falls through
to `primary_energy_factor(fuel)` annual. STANDARD tariff goes through
the monthly cascade.
### Cohort impact
The 41-variant heating-systems corpus closed its HW PE/CO2 residual on
18 variants (all dual-rate electric HW: electric 1/2/3/5/6/7/8/9, solid
fuel 4/5/6/7/8/9/10/11, ashp, gshp). Each variant moved from PE +25.51
or +48.66 → ±0.0000, CO2 +6.31 or +11.95 → ±0.0000. Cohort-1 ASHP certs
(STANDARD tariff) and the 6 Elmhurst U985 fixtures (gas combi, STANDARD
tariff) are unaffected — they continue to use the monthly cascade.
### 8.2 §12.4.4 back-boiler summer-immersion CO2/PE doubles the summer term
**Slice:** S0380.164.
**Code:**
[`_section_12_4_4_hw_blend`](../rdsap/cert_to_inputs.py).
**Tests:**
`test_section_12_4_4_hw_blend_mirrors_elmhurst_summer_annual_pe_co2_double_count`,
`test_section_12_4_4_hw_blend_standard_tariff_keeps_spec_literal_monthly_cascade`.
SAP 10.2 §12.4.4 (PDF p.36-37) routes DHW through the boiler Oct-May and
an electric immersion Jun-Sep for back-boiler combos (Table 4a codes
156 + 158). The spec-literal CO2/PE formula multiplies summer-immersion
fuel by the Table 12d / 12e monthly cascade (per Table 12 footnotes
(s)/(t)). The BRE-approved Elmhurst engine adds a SECOND term —
`summer_fuel × Table 12 ANNUAL electric factor` — on top of the
monthly cascade for the (264) HW CO2 and (278) HW PE worksheet lines on
dual-rate tariffs. Same shape as §8.1 / S0380.163 but additive rather
than substitutive.
**Cascade rule (post-S0380.164):**
| Tariff | §12.4.4 winter CO2 / PE | §12.4.4 summer immersion CO2 / PE |
|---|---|---|
| STANDARD | `W_fuel × boiler_annual_factor` | `Σ wh_summer_m × Table 12d/e monthly` (spec literal) |
| 7-hour / 10-hour / 18-hour / 24-hour | `W_fuel × boiler_annual_factor` | `Σ wh_summer_m × Table 12d/e monthly` **+ `S_fuel × Table 12 annual electric`** (Elmhurst mirror) |
Cost is computed cleanly per spec (`W_fuel × boiler_price + S_fuel ×
off_peak_low_price`) — the double-count quirk only affects the CO2 and
PE factor lines.
### Cohort impact
The heating-systems corpus has exactly one §12.4.4 fixture: `solid fuel 2`
(Table 4a code 158, anthracite, 18-hour tariff, 110 L cylinder + cyl
thermostat). Pre-slice the cascade carried ΔCO2 = 93.10 kg/yr / ΔPE
= 1027.51 kWh/yr — matching `684.55 kWh × 0.136 CO2` and
`684.55 kWh × 1.501 PE` to within rounding. Post-slice closes to
±0.0000 on all four metrics, completing the cohort closure at 25/25
cascade-OK variants EXACT vs the Elmhurst worksheet.
### ⚠ Single-cert evidence
The §12.4.4 divergence is documented here on **one** worksheet (SF2)
because the corpus has no second §12.4.4 fixture (`solid fuel 1` =
code 156 is an empty folder). The math nonetheless matches the
worksheet to within rounding and aligns with §8.1's S0380.163 mirror
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.