mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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>
528 lines
24 KiB
Markdown
528 lines
24 KiB
Markdown
# 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.
|