mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Sibling migration to the sap10_calculator move — `domain.ml` now lives
at the root-level layout (`domain/sap10_ml/`) matching the pattern
already used by `domain.addresses`, `domain.tasks`, `domain.postcode`,
and `domain.sap10_calculator`.
Changes:
- `git mv packages/domain/src/domain/ml → domain/sap10_ml` (19 files;
history preserved).
- Subpackage rename: `domain.ml` → `domain.sap10_ml`. 32 references
rewritten across .py and .md files: 11 internal + 21 external
(datatypes/epc/domain/mapper.py, 14 files in domain/sap10_calculator,
2 backend tests, 2 ADRs, 1 README, 1 design doc).
- Path-string updates: `pytest.ini` testpath
`packages/domain/src/domain/ml/tests` → `domain/sap10_ml/tests` so
ML tests stay in the default auto-discovered sweep. `CONTEXT.md`
also updated.
`packages/domain/src/domain/` is now empty — the workspace `domain/`
tree has been fully migrated. Together with the `domain/__init__.py`
deletions from the sap10_calculator commit (29ac35cc), `domain` is
now a single root-level namespace package with subpackages
{addresses, sap10_calculator, sap10_ml, tasks} + the standalone
`postcode.py` module.
Verified:
- Focused sweep (backend mapper-chain + sap10_calculator worksheet
e2e + golden fixtures): 99 passed / 19 failed — identical baseline.
- Wider sweep (all sap10_calculator + sap10_ml): 1654 passed / 20
failed (same pre-existing failures).
- domain/sap10_ml/tests: 210/210 PASSED at new path.
- Pyright net-zero: heat_transmission.py 13, cert_to_inputs.py 35,
mapper.py 33, rdsap_uvalues.py 1 (all unchanged from baseline).
Note: `packages/domain/pyproject.toml` still declares
`packages = ["src/domain"]` for the hatchling wheel — that target
directory is now empty and the wheel build is effectively a no-op.
Retiring the workspace package or repointing the wheel is a follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
282 lines
13 KiB
Python
282 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
|
||
51: 0.210, 52: 0.241, 53: 0.298, 54: 0.375, 55: 0.269,
|
||
56: 0.298, 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,
|
||
}
|
||
|
||
|
||
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
|