mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
slice S-B7: per-end-use fuel cost — HW uses water-fuel, lighting always electric
SAP 10.3 §12 charges fuel costs by end-use, not by main heating fuel.
For a gas-heated dwelling with an electric immersion hot-water cylinder,
HW bills at the electric rate (13.19 p/kWh) not the gas main-heating
rate (3.48 p/kWh) — a 3.8× cost difference for HW that propagates
straight to ECF. Lighting, central-heating pumps, and fans always
electric regardless of main fuel.
Discovered by hand-tracing cert 8035-9023 (Detached bungalow, actual
SAP 43, predicted 63). Trace showed our hot-water + lighting + pumps
lines were charging mains-gas rates throughout, under-counting cost by
~£290/yr.
100-cert parity probe (biggest single Session-B slice so far):
MAE 5.70 → 4.90 (-0.80, -14%)
RMSE 7.48 → 6.68 (-11%)
within ±1: 20% → 24%
within ±3: 37% → 46%
within ±5: 54% → 67%
bias +1.50 → -1.44 (over-corrected by ~3 SAP points)
The over-correction (bias now slightly negative) means we're now
under-predicting on average. Next slice tackles where we're charging
too much electricity — probably HW on dwellings with combi boilers (no
immersion, water still on main fuel) and the water_heating_code 901
("from main system") inheritance path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
29c776bb23
commit
aa2c7a9171
2 changed files with 48 additions and 12 deletions
|
|
@ -160,6 +160,12 @@ _DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K: Final[float] = 250.0
|
|||
_DEFAULT_PUMPS_FANS_KWH_PER_YR: Final[float] = 130.0
|
||||
|
||||
|
||||
# SAP 10.3 §12: lighting + central-heating pumps + fans always bill at
|
||||
# the standard-electricity rate regardless of the main heating fuel —
|
||||
# Table 32 code 30 (standard electricity), 13.19 p/kWh.
|
||||
_STANDARD_ELECTRICITY_P_PER_KWH: Final[float] = 13.19
|
||||
|
||||
|
||||
# SAP 10.3 Table 9 main_heating_control codes → control type (1/2/3).
|
||||
# Type 1: no time + temp control, or one but not both.
|
||||
# Type 2: programmer + room thermostat (+/− TRVs).
|
||||
|
|
@ -374,6 +380,25 @@ def _space_heating_fuel_cost_gbp_per_kwh(main: Optional[MainHeatingDetail]) -> f
|
|||
return _fuel_cost_gbp_per_kwh(main)
|
||||
|
||||
|
||||
def _hot_water_fuel_cost_gbp_per_kwh(
|
||||
main: Optional[MainHeatingDetail], water_heating_fuel: Optional[int]
|
||||
) -> float:
|
||||
"""Hot water bills at the *water-heating* fuel's rate — distinct from
|
||||
the main heating fuel for gas-heated dwellings whose DHW runs off an
|
||||
electric immersion. Falls back to the main fuel when the cert
|
||||
doesn't lodge a separate water fuel."""
|
||||
if water_heating_fuel is not None:
|
||||
return fuel_unit_price_p_per_kwh(water_heating_fuel) * _PENCE_TO_GBP
|
||||
return _fuel_cost_gbp_per_kwh(main)
|
||||
|
||||
|
||||
def _other_fuel_cost_gbp_per_kwh() -> float:
|
||||
"""Pumps, fans, and lighting always bill at the standard-electricity
|
||||
rate (SAP 10.3 §12; Table 32 code 30) regardless of the main heating
|
||||
fuel — these end uses are electric in every UK dwelling."""
|
||||
return _STANDARD_ELECTRICITY_P_PER_KWH * _PENCE_TO_GBP
|
||||
|
||||
|
||||
|
||||
|
||||
def _co2_factor_kg_per_kwh(main: Optional[MainHeatingDetail]) -> float:
|
||||
|
|
@ -495,7 +520,9 @@ def cert_to_inputs(epc: EpcPropertyData) -> CalculatorInputs:
|
|||
pumps_fans_kwh_per_yr=_DEFAULT_PUMPS_FANS_KWH_PER_YR,
|
||||
lighting_kwh_per_yr=lighting_kwh,
|
||||
space_heating_fuel_cost_gbp_per_kwh=_space_heating_fuel_cost_gbp_per_kwh(main),
|
||||
hot_water_fuel_cost_gbp_per_kwh=_fuel_cost_gbp_per_kwh(main),
|
||||
other_fuel_cost_gbp_per_kwh=_fuel_cost_gbp_per_kwh(main),
|
||||
hot_water_fuel_cost_gbp_per_kwh=_hot_water_fuel_cost_gbp_per_kwh(
|
||||
main, epc.sap_heating.water_heating_fuel
|
||||
),
|
||||
other_fuel_cost_gbp_per_kwh=_other_fuel_cost_gbp_per_kwh(),
|
||||
co2_factor_kg_per_kwh=_co2_factor_kg_per_kwh(main),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -203,19 +203,27 @@ def test_main_heating_efficiency_reads_sap_main_heating_code() -> None:
|
|||
assert inputs_lo.main_heating_efficiency == 0.70
|
||||
|
||||
|
||||
def test_mains_gas_fuel_cost_in_gbp_per_kwh() -> None:
|
||||
# Arrange — Table 12 mains-gas unit price is 3.48 p/kWh; mapper must
|
||||
# report this as £0.0348/kWh (decimal-pound, not pence). For mains-
|
||||
# gas dwellings all three end-use fuel costs collapse to the same value.
|
||||
epc = _typical_semi_detached_epc()
|
||||
def test_gas_heating_with_electric_immersion_charges_hw_at_electricity_rate() -> None:
|
||||
# Arrange — Default test fixture: mains-gas main heating but the
|
||||
# SapHeating fixture uses water_heating_fuel=26 (also mains gas) so
|
||||
# all three lines collapse to gas. Override water_heating_fuel to 29
|
||||
# (electricity) to verify the mapper picks the water fuel rate.
|
||||
gas_only = _typical_semi_detached_epc()
|
||||
electric_hw = _typical_semi_detached_epc()
|
||||
electric_hw.sap_heating.water_heating_fuel = 29 # electricity
|
||||
|
||||
# Act
|
||||
inputs = cert_to_inputs(epc)
|
||||
inputs_gas = cert_to_inputs(gas_only)
|
||||
inputs_hw = cert_to_inputs(electric_hw)
|
||||
|
||||
# Assert
|
||||
assert inputs.space_heating_fuel_cost_gbp_per_kwh == 0.0348
|
||||
assert inputs.hot_water_fuel_cost_gbp_per_kwh == 0.0348
|
||||
assert inputs.other_fuel_cost_gbp_per_kwh == 0.0348
|
||||
# Assert — gas main → space heating at gas rate; HW switches to electric
|
||||
# rate when water_heating_fuel is electric; lighting/pumps always electric.
|
||||
assert inputs_gas.space_heating_fuel_cost_gbp_per_kwh == 0.0348
|
||||
assert inputs_gas.hot_water_fuel_cost_gbp_per_kwh == 0.0348
|
||||
assert inputs_gas.other_fuel_cost_gbp_per_kwh == 0.1319
|
||||
assert inputs_hw.space_heating_fuel_cost_gbp_per_kwh == 0.0348
|
||||
assert inputs_hw.hot_water_fuel_cost_gbp_per_kwh == 0.1319
|
||||
assert inputs_hw.other_fuel_cost_gbp_per_kwh == 0.1319
|
||||
|
||||
|
||||
def test_main_heating_control_code_maps_to_sap_control_type() -> None:
|
||||
|
|
@ -309,6 +317,7 @@ def test_electric_storage_heater_space_heating_at_off_peak_rate() -> None:
|
|||
),
|
||||
],
|
||||
sap_heating=make_sap_heating(
|
||||
water_heating_fuel=29, # all-electric house: water is also electric
|
||||
main_heating_details=[
|
||||
MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue