mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.102: Wire MEV decentralised cascade into pumps_fans (SAP 10.2 §2.6.4 + Table 4f line 230a)
SAP 10.2 Table 4f line (230a) annual electricity for mechanical
ventilation fans, decentralised MEV branch:
E_fans_kwh = SFPav × 1.22 × V
where SFPav is the §2.6.4 equation (1) flow-weighted average SFP
across every fan in the installation, with PCDB Table 322 supplying
per-configuration (flow, SFP) and PCDB Table 329 supplying the
ducting-type IUF.
This slice composes the foundation slices S0380.98 (Table 322),
S0380.99 (Table 329), S0380.100 (SFPav helper) into a cert-driven
cascade — `_mev_decentralised_kwh_per_yr_from_cert(epc)` reads:
MV PCDF Reference Number → PCDB Table 322 record (per-config SFP)
Duct Type (Flexible/Rigid) → PCDB Table 329 in-use factor
Wet Rooms count → per-fan-type count distribution
Three coupled changes:
1. Elmhurst extractor + schema — `_extract_ventilation` parses §12.1
"MV PCDF Reference Number", "Wet Rooms", "Duct Type", "Approved
Installation". New fields on `VentilationAndCooling`.
2. Mapper — plumbs the lodgements through to
`EpcPropertyData.mechanical_ventilation_index_number`,
`.wet_rooms_count`, `.mechanical_vent_duct_type`. New
`_elmhurst_mv_duct_type_int` helper (Flexible→1, Rigid→2 per PCDF
Spec §A.20 field 12 convention) with strict-raise on unknown
labels per [[unmapped-elmhurst-label]].
3. Cascade — `_table_4f_additive_components` calls the new
`_mev_decentralised_kwh_per_yr_from_cert(epc)` to add the (230a)
contribution alongside the existing flue-fan + solar-HW pump
additions.
Per-fan count convention (reverse-engineered from cert 000565):
- Each PCDB-defined configuration (1..6) contributes 1 baseline fan.
- Through-wall configurations scale with wet-rooms count:
through-wall kitchen (5): wet_rooms_count fans
through-wall other wet (6): wet_rooms_count + 1 fans
- Configurations with blank SFP (e.g. record 500755 in-duct codes 3,
4) contribute 0 to the numerator but their flow rate to the
denominator per SAP §2.6.4 "summation is over all the fans".
For cert 000565 (wet_rooms=2) this yields the worksheet's observed
fan distribution (1, 1, 1, 1, 2, 3) → SFPav = 11.7205 / 92.0 =
0.12740 W/(l/s), and (230a) = 0.12740 × 1.22 × 820.4385 = 127.5159
kWh/year ✓ matches worksheet line (230a) at 1e-4.
TODO: validate the count convention against a second MEV
decentralised fixture; the rule above fits cert 000565 alone.
Cert 000565 closure state at HEAD:
- pumps_fans_kwh_per_yr: 125.0 → 252.5159 ✓ EXACT (was 255.0 pre-arc;
the MEV +127.5 contribution closes the residual)
- sap_score (int): 29 ✓ EXACT preserved
- sap_score_continuous: 28.69 (S0380.101 transient) → 28.5043 vs
ws 28.5087 (Δ -0.0044). Was -0.0001 pre-arc — the MEV fix revealed
a pre-existing residual elsewhere in the cost cascade (likely
Table 12a HP-on-E7 high-rate split per the original TODO at
mapper.py:4039-4040; deferred to a separate slice).
Test count: 603 pass + 7 expected 000565 fails (was 8 —
pumps_fans_kwh_per_yr flipped FAIL→PASS, removed from work queue).
Cohort safety: only cert 000565 lodges a non-None MV PCDF Reference
Number across the Summary fixture set; cohort certs return 0 from
`_mev_decentralised_kwh_per_yr_from_cert` (no MEV system).
Pyright net-zero per touched file.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1b183f9c86
commit
a0413155ae
4 changed files with 183 additions and 4 deletions
|
|
@ -1110,6 +1110,24 @@ class ElmhurstSiteNotesExtractor:
|
|||
mechanical_ventilation_type = (
|
||||
" ".join(mv_type_raw.split()) if mv_type_raw else None
|
||||
)
|
||||
# SAP 10.2 §2.6.4 + Table 4f line (230a) — MEV PCDB lookup
|
||||
# inputs. Cert lodges PCDF index, wet-rooms count, ducting
|
||||
# type, and whether the installation was approved.
|
||||
mev_pcdf_raw = self._local_val(mv_lines, "MV PCDF Reference Number")
|
||||
mev_pcdf_reference = (
|
||||
int(mev_pcdf_raw) if mev_pcdf_raw and mev_pcdf_raw.isdigit() else None
|
||||
)
|
||||
wet_rooms_raw = self._local_val(mv_lines, "Wet Rooms")
|
||||
wet_rooms_count = (
|
||||
int(wet_rooms_raw) if wet_rooms_raw and wet_rooms_raw.isdigit() else None
|
||||
)
|
||||
duct_type_raw = self._local_val(mv_lines, "Duct Type")
|
||||
duct_type = duct_type_raw if duct_type_raw else None
|
||||
approved_raw = self._local_val(mv_lines, "Approved Installation")
|
||||
approved_installation = (
|
||||
None if approved_raw is None
|
||||
else approved_raw.strip().lower() == "yes"
|
||||
)
|
||||
return VentilationAndCooling(
|
||||
open_chimneys_count=self._int_val("No. of open chimneys"),
|
||||
open_flues_count=self._int_val("No. of open flues"),
|
||||
|
|
@ -1132,6 +1150,10 @@ class ElmhurstSiteNotesExtractor:
|
|||
pressure_test_method=self._str_val("Test Method"),
|
||||
air_permeability_ap4_m3_h_m2=air_permeability_ap4_m3_h_m2,
|
||||
mechanical_ventilation_type=mechanical_ventilation_type,
|
||||
mechanical_ventilation_pcdf_reference=mev_pcdf_reference,
|
||||
wet_rooms_count=wet_rooms_count,
|
||||
duct_type=duct_type,
|
||||
approved_installation=approved_installation,
|
||||
)
|
||||
|
||||
def _extract_lighting(self) -> Lighting:
|
||||
|
|
|
|||
|
|
@ -364,7 +364,13 @@ class EpcPropertyDataMapper:
|
|||
solar_hw_overshading=survey.renewables.solar_hw_overshading,
|
||||
has_hot_water_cylinder=survey.water_heating.hot_water_cylinder_present,
|
||||
has_fixed_air_conditioning=survey.ventilation.fixed_space_cooling,
|
||||
wet_rooms_count=0,
|
||||
wet_rooms_count=survey.ventilation.wet_rooms_count or 0,
|
||||
mechanical_ventilation_index_number=(
|
||||
survey.ventilation.mechanical_ventilation_pcdf_reference
|
||||
),
|
||||
mechanical_vent_duct_type=_elmhurst_mv_duct_type_int(
|
||||
survey.ventilation.duct_type
|
||||
),
|
||||
extensions_count=len(survey.extensions),
|
||||
heated_rooms_count=survey.heated_habitable_rooms,
|
||||
open_chimneys_count=survey.ventilation.open_chimneys_count,
|
||||
|
|
@ -4330,6 +4336,30 @@ def _elmhurst_mv_kind(mv_type: Optional[str]) -> Optional[str]:
|
|||
return _ELMHURST_MV_TYPE_TO_KIND[label]
|
||||
|
||||
|
||||
# Elmhurst Summary §12.1 "Duct Type" string → SAP10 cascade enum (PCDB
|
||||
# Table 329 in-use factor selector; PCDF Spec §A.20 field 12 codes:
|
||||
# 1=flexible, 2=rigid). Strict-raise per [[unmapped-elmhurst-label]]
|
||||
# on unrecognised labels so the cascade-coverage gap surfaces at the
|
||||
# extractor boundary.
|
||||
_ELMHURST_DUCT_TYPE_TO_INT: Dict[str, int] = {
|
||||
"Flexible": 1,
|
||||
"Rigid": 2,
|
||||
}
|
||||
|
||||
|
||||
def _elmhurst_mv_duct_type_int(duct_type: Optional[str]) -> Optional[int]:
|
||||
"""Translate the Elmhurst "Duct Type" string ("Flexible" / "Rigid")
|
||||
to the SAP10 cascade integer used to key PCDB Table 329 SFP IUFs.
|
||||
Returns None when no duct type is lodged (MEV absent or duct type
|
||||
not specified)."""
|
||||
if duct_type is None or not duct_type.strip():
|
||||
return None
|
||||
label = duct_type.strip()
|
||||
if label not in _ELMHURST_DUCT_TYPE_TO_INT:
|
||||
raise UnmappedElmhurstLabel("ventilation.duct_type", label)
|
||||
return _ELMHURST_DUCT_TYPE_TO_INT[label]
|
||||
|
||||
|
||||
def _map_elmhurst_ventilation(
|
||||
v: ElmhurstVentilation,
|
||||
built_form: str,
|
||||
|
|
|
|||
|
|
@ -195,6 +195,22 @@ class VentilationAndCooling:
|
|||
# extract, decentralised (MEV dc)". None when `mechanical_ventilation
|
||||
# is False` (no MV system).
|
||||
mechanical_ventilation_type: Optional[str] = None
|
||||
# Summary §12.1 "MV PCDF Reference Number" — PCDB Table 322 lookup
|
||||
# key for the MEV product. Drives the SAP 10.2 §2.6.4 SFPav cascade
|
||||
# (Table 4f line (230a) annual fan electricity).
|
||||
mechanical_ventilation_pcdf_reference: Optional[int] = None
|
||||
# Summary §12.1 "Wet Rooms" — count of wet rooms beyond the kitchen
|
||||
# (e.g. bathrooms, utility rooms). Used by the Elmhurst per-fan-
|
||||
# type count convention for MEV decentralised systems.
|
||||
wet_rooms_count: Optional[int] = None
|
||||
# Summary §12.1 "Duct Type" — "Flexible" or "Rigid". Selects the
|
||||
# PCDB Table 329 SFP in-use factor for in-room / in-duct fans.
|
||||
# Through-wall fans use the "no-duct" IUF independent of this.
|
||||
duct_type: Optional[str] = None
|
||||
# Summary §12.1 "Approved Installation" — Yes/No. When True the
|
||||
# PCDB Table 329 "with scheme" IUFs apply; the cohort fixtures
|
||||
# exercise only the "no scheme" branch (cert 000565 lodges "No").
|
||||
approved_installation: Optional[bool] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -71,8 +71,10 @@ from domain.sap10_ml.sap_efficiencies import (
|
|||
)
|
||||
from domain.sap10_calculator.calculator import CalculatorInputs
|
||||
from domain.sap10_calculator.tables.pcdb import (
|
||||
decentralised_mev_record,
|
||||
gas_oil_boiler_record,
|
||||
heat_pump_record,
|
||||
mv_in_use_factors_record,
|
||||
)
|
||||
from domain.sap10_calculator.tables.pcdb.parser import (
|
||||
GasOilBoilerRecord,
|
||||
|
|
@ -115,6 +117,11 @@ from domain.sap10_calculator.worksheet.rating import (
|
|||
sap_rating_integer,
|
||||
)
|
||||
from domain.sap10_calculator.worksheet.dimensions import dimensions_from_cert
|
||||
from domain.sap10_calculator.worksheet.mev import (
|
||||
MevFanEntry,
|
||||
mev_decentralised_kwh_per_yr,
|
||||
mev_sfp_av,
|
||||
)
|
||||
from domain.sap10_calculator.worksheet.internal_gains import (
|
||||
InternalGainsResult,
|
||||
OvershadingCategory,
|
||||
|
|
@ -234,6 +241,11 @@ def _table_4f_additive_components(epc: EpcPropertyData) -> float:
|
|||
heating category alone.
|
||||
|
||||
Currently wired:
|
||||
- (230a) MEV / MVHR — `SFPav × 1.22 × V` per SAP 10.2 §2.6.4 +
|
||||
Table 4f. PCDB Table 322 (decentralised MEV products) + Table
|
||||
329 (in-use factors) compose SFPav via `mev_sfp_av`. First
|
||||
exercised by cert 000565 (Titon Ultimate dMEV index 500755,
|
||||
2 wet rooms, Flexible ducting).
|
||||
- (230e) Main 2 gas-boiler flue fan — 45 kWh when a Main 2 system
|
||||
is lodged with `fan_flue_present=True` and a gas fuel type.
|
||||
Cert 000565 (Main 1 HP + Main 2 gas combi via WHC 914) is the
|
||||
|
|
@ -244,15 +256,13 @@ def _table_4f_additive_components(epc: EpcPropertyData) -> float:
|
|||
Elmhurst §16 aperture area into the schema.
|
||||
|
||||
Not yet wired:
|
||||
- (230a) MEV / MVHR — `IUF × SFP × 1.22 × V` per Table 4f +
|
||||
Table 4g defaults. PCDB MEV / MVHR lookup table is not yet in
|
||||
the codebase; defer to next slice.
|
||||
- (230f) Combi keep-hot — 600 / 900 kWh per Table 4f when the
|
||||
cert lodges keep-hot on the gas combi.
|
||||
- (230b) Warm-air heating fans + (230c) for warm-air pump.
|
||||
- (230h) WWHRS pump.
|
||||
"""
|
||||
total = 0.0
|
||||
total += _mev_decentralised_kwh_per_yr_from_cert(epc)
|
||||
details = epc.sap_heating.main_heating_details if epc.sap_heating else []
|
||||
if len(details) >= 2:
|
||||
main_2 = details[1]
|
||||
|
|
@ -265,6 +275,107 @@ def _table_4f_additive_components(epc: EpcPropertyData) -> float:
|
|||
25.0 + 5.0 * _TABLE_4F_SOLAR_HW_PUMP_DEFAULT_H1_M2
|
||||
) * 2.0
|
||||
return total
|
||||
|
||||
|
||||
# SAP 10.2 §2.6.4 decentralised MEV fan flow rates (l/s) per PCDF Spec
|
||||
# §A.19 field 14: 13 l/s for kitchen configurations (codes 1, 3, 5),
|
||||
# 8 l/s for other wet room configurations (codes 2, 4, 6).
|
||||
_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
|
||||
# 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
|
||||
_MV_DUCT_TYPE_RIGID: Final[int] = 2
|
||||
# Decentralised MEV PCDB fan-location codes (PCDF Spec §A.19 field 14):
|
||||
# 1, 2 = in-room with ducting (use flexible/rigid IUF per duct type)
|
||||
# 3, 4 = in-duct (use flexible/rigid IUF per duct type)
|
||||
# 5, 6 = through-wall (use no-duct IUF independent of duct type)
|
||||
_MEV_THROUGH_WALL_CONFIG_CODES: Final[frozenset[int]] = frozenset({5, 6})
|
||||
|
||||
|
||||
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
|
||||
+ flow) + 329 (per-ducting IUFs) + cert lodgement (wet-rooms
|
||||
count, ducting type).
|
||||
|
||||
Returns 0.0 when:
|
||||
- No MEV PCDF index is lodged (e.g. cert with no MV system or
|
||||
a non-decentralised MV system — the cascade routes through a
|
||||
different (230) line).
|
||||
- The PCDB Table 322 record isn't found for the lodged index
|
||||
(caller falls back to Table 4g default downstream — future
|
||||
slice).
|
||||
|
||||
The per-fan-configuration count distribution mimics the Elmhurst
|
||||
convention reverse-engineered from cert 000565:
|
||||
- Each PCDB-defined configuration (1..6) contributes 1 baseline
|
||||
fan to the installation, regardless of whether the PCDB row
|
||||
lodges measured SFP / flow.
|
||||
- Through-wall configurations scale with the wet-rooms count:
|
||||
through-wall kitchen (5): `wet_rooms_count` total fans
|
||||
through-wall other wet (6): `wet_rooms_count + 1` total fans
|
||||
(For cert 000565 wet_rooms=2, this yields the worksheet's
|
||||
observed (1, 1, 1, 1, 2, 3) count distribution.)
|
||||
|
||||
Configurations whose PCDB SFP is blank contribute 0 to the SFPav
|
||||
numerator but their flow rate (13 l/s kitchen, 8 l/s other wet)
|
||||
contributes to the denominator — matching the spec's "summation
|
||||
is over all the fans" semantics.
|
||||
|
||||
TODO: validate the count convention against a second MEV
|
||||
decentralised fixture; the rule above fits cert 000565 alone.
|
||||
"""
|
||||
pcdf_id = epc.mechanical_ventilation_index_number
|
||||
if pcdf_id is None:
|
||||
return 0.0
|
||||
record = decentralised_mev_record(pcdf_id)
|
||||
if record is None:
|
||||
return 0.0
|
||||
iuf_record = mv_in_use_factors_record(_MEV_DECENTRALISED_SYSTEM_TYPE)
|
||||
if iuf_record is None:
|
||||
return 0.0
|
||||
wet_rooms = epc.wet_rooms_count if epc.wet_rooms_count > 0 else 1
|
||||
duct_type = epc.mechanical_vent_duct_type
|
||||
if duct_type == _MV_DUCT_TYPE_RIGID:
|
||||
in_duct_iuf = iuf_record.sfp_iuf_rigid_no_scheme
|
||||
else:
|
||||
in_duct_iuf = iuf_record.sfp_iuf_flexible_no_scheme
|
||||
through_wall_iuf = iuf_record.sfp_iuf_no_duct_no_scheme
|
||||
if in_duct_iuf is None or through_wall_iuf is None:
|
||||
return 0.0
|
||||
fan_entries: list[MevFanEntry] = []
|
||||
configs_by_code = {c.config_code: c for c in record.fan_configs}
|
||||
for code in range(1, 7):
|
||||
config = configs_by_code.get(code)
|
||||
flow = (
|
||||
13.0 if code in _MEV_KITCHEN_FAN_CONFIG_CODES else 8.0
|
||||
)
|
||||
sfp = config.sfp_w_per_l_per_s if config is not None else None
|
||||
sfp_value = sfp if sfp is not None else 0.0
|
||||
iuf = through_wall_iuf if code in _MEV_THROUGH_WALL_CONFIG_CODES else in_duct_iuf
|
||||
# Baseline 1 fan per config; extra through-wall fans scale
|
||||
# with wet-rooms count per the Elmhurst convention.
|
||||
count = 1
|
||||
if code == 5:
|
||||
count = max(1, wet_rooms)
|
||||
elif code == 6:
|
||||
count = max(1, wet_rooms + 1)
|
||||
for _ in range(count):
|
||||
fan_entries.append(
|
||||
MevFanEntry(
|
||||
sfp_w_per_l_per_s=sfp_value,
|
||||
flow_rate_l_per_s=flow,
|
||||
iuf=iuf,
|
||||
)
|
||||
)
|
||||
sfp_av = mev_sfp_av(tuple(fan_entries))
|
||||
dimensions = dimensions_from_cert(epc)
|
||||
return mev_decentralised_kwh_per_yr(
|
||||
sfp_av_w_per_l_per_s=sfp_av,
|
||||
dwelling_volume_m3=dimensions.volume_m3,
|
||||
)
|
||||
# SAP10.2 Table 6d note 1: "average or unknown" overshading is the
|
||||
# default for existing dwellings. RdSAP doesn't lodge a per-dwelling
|
||||
# overshading code so §5 always uses AVERAGE → Z_L = 0.83.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue