mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 33: §13a Primary Energy — Table 12e monthly cascade wiring
Adds Table 12e (p.195) monthly PE factors for electricity to `tables/table_12.py` + `pe_monthly_factors_kwh_per_kwh(fuel_code)` helper. Mirrors slice 32's CO2 cascade — same spec text, same shape: electricity end-uses use Σ(kWh_m × PE_m); non-electricity fuels keep the annual Table 12 / RdSAP10 Table 32 (p.95) factor. Calculator now consumes per-end-use PE factors on `CalculatorInputs` (`secondary_heating_primary_factor`, `pumps_fans_primary_factor`, `lighting_primary_factor`, `electric_shower_primary_factor`). Defaults to None → fall back to the global `space_heating_primary_factor` / `other_primary_factor` (synthetic path). Fixes the stale 1.969 default to RdSAP10 Table 32 standard-electricity PE = 1.501. `_effective_monthly_factor(monthly_kwh, monthly_factors)` generalises the slice-32 weighting helper; `_effective_monthly_co2_factor` and the new `_effective_monthly_pe_factor` are thin wrappers over it. Includes the electric-shower kWh in the PE total — closes the audit loop opened by slice 30 (electric shower had fuel cost + CO2 but no PE contribution). §13a cascade pins NOT added — §13a appears only in the Demand-SAP block (postcode climate); our cascade pins live against the Rating-SAP block (UK-average climate). The Demand-SAP postcode cascade is a separate scope, intentionally deferred. The calculator's existing `primary_energy_kwh_per_yr` SapResult output now uses the spec-correct PE factors but stays UK-average climate. Verification (000474): pumps_fans effective PE factor = 1.5128 (PDF: 1.5128 ✓) lighting effective PE factor = 1.5338 (PDF: 1.5338 ✓) pumps_fans PE = 242.0480 kWh (PDF: 242.0480 ✓) lighting PE = 214.6527 kWh (PDF: 214.6527 ✓) Wider regression: 1490/1490 PASS — zero failures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
fc1b009bf9
commit
729229ed61
3 changed files with 147 additions and 16 deletions
|
|
@ -190,7 +190,20 @@ class CalculatorInputs:
|
|||
# three collapse to the same value.
|
||||
space_heating_primary_factor: float = 1.0
|
||||
hot_water_primary_factor: float = 1.0
|
||||
other_primary_factor: float = 1.969 # standard-electricity PEF (SAP 10.2)
|
||||
# Standard-electricity PE factor per RdSAP10 Table 32 (p.95) / SAP10.2
|
||||
# Table 12 = 1.501. Table 12e (p.195) provides monthly overrides — see
|
||||
# the per-end-use PE factor fields below for the monthly cascade.
|
||||
other_primary_factor: float = 1.501
|
||||
# Per-end-use effective PE factors. For electricity end-uses with known
|
||||
# monthly kWh distribution, cert_to_inputs computes the days-weighted
|
||||
# Table 12e factor Σ(kWh_m × PE_m) / Σ(kWh_m). Gas end-uses keep the
|
||||
# annual Table 12 factor. None → calculator falls back to the global
|
||||
# `space_heating_primary_factor` / `hot_water_primary_factor` /
|
||||
# `other_primary_factor` (legacy synthetic path).
|
||||
secondary_heating_primary_factor: Optional[float] = None
|
||||
pumps_fans_primary_factor: Optional[float] = None
|
||||
lighting_primary_factor: Optional[float] = None
|
||||
electric_shower_primary_factor: Optional[float] = None
|
||||
# Generation offsets — applied as a cost credit against the ECF
|
||||
# numerator. SAP 10.2 Appendix M: PV self-consumption + export
|
||||
# collapse to a single credit at the export rate (Table 12 code 60).
|
||||
|
|
@ -460,16 +473,44 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
+ electric_shower_co2
|
||||
)
|
||||
|
||||
# Per-end-use effective PE factors. Same shape as the CO2 cascade:
|
||||
# electricity end-uses use Table 12e (p.195) monthly factors weighted
|
||||
# by per-month kWh; gas end-uses use the annual Table 12 / Table 32
|
||||
# PE factor. Defaults fall back to the legacy single-factor path so
|
||||
# synthetic CalculatorInputs constructions keep working.
|
||||
secondary_primary_factor = (
|
||||
inputs.secondary_heating_primary_factor
|
||||
if inputs.secondary_heating_primary_factor is not None
|
||||
else inputs.space_heating_primary_factor
|
||||
)
|
||||
pumps_fans_primary_factor = (
|
||||
inputs.pumps_fans_primary_factor
|
||||
if inputs.pumps_fans_primary_factor is not None
|
||||
else inputs.other_primary_factor
|
||||
)
|
||||
lighting_primary_factor = (
|
||||
inputs.lighting_primary_factor
|
||||
if inputs.lighting_primary_factor is not None
|
||||
else inputs.other_primary_factor
|
||||
)
|
||||
electric_shower_primary_factor = (
|
||||
inputs.electric_shower_primary_factor
|
||||
if inputs.electric_shower_primary_factor is not None
|
||||
else inputs.other_primary_factor
|
||||
)
|
||||
space_heating_primary_kwh = (
|
||||
main_fuel_kwh + secondary_fuel_kwh
|
||||
) * inputs.space_heating_primary_factor
|
||||
main_fuel_kwh * inputs.space_heating_primary_factor
|
||||
+ secondary_fuel_kwh * secondary_primary_factor
|
||||
)
|
||||
hot_water_primary_kwh = inputs.hot_water_kwh_per_yr * inputs.hot_water_primary_factor
|
||||
other_primary_kwh = (
|
||||
inputs.pumps_fans_kwh_per_yr + inputs.lighting_kwh_per_yr
|
||||
) * inputs.other_primary_factor
|
||||
# PV offsets primary energy at the same PEF (Appendix M: export PEF =
|
||||
# standard-electricity PEF for ratings, since the displaced grid kWh
|
||||
# would have been imported electricity).
|
||||
inputs.pumps_fans_kwh_per_yr * pumps_fans_primary_factor
|
||||
+ inputs.lighting_kwh_per_yr * lighting_primary_factor
|
||||
+ inputs.electric_shower_kwh_per_yr * electric_shower_primary_factor
|
||||
)
|
||||
# PV offsets primary energy at the export PEF (Table 32 code 60 =
|
||||
# 0.501 — half the import PEF since exported kWh isn't subject to the
|
||||
# full grid-loss multiplier).
|
||||
pv_primary_offset_kwh = inputs.pv_generation_kwh_per_yr * inputs.other_primary_factor
|
||||
primary_energy_kwh = max(
|
||||
0.0,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ from domain.sap.tables.pcdb.parser import GasOilBoilerRecord
|
|||
from domain.sap.tables.table_12 import (
|
||||
co2_monthly_factors_kg_per_kwh,
|
||||
co2_factor_kg_per_kwh,
|
||||
pe_monthly_factors_kwh_per_kwh,
|
||||
primary_energy_factor,
|
||||
unit_price_p_per_kwh,
|
||||
)
|
||||
|
|
@ -728,15 +729,16 @@ def _water_efficiency_with_category_inherit(
|
|||
|
||||
|
||||
|
||||
def _effective_monthly_co2_factor(
|
||||
monthly_kwh: tuple[float, ...], fuel_code: int
|
||||
def _effective_monthly_factor(
|
||||
monthly_kwh: tuple[float, ...],
|
||||
monthly_factors: Optional[tuple[float, ...]],
|
||||
) -> Optional[float]:
|
||||
"""SAP 10.2 Table 12d (p.194): for electricity end-uses, the effective
|
||||
annual CO2 factor is Σ(kWh_m × CO2_m) / Σ(kWh_m). Returns None for non-
|
||||
electricity fuels or when total kWh is zero (caller falls back to the
|
||||
annual Table 12 factor). Used to translate monthly Table 12d cascade
|
||||
into the calculator's annual × factor shape without restructuring."""
|
||||
monthly_factors = co2_monthly_factors_kg_per_kwh(fuel_code)
|
||||
"""Days-weighted effective annual factor = Σ(kWh_m × factor_m) / Σ kWh_m.
|
||||
|
||||
Used to translate SAP 10.2 Table 12d (CO2) and Table 12e (PE) monthly
|
||||
cascades into the calculator's annual × factor shape. Returns None
|
||||
when factors are None (non-electricity fuel — caller falls back to the
|
||||
annual Table 12 factor) or when total kWh is zero."""
|
||||
if monthly_factors is None:
|
||||
return None
|
||||
total_kwh = sum(monthly_kwh)
|
||||
|
|
@ -745,6 +747,26 @@ def _effective_monthly_co2_factor(
|
|||
return sum(k * f for k, f in zip(monthly_kwh, monthly_factors)) / total_kwh
|
||||
|
||||
|
||||
def _effective_monthly_co2_factor(
|
||||
monthly_kwh: tuple[float, ...], fuel_code: int
|
||||
) -> Optional[float]:
|
||||
"""SAP 10.2 Table 12d (p.194) monthly CO2 cascade. Thin wrapper over
|
||||
`_effective_monthly_factor` for the CO2 lookup."""
|
||||
return _effective_monthly_factor(
|
||||
monthly_kwh, co2_monthly_factors_kg_per_kwh(fuel_code)
|
||||
)
|
||||
|
||||
|
||||
def _effective_monthly_pe_factor(
|
||||
monthly_kwh: tuple[float, ...], fuel_code: int
|
||||
) -> Optional[float]:
|
||||
"""SAP 10.2 Table 12e (p.195) monthly PE cascade. Thin wrapper over
|
||||
`_effective_monthly_factor` for the PE lookup."""
|
||||
return _effective_monthly_factor(
|
||||
monthly_kwh, pe_monthly_factors_kwh_per_kwh(fuel_code)
|
||||
)
|
||||
|
||||
|
||||
def _days_in_month_proportioned(
|
||||
annual_kwh: float, days_in_month: tuple[int, ...]
|
||||
) -> tuple[float, ...]:
|
||||
|
|
@ -2019,6 +2041,26 @@ def cert_to_inputs(
|
|||
epc.sap_heating.water_heating_fuel or main_fuel
|
||||
),
|
||||
other_primary_factor=primary_energy_factor(30), # standard electricity
|
||||
# SAP 10.2 Table 12e (p.195) per-end-use effective PE factors. Same
|
||||
# shape as the Table 12d CO2 cascade: electricity end-uses use the
|
||||
# monthly factors weighted by per-month kWh; gas end-uses pass
|
||||
# through the annual Table 12 / Table 32 PE factor. Secondary
|
||||
# defaults to standard electricity per RdSAP §A.2.2.
|
||||
secondary_heating_primary_factor=_effective_monthly_pe_factor(
|
||||
energy_requirements_result.secondary_fuel_monthly_kwh,
|
||||
_STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
),
|
||||
pumps_fans_primary_factor=_effective_monthly_pe_factor(
|
||||
_days_in_month_proportioned(pumps_fans_kwh, _DAYS_IN_MONTH),
|
||||
_STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
),
|
||||
lighting_primary_factor=_effective_monthly_pe_factor(
|
||||
lighting_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
),
|
||||
electric_shower_primary_factor=_effective_monthly_pe_factor(
|
||||
wh_result.electric_shower_monthly_kwh if wh_result is not None else (0.0,) * 12,
|
||||
_STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
),
|
||||
fuel_cost=_fuel_cost(
|
||||
epc=epc,
|
||||
main=main,
|
||||
|
|
|
|||
|
|
@ -127,6 +127,54 @@ def co2_monthly_factors_kg_per_kwh(fuel_code: int | None) -> Optional[tuple[floa
|
|||
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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue