mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
slice S-B22: primary energy in SapResult + Table 12 PEF column
Wires SAP 10.2 Table 12 "Primary energy factor" column into Table 12 helpers and onto CalculatorInputs as three per-end-use factors (space heating, hot water, other). calculate_sap_from_inputs now emits primary_energy_kwh_per_yr and primary_energy_kwh_per_m2 on SapResult, matching the cert's `energy_consumption_current` field (PEUI). Triggered by a decomposition that revealed I'd been comparing our delivered energy to the cert's primary energy — apples to oranges. With proper primary-energy comparison the actual finding is: 300-cert primary-energy diff (cert calibration prices): energy MAE: 57.3 kWh/m² energy bias: +51.6 (we over-predict by ~50%) energy P50: +49.5 This is a much bigger systemic bug than the SAP MAE 5.34 suggested. Closing it requires investigating either (a) demand model over-prediction, (b) HW losses, (c) PEF values per fuel, or (d) cert reporting convention differences. Targeted for the next context. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
7786a6e9b7
commit
2a9999bdf6
4 changed files with 86 additions and 1 deletions
|
|
@ -105,6 +105,13 @@ class CalculatorInputs:
|
|||
hot_water_fuel_cost_gbp_per_kwh: float
|
||||
other_fuel_cost_gbp_per_kwh: float
|
||||
co2_factor_kg_per_kwh: float
|
||||
# Primary energy factors per end-use (Table 12 "Primary energy factor"
|
||||
# column). Used by §14 to derive the cert's `energy_consumption_current`
|
||||
# (which is PRIMARY energy per m²). For a single-fuel dwelling all
|
||||
# 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)
|
||||
# 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).
|
||||
|
|
@ -153,6 +160,8 @@ class SapResult:
|
|||
hot_water_kwh_per_yr: float
|
||||
pumps_fans_kwh_per_yr: float
|
||||
lighting_kwh_per_yr: float
|
||||
primary_energy_kwh_per_yr: float
|
||||
primary_energy_kwh_per_m2: float
|
||||
monthly: tuple[MonthlyEntry, ...]
|
||||
|
||||
|
||||
|
|
@ -306,6 +315,22 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
sap_cont = sap_rating(ecf=ecf)
|
||||
co2 = delivered_fuel_kwh * inputs.co2_factor_kg_per_kwh
|
||||
|
||||
primary_energy_kwh = (
|
||||
main_fuel_kwh * inputs.space_heating_primary_factor
|
||||
+ secondary_fuel_kwh * inputs.space_heating_primary_factor
|
||||
+ inputs.hot_water_kwh_per_yr * inputs.hot_water_primary_factor
|
||||
+ (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).
|
||||
primary_energy_kwh = max(
|
||||
0.0,
|
||||
primary_energy_kwh - inputs.pv_generation_kwh_per_yr * inputs.other_primary_factor,
|
||||
)
|
||||
primary_energy_per_m2 = primary_energy_kwh / tfa if tfa > 0 else 0.0
|
||||
|
||||
return SapResult(
|
||||
sap_score=sap_int,
|
||||
sap_score_continuous=sap_cont,
|
||||
|
|
@ -318,6 +343,8 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
hot_water_kwh_per_yr=inputs.hot_water_kwh_per_yr,
|
||||
pumps_fans_kwh_per_yr=inputs.pumps_fans_kwh_per_yr,
|
||||
lighting_kwh_per_yr=inputs.lighting_kwh_per_yr,
|
||||
primary_energy_kwh_per_yr=primary_energy_kwh,
|
||||
primary_energy_kwh_per_m2=primary_energy_per_m2,
|
||||
monthly=monthly,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,11 @@ from domain.ml.sap_efficiencies import (
|
|||
water_heating_efficiency as _legacy_water_heating_efficiency,
|
||||
)
|
||||
from domain.sap.calculator import CalculatorInputs, WindowInput
|
||||
from domain.sap.tables.table_12 import co2_factor_kg_per_kwh, unit_price_p_per_kwh
|
||||
from domain.sap.tables.table_12 import (
|
||||
co2_factor_kg_per_kwh,
|
||||
primary_energy_factor,
|
||||
unit_price_p_per_kwh,
|
||||
)
|
||||
from domain.sap.worksheet.dimensions import dimensions_from_cert
|
||||
from domain.sap.worksheet.heat_transmission import (
|
||||
DwellingExposure,
|
||||
|
|
@ -806,4 +810,9 @@ def cert_to_inputs(
|
|||
secondary_heating_fuel_cost_gbp_per_kwh=_secondary_fuel_cost_gbp_per_kwh(
|
||||
epc.sap_heating, main, epc.sap_energy_source.meter_type, prices
|
||||
),
|
||||
space_heating_primary_factor=primary_energy_factor(main_fuel),
|
||||
hot_water_primary_factor=primary_energy_factor(
|
||||
epc.sap_heating.water_heating_fuel or main_fuel
|
||||
),
|
||||
other_primary_factor=primary_energy_factor(30), # standard electricity
|
||||
)
|
||||
|
|
|
|||
|
|
@ -132,6 +132,47 @@ def unit_price_p_per_kwh(fuel_code: int | None) -> float:
|
|||
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
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ def main(argv: list[str] | None = None) -> None:
|
|||
epc = EpcPropertyDataMapper.from_api_response(document)
|
||||
inputs = cert_to_inputs(epc, prices=prices)
|
||||
result = calculate_sap_from_inputs(inputs)
|
||||
cert_primary = epc.energy_consumption_current
|
||||
results.append({
|
||||
"cert": cn,
|
||||
"actual": actual,
|
||||
|
|
@ -83,6 +84,13 @@ def main(argv: list[str] | None = None) -> None:
|
|||
"tfa": epc.total_floor_area_m2,
|
||||
"ext": epc.extensions_count,
|
||||
"dwelling": epc.dwelling_type,
|
||||
"our_primary_kwh_m2": round(result.primary_energy_kwh_per_m2, 1),
|
||||
"cert_primary_kwh_m2": cert_primary,
|
||||
"primary_resid": (
|
||||
round(result.primary_energy_kwh_per_m2 - cert_primary, 1)
|
||||
if cert_primary is not None
|
||||
else None
|
||||
),
|
||||
})
|
||||
except Exception as e: # noqa: BLE001 — exploratory probe
|
||||
errors.append({"cert": cn, "actual": actual, "error": f"{type(e).__name__}: {e}"})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue