mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.103: MEV fans cost split via Table 12a Grid 2 FANS_FOR_MECH_VENT rate (SAP 10.2 Table 12a)
SAP 10.2 Table 12a Grid 2 (PDF p.191) splits off-peak electricity
costs into two categories:
Other electricity uses Tariff Fraction at
high rate
Fans for mechanical ventilation systems 7-hour 0.71
10-hour 0.58
All other uses, and locally generated 7-hour 0.90
electricity 10-hour 0.80
Cert 000565 (Dual meter, 10-hour off-peak, MEV decentralised) lodges
127.5159 kWh of MEV-fan electricity (line 230a) that bills at the
`FANS_FOR_MECH_VENT` blend (0.58 × 14.68 + 0.42 × 7.50 = 11.6644
p/kWh), distinct from the 125 kWh of other pumps_fans (45 kWh gas-
boiler flue fan + 80 kWh solar HW pump) which bills at the
`ALL_OTHER_USES` blend (0.80 × 14.68 + 0.20 × 7.50 = 13.2440 p/kWh).
Pre-slice the cascade applied `ALL_OTHER_USES` to ALL 252.5159 kWh,
over-counting MEV cost by 127.5159 × (0.13244 - 0.11664) = +£2.01/yr.
Worksheet pin verification (line (249)):
"Pumps, fans and electric keep-hot ... 172.5159 13.2440 20.8338"
127.5159 × 0.11664 + 45 × 0.13244 = £14.8753 + £5.9598 = £20.8351
≈ ws £20.8338 ✓
Pump for solar water heating 80.0 × 0.13244 = £10.5952 ✓
Implementation (3-layer):
1. `calculator.py:CalculatorInputs` — new optional
`pumps_fans_fuel_cost_gbp_per_kwh: Optional[float] = None`.
2. `calculator.py` legacy cost path — `pumps_fans_cost` resolves
via the new field with fallback to `other_fuel_cost_gbp_per_kwh`.
3. `cert_to_inputs.py:_pumps_fans_fuel_cost_gbp_per_kwh` — computes
the kWh-weighted blended rate when off-peak + MEV is lodged.
Reuses `_mev_decentralised_kwh_per_yr_from_cert` (S0380.102) to
recover the MEV portion.
Cohort safety: STANDARD-tariff certs (the entire cohort except cert
000565) get None back → existing `other_fuel_cost_gbp_per_kwh`
fallback unchanged. Certs without MEV (zero MEV kWh) also get None
→ no behavioural change.
Movement at HEAD (cert 000565):
- pumps_fans_kwh_per_yr ✓ EXACT (unchanged)
- total_fuel_cost_gbp: 4680.6514 → 4678.6372 (Δ +£0.39 → -£1.62)
- ecf: 5.3873 → 5.3850 (Δ +0.0007 → -0.0016)
- sap_score_continuous: 28.5043 → 28.5269 (Δ -0.0044 → +0.0182)
Continuous-SAP residual drifted from -0.0044 to +0.0182 in absolute
value: closing the MEV cost over-count exposes a pre-existing
space-heating cascade under-count (main_heating_fuel_kwh is -16 kWh
under ws). Per user direction [[feedback-spec-floor-skepticism]]:
shipping spec-correct intermediate-value fixes even when they
transiently drift continuous SAP. The remaining residual is now
SH-cascade driven; a separate slice.
Test count: 597 pass + 7 expected 000565 fails unchanged.
Pyright net-zero per touched file.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a0413155ae
commit
e3abe9b2b5
3 changed files with 133 additions and 1 deletions
|
|
@ -1847,6 +1847,51 @@ def test_summary_000565_ext2_floor_routes_to_u_value_0p22_via_table_20_per_rdsap
|
|||
assert bp_2.floor_insulation_thickness == "200mm"
|
||||
|
||||
|
||||
def test_summary_000565_mev_fans_cost_uses_table_12a_grid_2_fans_for_mech_vent_rate() -> None:
|
||||
# Arrange — SAP 10.2 Table 12a Grid 2 (PDF p.191) "Other electricity
|
||||
# uses" splits two cost categories on off-peak tariffs:
|
||||
#
|
||||
# Fans for mechanical ventilation systems 10-hour 0.58
|
||||
# All other uses, and locally generated 10-hour 0.80
|
||||
# electricity
|
||||
#
|
||||
# Cert 000565 lodges 127.5159 kWh of MEV decentralised fan energy
|
||||
# (line 230a) which must be billed at the FANS_FOR_MECH_VENT blend
|
||||
# (0.58 × 14.68 + 0.42 × 7.50 = 11.6644 p/kWh), NOT the
|
||||
# ALL_OTHER_USES blend (13.244 p/kWh). The remaining 125 kWh of
|
||||
# pumps_fans (45 flue fan + 80 solar HW pump) stay at 13.244.
|
||||
#
|
||||
# Worksheet line (249) verifies the split:
|
||||
# Pumps, fans and electric keep-hot 172.5159 × effective 12.076 = £20.8338
|
||||
# = 127.5159 × 0.11664 + 45 × 0.13244
|
||||
# = 14.8753 + 5.9598 = £20.8351 ≈ £20.8338 ✓
|
||||
# Pump for solar water heating 80.0000 × 13.244 / 100 = £10.5952
|
||||
#
|
||||
# Pre-slice the cascade applied 0.13244 to ALL 252.5159 kWh, over-
|
||||
# counting MEV cost by 127.5159 × (0.13244 - 0.11664) = £2.01.
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
cert_to_inputs,
|
||||
)
|
||||
|
||||
# Act
|
||||
result = calculate_sap_from_inputs(cert_to_inputs(epc))
|
||||
|
||||
# Assert — total fuel cost should be ≤ +£0.05 over worksheet (the
|
||||
# MEV cost split closes the +£2.01 pumps_fans over-count; remaining
|
||||
# residual is the space-heating cascade under-count, separate slice).
|
||||
# Worksheet line (255) = £4680.2593.
|
||||
delta = result.total_fuel_cost_gbp - 4680.2593
|
||||
assert delta < 0.05, (
|
||||
f"cascade total_fuel_cost_gbp={result.total_fuel_cost_gbp:.4f}; "
|
||||
f"ws=£4680.2593; Δ={delta:+.4f} (expected ≤+£0.05 after MEV "
|
||||
f"cost split closes the +£2.01 over-count)"
|
||||
)
|
||||
|
||||
|
||||
def test_summary_000565_main_1_ashp_sap_code_224_routes_to_main_heating_category_4_per_sap_table_4a() -> None:
|
||||
# Arrange — SAP 10.2 Table 4a (PDF p.165) "Main heating systems":
|
||||
# the category column lists "Heat pumps" as category 4. Codes in
|
||||
|
|
|
|||
|
|
@ -181,6 +181,15 @@ class CalculatorInputs:
|
|||
hot_water_fuel_cost_gbp_per_kwh: float
|
||||
other_fuel_cost_gbp_per_kwh: float
|
||||
co2_factor_kg_per_kwh: float
|
||||
# SAP 10.2 Table 12a Grid 2 split — MEV/MVHR fans on off-peak
|
||||
# tariffs (7-hour: 0.71 high-frac; 10-hour: 0.58 high-frac) bill
|
||||
# at a DIFFERENT blended rate than "all other uses" (7-hour: 0.90;
|
||||
# 10-hour: 0.80). Cert_to_inputs supplies the MEV-kWh-weighted
|
||||
# blended rate here for pumps_fans on off-peak; None on standard-
|
||||
# tariff certs (no split applies) and on certs without MEV/MVHR.
|
||||
# When None the legacy `other_fuel_cost_gbp_per_kwh` applies to
|
||||
# the whole pumps_fans stream.
|
||||
pumps_fans_fuel_cost_gbp_per_kwh: Optional[float] = None
|
||||
# Pre-computed monthly external temperature (°C). When provided, the
|
||||
# calculator's per-month solve uses this directly instead of looking up
|
||||
# `external_temperature_c(region, month)`. Set by cert_to_inputs from
|
||||
|
|
@ -516,7 +525,12 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
hot_water_cost = (
|
||||
inputs.hot_water_kwh_per_yr * inputs.hot_water_fuel_cost_gbp_per_kwh
|
||||
)
|
||||
pumps_fans_cost = inputs.pumps_fans_kwh_per_yr * inputs.other_fuel_cost_gbp_per_kwh
|
||||
pumps_fans_rate = (
|
||||
inputs.pumps_fans_fuel_cost_gbp_per_kwh
|
||||
if inputs.pumps_fans_fuel_cost_gbp_per_kwh is not None
|
||||
else inputs.other_fuel_cost_gbp_per_kwh
|
||||
)
|
||||
pumps_fans_cost = inputs.pumps_fans_kwh_per_yr * pumps_fans_rate
|
||||
lighting_cost = inputs.lighting_kwh_per_yr * inputs.other_fuel_cost_gbp_per_kwh
|
||||
# SAP 10.2 §10a (PDF p.145) line (247a): instantaneous electric
|
||||
# showers route their (64a) kWh through the "other fuel" tariff
|
||||
|
|
|
|||
|
|
@ -1593,6 +1593,62 @@ def _other_fuel_cost_gbp_per_kwh(
|
|||
return blended * _PENCE_TO_GBP
|
||||
|
||||
|
||||
def _pumps_fans_fuel_cost_gbp_per_kwh(
|
||||
*,
|
||||
tariff: Tariff,
|
||||
mev_kwh_per_yr: float,
|
||||
total_pumps_fans_kwh_per_yr: float,
|
||||
) -> Optional[float]:
|
||||
"""SAP 10.2 Table 12a Grid 2 — MEV/MVHR fan electricity bills at the
|
||||
`FANS_FOR_MECH_VENT` high-rate fraction (10-hour: 0.58; 7-hour:
|
||||
0.71), distinct from `ALL_OTHER_USES` (10-hour: 0.80; 7-hour: 0.90)
|
||||
which covers central-heating circulation pumps, flue fans, solar
|
||||
HW pump, and locally-generated electricity.
|
||||
|
||||
Returns the kWh-weighted blended rate across the two Grid 2
|
||||
categories — `(mev_kwh × fans_rate + non_mev_kwh × other_rate) /
|
||||
total_kwh`. Returns None on STANDARD tariff (no off-peak split
|
||||
applies; the calculator's `other_fuel_cost_gbp_per_kwh` already
|
||||
yields the right scalar) and when no MEV is lodged (no split
|
||||
needed; the same `other_fuel_cost_gbp_per_kwh` applies).
|
||||
|
||||
Worksheet pin for cert 000565 (TEN_HOUR + MEV 127.5 kWh + 125 kWh
|
||||
other pumps/fans):
|
||||
fans_blend = 0.58 × 14.68 + 0.42 × 7.50 = 11.6644 p/kWh
|
||||
other_blend = 0.80 × 14.68 + 0.20 × 7.50 = 13.2440 p/kWh
|
||||
weighted = (127.5159 × 11.6644 + 125.0 × 13.2440) / 252.5159
|
||||
= 12.4467 p/kWh
|
||||
The (249) line in the worksheet uses the same weighting to bill
|
||||
MEV at the lower 11.6644 rate; without this helper the cascade
|
||||
over-counted by £2.01 / yr.
|
||||
"""
|
||||
if tariff is Tariff.STANDARD:
|
||||
return None
|
||||
if mev_kwh_per_yr <= 0.0 or total_pumps_fans_kwh_per_yr <= 0.0:
|
||||
return None
|
||||
try:
|
||||
fans_high_frac = other_use_high_rate_fraction(
|
||||
OtherUse.FANS_FOR_MECH_VENT, tariff,
|
||||
)
|
||||
other_high_frac = other_use_high_rate_fraction(
|
||||
OtherUse.ALL_OTHER_USES, tariff,
|
||||
)
|
||||
except NotImplementedError:
|
||||
return None
|
||||
high_rate, low_rate = _tariff_high_low_rates_p_per_kwh(tariff)
|
||||
fans_blend = (
|
||||
fans_high_frac * high_rate + (1.0 - fans_high_frac) * low_rate
|
||||
)
|
||||
other_blend = (
|
||||
other_high_frac * high_rate + (1.0 - other_high_frac) * low_rate
|
||||
)
|
||||
non_mev_kwh = max(0.0, total_pumps_fans_kwh_per_yr - mev_kwh_per_yr)
|
||||
weighted_p_per_kwh = (
|
||||
mev_kwh_per_yr * fans_blend + non_mev_kwh * other_blend
|
||||
) / total_pumps_fans_kwh_per_yr
|
||||
return weighted_p_per_kwh * _PENCE_TO_GBP
|
||||
|
||||
|
||||
# Water-heating codes that say "inherit from the main system" — the
|
||||
# `seasonal_efficiency` cascade returns 0 as a sentinel for these in the
|
||||
# legacy `domain.sap10_ml.sap_efficiencies` module. We need to inherit through
|
||||
|
|
@ -3968,6 +4024,11 @@ def cert_to_inputs(
|
|||
# SAP 10.2 Table 4f (p.174) — additive components on top of the
|
||||
# Main 1 category base. Each component is per-cert-lodging:
|
||||
pumps_fans_kwh += _table_4f_additive_components(epc)
|
||||
# Track the MEV/MVHR-fan portion separately so the cost cascade can
|
||||
# apply Table 12a Grid 2 `FANS_FOR_MECH_VENT` (0.58 high-frac on
|
||||
# 10-hour) instead of `ALL_OTHER_USES` (0.80) — see
|
||||
# `_pumps_fans_fuel_cost_gbp_per_kwh`. Zero when no MEV is lodged.
|
||||
mev_kwh_for_cost_split = _mev_decentralised_kwh_per_yr_from_cert(epc)
|
||||
primary_age = (
|
||||
epc.sap_building_parts[0].construction_age_band if epc.sap_building_parts else None
|
||||
)
|
||||
|
|
@ -4388,6 +4449,18 @@ def cert_to_inputs(
|
|||
other_fuel_cost_gbp_per_kwh=_other_fuel_cost_gbp_per_kwh(
|
||||
_rdsap_tariff(epc), prices
|
||||
),
|
||||
# SAP 10.2 Table 12a Grid 2 — MEV/MVHR fans bill at a different
|
||||
# high-rate fraction (10-hour: 0.58; 7-hour: 0.71) than the
|
||||
# general "all other uses" category (10-hour: 0.80; 7-hour:
|
||||
# 0.90). Compute the kWh-weighted blended rate so the
|
||||
# calculator's legacy pumps_fans cost line resolves correctly.
|
||||
# None on standard-tariff certs (no split applies) and on certs
|
||||
# without MEV (no MEV portion to split out).
|
||||
pumps_fans_fuel_cost_gbp_per_kwh=_pumps_fans_fuel_cost_gbp_per_kwh(
|
||||
tariff=_rdsap_tariff(epc),
|
||||
mev_kwh_per_yr=mev_kwh_for_cost_split,
|
||||
total_pumps_fans_kwh_per_yr=pumps_fans_kwh,
|
||||
),
|
||||
# Table 32 standing charges for the off-peak fallback path.
|
||||
# STANDARD-tariff certs route via `fuel_cost.additional_
|
||||
# standing_charges_gbp` (set inside `_fuel_cost`) and the
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue