mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
The gov-API `main_fuel_type`/`water_heating_fuel` enum (epc_codes.csv) codes 30="waste combustion (community)", 31="biomass (community)", 32="biogas (community)" collide in VALUE with the Table-32 electricity codes 30 (standard rate), 31 (7-hour low) and 32 (7-hour high). All three sit in `_ELECTRIC_FUEL_CODES`, so `is_electric_fuel_code` flagged a community-scheme main as electric and `_is_electric_main` routed its cost through the off-peak electricity branch — BYPASSING the heat-network rate in `_heat_network_factor_fuel_code`. Cert 8536 (biomass community, SAP code 301) was billing at 5.5 p/kWh grid electricity instead of the 4.24 p/kWh heat-network rate → -17.2 SAP. Per RdSAP 10 §C / SAP 10.2 Table 12 (PDF p.191) the community waste/biomass/biogas rows are codes 42/43/44 (the same rows the backwards-compat enum codes 11/12/13 already map to). Add 30->42, 31->43, 32->44 to both API fuel-translation tables. The remap CANNOT be global (`canonical_fuel_code`): the cascade uses the bare Table-32 code 30 internally as `_STANDARD_ELECTRICITY_FUEL_CODE` (the RdSAP no-water-heating immersion default writes `water_heating_fuel=30`), so a blanket remap mis-prices genuine grid electricity as community waste (cert 2211 regressed +16 SAP in a prototype). Instead `_heat_network_community_fuel_code` translates only when `_is_heat_network_main` is true, at the `_main_fuel_code` / `_water_heating_fuel_code` fuel-TYPE boundary, where the community meaning is unambiguous. Per the strict-raise principle ([[reference-unmapped-sap-code]]), a heat-network main lodging a colliding community fuel the table doesn't cover raises `UnmappedSapCode` rather than silently falling through to the same-numbered electricity code. Eval (API SAP vs lodged): cert 8536 -17.25 -> -6.51, cert 5036 -6.29 -> +1.36; mean|err| 1.329 -> 1.312, within-1.0 67.88% -> 67.99%, within-2.0 81.74% -> 81.85%, within-0.5 held at 53.14%, 909 computed / 0 raises. No golden / calculator regressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
288 lines
13 KiB
Python
288 lines
13 KiB
Python
"""SAP 10.2 (14-03-2025 amendment) Table 12 — fuel prices, CO2 emission
|
||
factors, primary energy factors.
|
||
|
||
Sourced verbatim from BRE, *The Government's Standard Assessment
|
||
Procedure for Energy Rating of Dwellings, SAP 10.2* (14-03-2025), page
|
||
189 (Table 12). Keys are the SAP 10.2/10.3 fuel code numbers — they
|
||
remained stable across the 10.2 → 10.3 jump.
|
||
|
||
The calculator targets SAP 10.2 per ADR-0010 because no SAP-10.3-lodged
|
||
certs exist in the corpus to validate against. SAP 10.3 differs from
|
||
SAP 10.2 mainly on CO2 factors (grid electricity 0.136 → 0.086 kg/kWh,
|
||
−37%; mains gas 0.210 → 0.214 kg/kWh, +2%); prices and primary energy
|
||
factors are largely unchanged. When the corpus migrates to SAP 10.3
|
||
this module re-points to those values.
|
||
|
||
The Energy Cost Deflator stays at 0.36 (used in ECF — see
|
||
`domain.sap10_calculator.worksheet.rating`).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Final, Optional
|
||
|
||
|
||
# SAP 10.3 Table 12 — unit price in pence per kWh.
|
||
UNIT_PRICE_P_PER_KWH: Final[dict[int, float]] = {
|
||
# Gas fuels
|
||
1: 3.64, # mains gas
|
||
2: 6.74, # bulk LPG
|
||
3: 9.46, # bottled LPG (main heating)
|
||
5: 11.20, # bottled LPG (secondary)
|
||
9: 3.64, # LPG SC11F
|
||
7: 6.74, # biogas (including anaerobic digestion)
|
||
# Liquid fuels
|
||
4: 4.94, # heating oil
|
||
71: 6.79, # bio-liquid HVO
|
||
73: 6.79, # bio-liquid FAME
|
||
75: 5.49, # B30K
|
||
76: 47.0, # bioethanol
|
||
# Solid fuels
|
||
11: 5.58, # house coal
|
||
15: 4.19, # anthracite
|
||
12: 5.91, # manufactured smokeless fuel
|
||
20: 5.12, # wood logs
|
||
22: 6.91, # wood pellets (secondary)
|
||
23: 6.25, # wood pellets (main)
|
||
21: 3.72, # wood chips
|
||
10: 4.77, # dual fuel
|
||
# Electricity
|
||
30: 16.49, # standard tariff
|
||
32: 19.60, # 7-hour tariff (high rate)
|
||
31: 9.40, # 7-hour tariff (low rate / off-peak)
|
||
34: 20.54, # 10-hour tariff (high rate)
|
||
33: 12.27, # 10-hour tariff (low rate)
|
||
38: 17.41, # 18-hour tariff (high rate)
|
||
40: 14.17, # 18-hour tariff (low rate)
|
||
35: 14.04, # 24-hour heating tariff
|
||
60: 5.59, # electricity sold to grid, PV
|
||
36: 5.59, # electricity sold to grid, other
|
||
# 39 "electricity, any tariff" carries N/A unit price — used only to
|
||
# identify the fuel for a system; cost data comes from a paired
|
||
# standard / off-peak code.
|
||
# Heat networks
|
||
51: 4.44, 52: 4.44, 53: 4.44, 54: 4.44,
|
||
55: 4.44, 56: 4.44, 57: 4.44, 58: 4.44,
|
||
41: 4.44, # heat from electric heat pump
|
||
42: 4.44, # heat recovered from waste combustion
|
||
43: 4.44, # heat from boilers - biomass
|
||
44: 4.44, # heat from boilers - biogas
|
||
45: 3.11, # high grade heat recovered from process
|
||
46: 3.11, # heat recovered from geothermal / natural processes
|
||
48: 3.11, # heat from CHP
|
||
49: 3.11, # low grade heat recovered from process
|
||
50: 0.0, # electricity for pumping in distribution network
|
||
47: 3.11, # heat recovered from power station
|
||
}
|
||
_DEFAULT_P_PER_KWH: Final[float] = 3.64 # fall back to mains gas
|
||
|
||
|
||
# SAP 10.2 Table 12 — annual-average CO2 emission factor in kg CO2-
|
||
# equivalent per kWh of delivered energy. For ELECTRICITY end-uses,
|
||
# Table 12d (above) overrides this annual factor with monthly values per
|
||
# the spec text on p.194; the value here is the legacy fallback when
|
||
# monthly distribution isn't available.
|
||
# SAP 10.2 Table 12d (p.194) — monthly variation in CO2 emission factors
|
||
# for electricity. The spec text: "Where electricity is the fuel used, the
|
||
# relevant set of factors in the table below should be used to calculate
|
||
# the monthly CO2 emissions INSTEAD of the annual average factor given in
|
||
# Table 12." So for ratings, electricity end-uses use Σ(kWh_m × CO2_m)
|
||
# rather than annual_kwh × annual_factor.
|
||
CO2_KG_PER_KWH_MONTHLY: Final[dict[int, tuple[float, ...]]] = {
|
||
# Standard tariff (default electricity)
|
||
30: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163),
|
||
# 7-hour tariff
|
||
32: (0.171, 0.168, 0.161, 0.150, 0.138, 0.125, 0.117, 0.118, 0.128, 0.143, 0.158, 0.171),
|
||
31: (0.143, 0.141, 0.135, 0.126, 0.116, 0.105, 0.098, 0.099, 0.107, 0.120, 0.133, 0.144),
|
||
# 10-hour tariff
|
||
34: (0.168, 0.165, 0.159, 0.148, 0.136, 0.124, 0.115, 0.116, 0.126, 0.141, 0.156, 0.168),
|
||
33: (0.155, 0.153, 0.146, 0.137, 0.126, 0.114, 0.106, 0.107, 0.116, 0.130, 0.144, 0.155),
|
||
# 18-hour tariff (matches standard tariff)
|
||
38: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163),
|
||
40: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163),
|
||
# 24-hour heating tariff
|
||
35: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163),
|
||
# Electricity sold to grid (PV)
|
||
60: (0.196, 0.190, 0.175, 0.153, 0.129, 0.106, 0.092, 0.093, 0.110, 0.138, 0.169, 0.197),
|
||
# Electricity sold to grid, other
|
||
36: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163),
|
||
# Electricity, any tariff
|
||
39: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163),
|
||
# Heat from electric heat pump
|
||
41: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163),
|
||
# Low-grade heat recovered from process
|
||
49: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163),
|
||
# Electricity for pumping in distribution network
|
||
50: (0.163, 0.160, 0.153, 0.143, 0.132, 0.120, 0.111, 0.112, 0.122, 0.136, 0.151, 0.163),
|
||
}
|
||
|
||
|
||
def co2_monthly_factors_kg_per_kwh(fuel_code: int | None) -> Optional[tuple[float, ...]]:
|
||
"""SAP 10.2 Table 12d (p.194) monthly CO2 factors for electricity. Returns
|
||
None for non-electricity fuels (use the annual `co2_factor_kg_per_kwh`)."""
|
||
if fuel_code is None:
|
||
return None
|
||
if fuel_code in CO2_KG_PER_KWH_MONTHLY:
|
||
return CO2_KG_PER_KWH_MONTHLY[fuel_code]
|
||
return None
|
||
|
||
|
||
# SAP 10.2 Table 12e (p.195) — monthly variation in PE (primary energy)
|
||
# emission factors for electricity. Spec text: "Where electricity is the
|
||
# fuel used, the relevant set of factors in the table below should be
|
||
# used to calculate the monthly primary energy instead the annual average
|
||
# factor given in Table 12." Same shape as Table 12d (CO2): electricity
|
||
# end-uses use Σ(kWh_m × PE_m); gas/non-electricity fuels keep the
|
||
# annual Table 12 PE factor.
|
||
PE_FACTOR_MONTHLY: Final[dict[int, tuple[float, ...]]] = {
|
||
# Standard tariff
|
||
30: (1.602, 1.593, 1.568, 1.530, 1.487, 1.441, 1.410, 1.413, 1.449, 1.504, 1.558, 1.604),
|
||
# 7-hour tariff
|
||
32: (1.635, 1.626, 1.600, 1.562, 1.518, 1.471, 1.440, 1.443, 1.479, 1.535, 1.591, 1.637),
|
||
31: (1.521, 1.512, 1.488, 1.453, 1.411, 1.368, 1.339, 1.342, 1.376, 1.428, 1.480, 1.522),
|
||
# 10-hour tariff
|
||
34: (1.625, 1.615, 1.590, 1.552, 1.507, 1.462, 1.430, 1.433, 1.470, 1.525, 1.580, 1.626),
|
||
33: (1.571, 1.561, 1.537, 1.500, 1.457, 1.413, 1.382, 1.386, 1.421, 1.474, 1.528, 1.572),
|
||
# 18-hour tariff (matches standard tariff)
|
||
38: (1.602, 1.593, 1.568, 1.530, 1.487, 1.441, 1.410, 1.413, 1.449, 1.504, 1.558, 1.604),
|
||
40: (1.602, 1.593, 1.568, 1.530, 1.487, 1.441, 1.410, 1.413, 1.449, 1.504, 1.558, 1.604),
|
||
# 24-hour heating tariff
|
||
35: (1.602, 1.593, 1.568, 1.530, 1.487, 1.441, 1.410, 1.413, 1.449, 1.504, 1.558, 1.604),
|
||
# Electricity sold to grid (PV) — note (i): deducted, low PE factor
|
||
60: (0.715, 0.697, 0.645, 0.567, 0.478, 0.389, 0.330, 0.336, 0.405, 0.513, 0.623, 0.718),
|
||
# Electricity sold to grid, other
|
||
36: (0.602, 0.593, 0.568, 0.530, 0.487, 0.441, 0.410, 0.413, 0.449, 0.504, 0.558, 0.604),
|
||
# Electricity, any tariff
|
||
39: (1.602, 1.593, 1.568, 1.530, 1.487, 1.441, 1.410, 1.413, 1.449, 1.504, 1.558, 1.604),
|
||
# Heat from electric heat pump
|
||
41: (1.602, 1.593, 1.568, 1.530, 1.487, 1.441, 1.410, 1.413, 1.449, 1.504, 1.558, 1.604),
|
||
# Low-grade heat recovered from process
|
||
49: (1.602, 1.593, 1.568, 1.530, 1.487, 1.441, 1.410, 1.413, 1.449, 1.504, 1.558, 1.604),
|
||
# Electricity for pumping in distribution network
|
||
50: (1.602, 1.593, 1.568, 1.530, 1.487, 1.441, 1.410, 1.413, 1.449, 1.504, 1.558, 1.604),
|
||
}
|
||
|
||
|
||
def pe_monthly_factors_kwh_per_kwh(
|
||
fuel_code: int | None,
|
||
) -> Optional[tuple[float, ...]]:
|
||
"""SAP 10.2 Table 12e (p.195) monthly PE factors for electricity. Returns
|
||
None for non-electricity fuels (use the annual `primary_energy_factor`)."""
|
||
if fuel_code is None:
|
||
return None
|
||
if fuel_code in PE_FACTOR_MONTHLY:
|
||
return PE_FACTOR_MONTHLY[fuel_code]
|
||
return None
|
||
|
||
|
||
CO2_KG_PER_KWH: Final[dict[int, float]] = {
|
||
# Gas fuels
|
||
1: 0.210,
|
||
2: 0.241, 3: 0.241, 5: 0.241, 9: 0.241,
|
||
7: 0.024,
|
||
# Liquid fuels
|
||
4: 0.298,
|
||
71: 0.036, 73: 0.018,
|
||
75: 0.214, 76: 0.105,
|
||
# Solid fuels
|
||
11: 0.395, 15: 0.395, 12: 0.366,
|
||
20: 0.028, 22: 0.053, 23: 0.053, 21: 0.023,
|
||
10: 0.087,
|
||
# Electricity — all grid tariffs use the same annual-average CO2 factor.
|
||
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
|
||
# 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,
|
||
}
|
||
_DEFAULT_CO2_KG_PER_KWH: Final[float] = 0.210 # mains gas baseline
|
||
|
||
|
||
# Gov EPC API main_fuel_type → SAP 10.3 Table 12 fuel code. Lifted from
|
||
# the SAP 10.2 mapper (`domain.sap10_ml.sap_efficiencies._API_TO_TABLE32`) —
|
||
# the API enum and Table 32/12 codes are unchanged across spec versions.
|
||
API_FUEL_TO_TABLE_12: Final[dict[int, int]] = {
|
||
0: 30, 1: 1, 2: 2, 3: 3, 4: 4, 5: 15, 6: 20, 7: 23, 8: 21, 9: 10,
|
||
10: 30, 11: 42, 12: 43, 13: 44, 14: 11, 15: 12, 16: 22, 17: 9,
|
||
18: 75, 19: 76, 20: 51, 21: 52, 22: 53, 23: 55, 24: 54, 25: 41,
|
||
26: 1, 27: 2, 28: 4, 29: 30, 30: 42, 31: 43, 32: 44, 33: 11,
|
||
}
|
||
|
||
|
||
def unit_price_p_per_kwh(fuel_code: int | None) -> float:
|
||
"""Unit price (p/kWh) for the given fuel code. Accepts either a
|
||
Table 12 code or a gov API main_fuel_type / water_heating_fuel
|
||
enum; translates the latter via `API_FUEL_TO_TABLE_12`. Unknown →
|
||
mains gas (3.64 p/kWh)."""
|
||
if fuel_code is None:
|
||
return _DEFAULT_P_PER_KWH
|
||
if fuel_code in UNIT_PRICE_P_PER_KWH:
|
||
return UNIT_PRICE_P_PER_KWH[fuel_code]
|
||
translated = API_FUEL_TO_TABLE_12.get(fuel_code)
|
||
if translated is not None and translated in UNIT_PRICE_P_PER_KWH:
|
||
return UNIT_PRICE_P_PER_KWH[translated]
|
||
return _DEFAULT_P_PER_KWH
|
||
|
||
|
||
# SAP 10.2 Table 12 "Primary energy factor" column. The cert's
|
||
# `energy_consumption_current` field (PEUI) is delivered energy times
|
||
# this factor per fuel, summed across end-uses, divided by TFA.
|
||
PRIMARY_ENERGY_FACTOR: Final[dict[int, float]] = {
|
||
# Gas
|
||
1: 1.130,
|
||
2: 1.141, 3: 1.141, 5: 1.133, 9: 1.163,
|
||
7: 1.286,
|
||
# Liquid
|
||
4: 1.180,
|
||
71: 1.180, 73: 1.180, 75: 1.136, 76: 1.472,
|
||
# Solid
|
||
11: 1.064, 15: 1.064, 12: 1.261, 20: 1.046,
|
||
22: 1.325, 23: 1.325, 21: 1.046, 10: 1.049,
|
||
# Electricity — all grid tariffs same PEF.
|
||
30: 1.501, 31: 1.501, 32: 1.501, 33: 1.501, 34: 1.501, 35: 1.501,
|
||
38: 1.501, 40: 1.501, 39: 1.501, 60: 0.501, 36: 0.501,
|
||
# Heat networks (sample — main values; less common)
|
||
51: 1.130, 52: 1.141, 53: 1.180, 54: 1.064, 55: 1.180,
|
||
56: 1.180, 57: 1.180, 58: 1.180,
|
||
41: 1.501, 42: 0.063, 43: 1.037, 44: 1.286,
|
||
45: 0.051, 46: 0.051, 47: 0.063, 48: 1.501, 49: 1.501,
|
||
50: 0.0,
|
||
}
|
||
_DEFAULT_PEF: Final[float] = 1.130 # mains gas baseline
|
||
|
||
|
||
def primary_energy_factor(fuel_code: int | None) -> float:
|
||
"""Primary energy factor for the given fuel code, accepting either
|
||
Table 12 code or gov API enum (translated). Unknown → mains gas
|
||
(1.13)."""
|
||
if fuel_code is None:
|
||
return _DEFAULT_PEF
|
||
if fuel_code in PRIMARY_ENERGY_FACTOR:
|
||
return PRIMARY_ENERGY_FACTOR[fuel_code]
|
||
translated = API_FUEL_TO_TABLE_12.get(fuel_code)
|
||
if translated is not None and translated in PRIMARY_ENERGY_FACTOR:
|
||
return PRIMARY_ENERGY_FACTOR[translated]
|
||
return _DEFAULT_PEF
|
||
|
||
|
||
def co2_factor_kg_per_kwh(fuel_code: int | None) -> float:
|
||
"""CO2 emission factor (kg CO2e/kWh) for the given fuel code, with
|
||
the same accept-either-API-or-Table-12-code translation as
|
||
`unit_price_p_per_kwh`. Unknown → mains gas (0.214)."""
|
||
if fuel_code is None:
|
||
return _DEFAULT_CO2_KG_PER_KWH
|
||
if fuel_code in CO2_KG_PER_KWH:
|
||
return CO2_KG_PER_KWH[fuel_code]
|
||
translated = API_FUEL_TO_TABLE_12.get(fuel_code)
|
||
if translated is not None and translated in CO2_KG_PER_KWH:
|
||
return CO2_KG_PER_KWH[translated]
|
||
return _DEFAULT_CO2_KG_PER_KWH
|