mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(tariff): include MVHR fan electricity in the off-peak Grid 2 fan split
SAP 10.2 Table 12a Grid 2 (PDF p.191) bills "Fans for mechanical ventilation systems" at 0.71 (7-hour) / 0.58 (10-hour), distinct from "All other uses" (0.90 / 0.80) which covers circulation pumps, flue fans and the solar HW pump. The cost-split mech-vent kWh (`mev_kwh_for_cost_split`) only summed the decentralised-MEV (230b) fans, not the (230a) MVHR fan electricity — even though the total pumps/fans bucket adds both. So an MVHR dwelling on an off-peak tariff billed its fan electricity at the 0.90/0.80 "all other uses" rate instead of 0.71/0.58. The comment already said "MEV/MVHR-fan portion"; only the MEV term was wired when MVHR landed. Fixed to mirror both mechanical-ventilation fan terms summed into the total. Worksheet-proven on simulated case 50 (000565 semi + MVHR Vent Axia + dual electric immersion, Unknown meter -> 7-hour via the §12 dual-immersion trigger): the fan bucket (315.64 kWh, 100% MVHR per worksheet line 230a) was billing at 14.311 p/kWh (0.90) vs Elmhurst's 12.451 p/kWh (0.71) — +£5.87/yr, -0.23 SAP. After the fix our existing-dwelling rating reconciles to Elmhurst EXACTLY: SAP value 38.8426 (=Elmhurst 38.8426 -> 39), total cost £1317.0116 (=Elmhurst £1317.0116 to the penny). Same `mev_kwh_for_cost_split` feeds the CO2 + PE cascades, so all three split consistently. 0 corpus impact (all 3 corpus MVHR certs are standard tariff); gauge unchanged 73.3% / MAE 0.774 / CO2 0.08 / PE 3.4. Pin: test_mvhr_fan_electricity_bills_at_grid2_fan_fraction_on_off_peak. pyright strict gate not run locally (pyright not installed in this container). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
eea5d3a5a8
commit
cd5113abf2
2 changed files with 80 additions and 4 deletions
|
|
@ -7318,10 +7318,21 @@ def cert_to_inputs(
|
||||||
pumps_fans_kwh += _table_4f_circulation_pump_kwh(_pumps_main_2)
|
pumps_fans_kwh += _table_4f_circulation_pump_kwh(_pumps_main_2)
|
||||||
pumps_fans_kwh += _table_4f_additive_components(epc)
|
pumps_fans_kwh += _table_4f_additive_components(epc)
|
||||||
# Track the MEV/MVHR-fan portion separately so the cost cascade can
|
# 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
|
# apply Table 12a Grid 2 `FANS_FOR_MECH_VENT` (0.71 high-frac on
|
||||||
# 10-hour) instead of `ALL_OTHER_USES` (0.80) — see
|
# 7-hour, 0.58 on 10-hour) instead of `ALL_OTHER_USES` (0.90 / 0.80)
|
||||||
# `_pumps_fans_fuel_cost_gbp_per_kwh`. Zero when no MEV is lodged.
|
# — see `_pumps_fans_fuel_cost_gbp_per_kwh`. Must mirror the
|
||||||
mev_kwh_for_cost_split = _mev_decentralised_kwh_per_yr_from_cert(epc)
|
# mechanical-ventilation fan terms summed into the total pumps/fans
|
||||||
|
# at `_table_4f_additive_components` (230b decentralised MEV/extract)
|
||||||
|
# + the (230a) MVHR fan: both are "Fans for mechanical ventilation
|
||||||
|
# systems" in Grid 2, while flue fans / circulation pumps / solar HW
|
||||||
|
# pump are "All other uses". The MVHR term was omitted when MVHR
|
||||||
|
# landed, so an MVHR dwelling on off-peak billed its fan electricity
|
||||||
|
# at 0.90 instead of 0.71 (case 50: +£5.87/yr, -0.23 SAP). Zero when
|
||||||
|
# no mechanical-ventilation fan is lodged.
|
||||||
|
mev_kwh_for_cost_split = (
|
||||||
|
_mev_decentralised_kwh_per_yr_from_cert(epc)
|
||||||
|
+ _mvhr_fan_kwh_per_yr_from_cert(epc)
|
||||||
|
)
|
||||||
primary_age = _dwelling_age_band(epc)
|
primary_age = _dwelling_age_band(epc)
|
||||||
|
|
||||||
# SAP 10.2 Appendix D2.1: if the cert lodges a PCDB index number that
|
# SAP 10.2 Appendix D2.1: if the cert lodges a PCDB index number that
|
||||||
|
|
|
||||||
|
|
@ -3876,6 +3876,71 @@ def test_mvhr_system_values_apply_pcdb_wet_room_point_and_table_329_iufs() -> No
|
||||||
assert abs(fan_kwh - expected_fan_kwh) <= 1e-6
|
assert abs(fan_kwh - expected_fan_kwh) <= 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_mvhr_fan_electricity_bills_at_grid2_fan_fraction_on_off_peak() -> None:
|
||||||
|
# Arrange — an MVHR dwelling on an off-peak tariff. SAP 10.2 Table 12a
|
||||||
|
# Grid 2 (PDF p.191) bills "Fans for mechanical ventilation systems" at
|
||||||
|
# 0.71 (7-hour) / 0.58 (10-hour) high-rate fraction, distinct from "All
|
||||||
|
# other uses" (0.90 / 0.80) which covers circulation pumps + flue fans.
|
||||||
|
# The MVHR fan electricity (230a) was summed into the pumps/fans total
|
||||||
|
# but NOT into the cost-split mech-vent kWh, so on off-peak it billed at
|
||||||
|
# the 0.90/0.80 "all other uses" rate. Worksheet-proven on simulated
|
||||||
|
# case 50 (000565 + MVHR + dual immersion): +£5.87/yr, -0.23 SAP.
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||||
|
_mvhr_fan_kwh_per_yr_from_cert, # pyright: ignore[reportPrivateUsage]
|
||||||
|
_tariff_high_low_rates_p_per_kwh, # pyright: ignore[reportPrivateUsage]
|
||||||
|
)
|
||||||
|
from domain.sap10_calculator.tables.table_12a import (
|
||||||
|
OtherUse,
|
||||||
|
Tariff,
|
||||||
|
other_use_high_rate_fraction,
|
||||||
|
)
|
||||||
|
from tests.domain.sap10_calculator.worksheet import (
|
||||||
|
_elmhurst_worksheet_000565 as _w000565,
|
||||||
|
)
|
||||||
|
|
||||||
|
base = _w000565.build_epc()
|
||||||
|
assert base.sap_ventilation is not None
|
||||||
|
epc = dataclasses.replace(
|
||||||
|
base,
|
||||||
|
mechanical_ventilation_index_number=500140,
|
||||||
|
wet_rooms_count=2,
|
||||||
|
mechanical_vent_duct_type=2, # rigid
|
||||||
|
sap_ventilation=dataclasses.replace(
|
||||||
|
base.sap_ventilation, mechanical_ventilation_kind="MVHR"
|
||||||
|
),
|
||||||
|
sap_energy_source=dataclasses.replace(
|
||||||
|
base.sap_energy_source, meter_type="1" # off-peak (10-hour)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
inputs = cert_to_inputs(epc)
|
||||||
|
rate = inputs.pumps_fans_fuel_cost_gbp_per_kwh
|
||||||
|
|
||||||
|
# Assert — the resolved rate is the kWh-weighted blend of the MVHR fan
|
||||||
|
# portion at the FANS_FOR_MECH_VENT fraction and the remaining pumps at
|
||||||
|
# ALL_OTHER_USES. The presence of the MVHR fan in the split makes it
|
||||||
|
# strictly cheaper than billing the whole bucket at ALL_OTHER_USES.
|
||||||
|
tariff = Tariff.TEN_HOUR
|
||||||
|
high, low = _tariff_high_low_rates_p_per_kwh(tariff)
|
||||||
|
fan_frac = other_use_high_rate_fraction(OtherUse.FANS_FOR_MECH_VENT, tariff)
|
||||||
|
other_frac = other_use_high_rate_fraction(OtherUse.ALL_OTHER_USES, tariff)
|
||||||
|
fan_blend = fan_frac * high + (1.0 - fan_frac) * low
|
||||||
|
other_blend = other_frac * high + (1.0 - other_frac) * low
|
||||||
|
fan_kwh = _mvhr_fan_kwh_per_yr_from_cert(epc)
|
||||||
|
total_kwh = inputs.pumps_fans_kwh_per_yr
|
||||||
|
non_fan_kwh = total_kwh - fan_kwh
|
||||||
|
expected = (
|
||||||
|
(fan_kwh * fan_blend + non_fan_kwh * other_blend) / total_kwh / 100.0
|
||||||
|
)
|
||||||
|
assert rate is not None
|
||||||
|
assert fan_kwh > 0.0
|
||||||
|
assert abs(rate - expected) <= 1e-12
|
||||||
|
assert rate < other_blend / 100.0 # regression guard: fan portion is in the split
|
||||||
|
|
||||||
|
|
||||||
def test_mvhr_system_values_fall_back_to_table_4g_defaults_without_pcdb_index() -> None:
|
def test_mvhr_system_values_fall_back_to_table_4g_defaults_without_pcdb_index() -> None:
|
||||||
# Arrange — an MVHR cert lodged with NO PCDF index (corpus cert
|
# Arrange — an MVHR cert lodged with NO PCDF index (corpus cert
|
||||||
# "Flat 1"). SAP 10.2 Table 4g (PDF p.176): default raw efficiency
|
# "Flat 1"). SAP 10.2 Table 4g (PDF p.176): default raw efficiency
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue