mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(ventilation): apply Table 4g note 3 in-use factor to index-less MEV SFP
The no-PCDB MEV fan-electricity path fed the SAP 10.2 Table 4g default SFP
(0.8 W/(l/s)) directly as SFPav. But Table 4g note 3 (PDF p.176) is explicit:
the default SFP values "are to be multiplied by the appropriate in-use factor
for default data from the PCDB" — PCDB Table 329 system_type 10 ("default
data, used when SFP is taken from Table 4g rather than the PCDB"), IUF 2.5
(duct-agnostic per note 2). Table 4h, which previously held these factors, is
retired ("no longer used – data now stored in the PCDB").
Omitting the IUF under-billed the index-less MEV fan electricity by 2.5x
(SFPav 0.8 instead of 0.8 x 2.5 = 2.0), so cost was too low and the cohort
over-rated. This is distinct from the with-index path, which already applies
the tested-product system_type-2 "no scheme" IUF (~1.45) per fan.
Index-less gas-house MEV cohort: +1.37 median -> -0.18 (12% -> 92% within 0.5),
no overshoot — the missing IUF was exactly the over-rate. API gauge 67.7% ->
68.4% within-0.5 (mean|err| 0.992 -> 0.986, signed +0.031 -> +0.006).
Worksheet harness 47/47, 0 divergers (Summary-path MEV certs carry a PCDB
index or are natural, so unaffected).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5b2cf5edc7
commit
4fb9b853dc
2 changed files with 86 additions and 12 deletions
|
|
@ -566,6 +566,11 @@ def _table_4f_additive_components(epc: EpcPropertyData) -> float:
|
|||
_MEV_KITCHEN_FAN_CONFIG_CODES: Final[frozenset[int]] = frozenset({1, 3, 5})
|
||||
# PCDB Table 329 / 322 system_type=2 = decentralised MEV.
|
||||
_MEV_DECENTRALISED_SYSTEM_TYPE: Final[int] = 2
|
||||
# PCDB Table 329 system_type=10 = "default data" (PCDF Spec §A.20) — the
|
||||
# in-use-factor row used when the SFP is taken from SAP 10.2 Table 4g
|
||||
# rather than a specific PCDB product. Table 4g note 3 (PDF p.176)
|
||||
# requires the default SFP to be multiplied by this IUF (2.5, duct-agnostic).
|
||||
_MEV_DEFAULT_DATA_SYSTEM_TYPE: Final[int] = 10
|
||||
# Elmhurst "Duct Type" cascade integer: 1=Flexible, 2=Rigid (per
|
||||
# `_ELMHURST_DUCT_TYPE_TO_INT` in datatypes.epc.domain.mapper).
|
||||
_MV_DUCT_TYPE_FLEXIBLE: Final[int] = 1
|
||||
|
|
@ -578,11 +583,25 @@ _MEV_THROUGH_WALL_CONFIG_CODES: Final[frozenset[int]] = frozenset({5, 6})
|
|||
|
||||
# SAP 10.2 Table 4g (PDF p.176) / §2.6.3 note 1 — default specific fan
|
||||
# power (W per litre/sec) for an MEV system whose fan(s) are not in the
|
||||
# PCDB. Used directly as the IUF-adjusted SFPav in the (230a) formula
|
||||
# (SFPav × 1.22 × V) when no Table 322 record resolves.
|
||||
# PCDB. This is the RAW SFP: Table 4g note 3 requires it to be multiplied
|
||||
# by the "default data" in-use factor (Table 329 system_type 10) before
|
||||
# use as the SFPav in the (230a) formula (SFPav × 1.22 × V).
|
||||
_TABLE_4G_DEFAULT_MEV_SFP_W_PER_L_PER_S: Final[float] = 0.8
|
||||
|
||||
|
||||
def _mev_default_data_iuf() -> float:
|
||||
"""SAP 10.2 Table 4g note 3 (PDF p.176) in-use factor for default MEV
|
||||
data — PCDB Table 329 system_type 10 (IUF 2.5, identical across rigid/
|
||||
flexible/no-duct columns per Table 4g note 2 "applies to both rigid and
|
||||
flexible ducting"). Falls back to 1.0 if the Table 329 record is
|
||||
unavailable (ETL bootstrap), preserving the pre-fix raw-SFP behaviour
|
||||
rather than zeroing fan electricity."""
|
||||
record = mv_in_use_factors_record(_MEV_DEFAULT_DATA_SYSTEM_TYPE)
|
||||
if record is None or record.sfp_iuf_rigid_no_scheme is None:
|
||||
return 1.0
|
||||
return record.sfp_iuf_rigid_no_scheme
|
||||
|
||||
|
||||
def _mev_decentralised_kwh_per_yr_from_cert(epc: EpcPropertyData) -> float:
|
||||
"""Compose the SAP 10.2 §5 Table 4f line (230a) MEV decentralised
|
||||
annual electricity contribution from PCDB Tables 322 (per-fan SFP
|
||||
|
|
@ -635,8 +654,16 @@ def _mev_decentralised_kwh_per_yr_from_cert(epc: EpcPropertyData) -> float:
|
|||
!= MechanicalVentilationKind.EXTRACT_OR_PIV_OUTSIDE.name
|
||||
):
|
||||
return 0.0
|
||||
# SAP 10.2 Table 4g note 3 (PDF p.176): the default SFP "[is] to be
|
||||
# multiplied by the appropriate in-use factor for default data from
|
||||
# the PCDB" (Table 329 system_type 10, IUF 2.5). Omitting it
|
||||
# under-billed the index-less MEV fan electricity by 2.5x → +1.3 SAP
|
||||
# over-rate on the no-PCDB MEV cohort (mostly gas houses). Distinct
|
||||
# from the with-index path below, which applies the tested-product
|
||||
# system_type-2 "no scheme" IUF (~1.45) per fan.
|
||||
sfp_av = _TABLE_4G_DEFAULT_MEV_SFP_W_PER_L_PER_S * _mev_default_data_iuf()
|
||||
return mev_decentralised_kwh_per_yr(
|
||||
sfp_av_w_per_l_per_s=_TABLE_4G_DEFAULT_MEV_SFP_W_PER_L_PER_S,
|
||||
sfp_av_w_per_l_per_s=sfp_av,
|
||||
dwelling_volume_m3=dimensions_from_cert(epc).volume_m3,
|
||||
)
|
||||
iuf_record = mv_in_use_factors_record(_MEV_DECENTRALISED_SYSTEM_TYPE)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
|||
_heat_network_factor_fuel_code, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_standing_charge_gbp, # pyright: ignore[reportPrivateUsage]
|
||||
_main_fuel_code, # pyright: ignore[reportPrivateUsage]
|
||||
_mev_decentralised_kwh_per_yr_from_cert, # pyright: ignore[reportPrivateUsage]
|
||||
_mev_default_data_iuf, # pyright: ignore[reportPrivateUsage]
|
||||
_TABLE_4G_DEFAULT_MEV_SFP_W_PER_L_PER_S, # pyright: ignore[reportPrivateUsage]
|
||||
dimensions_from_cert,
|
||||
_table_12_factor_fuel_code, # pyright: ignore[reportPrivateUsage]
|
||||
_is_electric_main, # pyright: ignore[reportPrivateUsage]
|
||||
_is_heat_network_electric_main, # pyright: ignore[reportPrivateUsage]
|
||||
|
|
@ -90,6 +94,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
|||
ventilation_from_cert,
|
||||
)
|
||||
from domain.sap10_calculator.tables.pcdb import GasOilBoilerRecord, gas_oil_boiler_record
|
||||
from domain.sap10_calculator.worksheet.mev import mev_decentralised_kwh_per_yr
|
||||
from tests.domain.sap10_calculator.worksheet import _elmhurst_worksheet_000477 as _w000477
|
||||
from domain.sap10_calculator.worksheet.water_heating import (
|
||||
combi_loss_monthly_kwh_table_3b_row_1_instantaneous,
|
||||
|
|
@ -1436,12 +1441,12 @@ def test_corridor_flat_assumes_draught_lobby_present_zeroing_line_13() -> None:
|
|||
|
||||
def test_index_less_mev_uses_table_4g_default_sfp_for_fan_electricity() -> None:
|
||||
# Arrange — an MEV system with NO PCDB record (index absent / not in
|
||||
# Table 322). SAP 10.2 §2.6.3 / Table 4g note 1 prescribes a default
|
||||
# specific fan power of 0.8 W/(l/s), used directly as the SFPav in the
|
||||
# §5 Table 4f line (230a) `SFPav × 1.22 × V`. Without it the cascade
|
||||
# billed ZERO fan electricity for index-less MEV (the +2.2 SAP
|
||||
# over-rate on the index-less MEV cohort, mostly gas houses). A natural
|
||||
# dwelling contributes nothing.
|
||||
# Table 322). SAP 10.2 §2.6.3 / Table 4g gives a default specific fan
|
||||
# power of 0.8 W/(l/s); Table 4g note 3 (PDF p.176) requires multiplying
|
||||
# it by the default-data in-use factor (Table 329 system_type 10, IUF
|
||||
# 2.5) before use as the SFPav in the §5 Table 4f line (230a)
|
||||
# `SFPav × 1.22 × V`. So the effective SFPav is 0.8 × 2.5 = 2.0. A
|
||||
# natural dwelling contributes nothing.
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
_TABLE_4G_DEFAULT_MEV_SFP_W_PER_L_PER_S, # pyright: ignore[reportPrivateUsage]
|
||||
_mev_decentralised_kwh_per_yr_from_cert, # pyright: ignore[reportPrivateUsage]
|
||||
|
|
@ -1468,10 +1473,10 @@ def test_index_less_mev_uses_table_4g_default_sfp_for_fan_electricity() -> None:
|
|||
mev_kwh = _mev_decentralised_kwh_per_yr_from_cert(mev_no_index)
|
||||
natural_kwh = _mev_decentralised_kwh_per_yr_from_cert(natural)
|
||||
|
||||
# Assert — default SFP 0.8 × 1.22 × V for the index-less MEV; zero for
|
||||
# the naturally-ventilated dwelling.
|
||||
# Assert — IUF-adjusted SFPav (0.8 × 2.5) × 1.22 × V for the index-less
|
||||
# MEV; zero for the naturally-ventilated dwelling.
|
||||
volume = dimensions_from_cert(mev_no_index).volume_m3
|
||||
expected = _TABLE_4G_DEFAULT_MEV_SFP_W_PER_L_PER_S * 1.22 * volume
|
||||
expected = _TABLE_4G_DEFAULT_MEV_SFP_W_PER_L_PER_S * 2.5 * 1.22 * volume
|
||||
assert abs(mev_kwh - expected) <= 1e-6
|
||||
assert mev_kwh > 0.0
|
||||
assert natural_kwh == 0.0
|
||||
|
|
@ -7306,3 +7311,45 @@ def test_non_heat_network_main_returns_none_so_caller_uses_fuel_standing() -> No
|
|||
|
||||
# Act / Assert
|
||||
assert _heat_network_standing_charge_gbp(epc, main) is None
|
||||
|
||||
|
||||
def test_mev_default_data_iuf_is_table_329_system_type_10_value() -> None:
|
||||
# Arrange / Act — SAP 10.2 Table 4g note 3 (PDF p.176) directs the
|
||||
# default SFP to "the appropriate in-use factor for default data from
|
||||
# the PCDB" = Table 329 system_type 10, which lodges IUF 2.5 (identical
|
||||
# across rigid/flexible/no-duct columns).
|
||||
iuf = _mev_default_data_iuf()
|
||||
|
||||
# Assert
|
||||
assert abs(iuf - 2.5) <= 1e-9
|
||||
|
||||
|
||||
def test_index_less_mev_applies_table_4g_note_3_default_data_iuf() -> None:
|
||||
# Arrange — an MEV (mechanical extract) dwelling with NO PCDB index.
|
||||
# SAP 10.2 Table 4g gives the default SFP 0.8 W/(l/s), and note 3
|
||||
# requires multiplying it by the default-data in-use factor (2.5) before
|
||||
# use as SFPav in the (230a) fan-electricity formula. Before this fix
|
||||
# the raw 0.8 was used directly, under-billing fan electricity by 2.5x
|
||||
# and over-rating the index-less MEV cohort by ~+1.3 SAP.
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[make_building_part()],
|
||||
sap_ventilation=SapVentilation(
|
||||
mechanical_ventilation_kind="EXTRACT_OR_PIV_OUTSIDE",
|
||||
),
|
||||
)
|
||||
|
||||
# Act
|
||||
fan_kwh = _mev_decentralised_kwh_per_yr_from_cert(epc)
|
||||
volume_m3 = dimensions_from_cert(epc).volume_m3
|
||||
expected = mev_decentralised_kwh_per_yr(
|
||||
sfp_av_w_per_l_per_s=_TABLE_4G_DEFAULT_MEV_SFP_W_PER_L_PER_S * 2.5,
|
||||
dwelling_volume_m3=volume_m3,
|
||||
)
|
||||
|
||||
# Assert — fan electricity follows the IUF-adjusted SFPav (2.0), i.e.
|
||||
# 2.5x the raw-0.8 value, not the raw default.
|
||||
assert fan_kwh > 0.0
|
||||
assert abs(fan_kwh - expected) <= 1e-9
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue