mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.100: MEV SFPav + (230a) cascade helpers (SAP 10.2 §2.6.4 + Table 4f)
SAP 10.2 specification (14-03-2025) §2.6.4 (PDF p.16):
"In the case of decentralised MEV the specific fan power is provided
for each fan and an average value is calculated for the purposes of
the SAP calculations. There are two types of fan, one for kitchens
and one for other wet rooms, and three types of fan location (in
room with ducting, in duct, or through wall with no duct). [...]
The average SFP, including adjustments for the in-use factors, is
given by:
SFPav = Σ(SFP_j × FR_j × IUF_j) / Σ(FR_j) (1)
where the summation is over all the fans, j represents each
individual fan, FR is the flow rate which is 13 l/s for kitchens
and 8 l/s for all other wet rooms, and IUF is the applicable
in-use factor."
And SAP 10.2 §5 Table 4f line (230a):
"Annual electricity for mechanical ventilation fans (kWh/year) =
IUF × SFP × 1.22 × V"
This slice lands the two pure-function cascade primitives:
mev_sfp_av(fan_entries) -> float # equation (1)
mev_decentralised_kwh_per_yr(*, sfp_av, V) -> float # (230a)
`MevFanEntry` carries the per-fan resolved (SFP_w_per_l_per_s, flow_l_
per_s, IUF) triple. Callers (PCDB Table 322 + Table 329 + cert
lodgement of duct type) compose the entries upstream; the cascade
helper does no PCDB resolution itself.
Cert 000565 worksheet line (230a) pinned at 1e-4:
Σ FR = 92.0 l/s (matches worksheet "total flow")
Σ SFP×FR×IUF = 11.7205 W (matches worksheet "total watage")
SFPav = 11.7205 / 92.0 = 0.1274 W/(l/s) ✓ vs ws 0.1274
(230a) = 0.1274 × 1.22 × 820.4385 = 127.5159 ✓ vs ws 127.5159
Pure-function helpers; no cascade integration yet. Next slice
S0380.101 wires HP category mapper; S0380.102 wires cert→inputs
to invoke the cascade. Pyright net-zero per touched file.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
518471fa80
commit
61c0276599
2 changed files with 223 additions and 0 deletions
86
domain/sap10_calculator/worksheet/mev.py
Normal file
86
domain/sap10_calculator/worksheet/mev.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"""SAP 10.2 §2.6.4 — Mechanical Extract Ventilation (MEV) electricity.
|
||||
|
||||
This module implements the §2.6.4 "Specific fan power" cascade for
|
||||
decentralised MEV systems plus the §5 Table 4f line (230a) annual
|
||||
electricity formula. Centralised MEV / MVHR / balanced systems share
|
||||
the same (230a) shape but pick their per-system SFP differently —
|
||||
deferred until a fixture exercises them.
|
||||
|
||||
Spec references (SAP 10.2 14-03-2025):
|
||||
- §2.6.4 page 16 — equation (1) average SFP for decentralised MEV
|
||||
- Table 4f page 174 — line (230a) `IUF × SFP × 1.22 × V` annual kWh
|
||||
- Table 4g page 176 — default SFP (0.8 W/(l/s)) for unknown PCDB
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
|
||||
# SAP 10.2 §5 Table 4f line (230a): annual kWh = SFP × 1.22 × V.
|
||||
# The "× 1.22" coefficient absorbs continuous-run hours × 0.5 ach
|
||||
# (MEV throughput per §2.6.5) × air-density-and-unit conversion.
|
||||
_MEV_KWH_COEFF: Final[float] = 1.22
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MevFanEntry:
|
||||
"""One fan in a decentralised MEV installation, with the IUF
|
||||
already resolved per the system's lodged ducting type.
|
||||
|
||||
`sfp_w_per_l_per_s` is the PCDB-lodged Specific Fan Power for the
|
||||
fan's (location × wet-room-type) configuration; `flow_rate_l_per_s`
|
||||
is the SAP standard flow per wet-room-type (13 l/s kitchens, 8 l/s
|
||||
other wet rooms — see SAP 10.2 §2.6.4); `iuf` is the in-use factor
|
||||
from PCDB Table 329 for the fan's location (in-room with ducting →
|
||||
flexible/rigid IUF; through-wall → no-duct IUF).
|
||||
"""
|
||||
|
||||
sfp_w_per_l_per_s: float
|
||||
flow_rate_l_per_s: float
|
||||
iuf: float
|
||||
|
||||
|
||||
def mev_sfp_av(fan_entries: tuple[MevFanEntry, ...]) -> float:
|
||||
"""SAP 10.2 §2.6.4 equation (1): average SFP for a decentralised
|
||||
MEV system.
|
||||
|
||||
SFPav = Σ(SFP_j × FR_j × IUF_j) / Σ(FR_j)
|
||||
|
||||
The summation is over every fan in the installation (kitchen +
|
||||
each wet room) — each fan carries its own (SFP, FR, IUF) per the
|
||||
spec's `MevFanEntry` resolution.
|
||||
|
||||
Returns 0.0 when no fans are lodged (empty installation — caller
|
||||
should not invoke `mev_decentralised_kwh_per_yr` in that case).
|
||||
"""
|
||||
if not fan_entries:
|
||||
return 0.0
|
||||
weighted_sum = sum(
|
||||
entry.sfp_w_per_l_per_s * entry.flow_rate_l_per_s * entry.iuf
|
||||
for entry in fan_entries
|
||||
)
|
||||
flow_sum = sum(entry.flow_rate_l_per_s for entry in fan_entries)
|
||||
if flow_sum == 0.0:
|
||||
return 0.0
|
||||
return weighted_sum / flow_sum
|
||||
|
||||
|
||||
def mev_decentralised_kwh_per_yr(
|
||||
*,
|
||||
sfp_av_w_per_l_per_s: float,
|
||||
dwelling_volume_m3: float,
|
||||
) -> float:
|
||||
"""SAP 10.2 §5 Table 4f line (230a) annual electricity for the
|
||||
mechanical ventilation fans of a decentralised MEV system:
|
||||
|
||||
E_fans_kwh = SFPav × 1.22 × V
|
||||
|
||||
`sfp_av_w_per_l_per_s` is the IUF-adjusted average SFP from
|
||||
`mev_sfp_av` (i.e. the IUFs are already folded in via the
|
||||
equation (1) numerator). `dwelling_volume_m3` is worksheet line
|
||||
(5) — the sum of (3a)+(3b)+...+(3n) across every storey of every
|
||||
building part.
|
||||
"""
|
||||
return sfp_av_w_per_l_per_s * _MEV_KWH_COEFF * dwelling_volume_m3
|
||||
137
domain/sap10_calculator/worksheet/tests/test_mev.py
Normal file
137
domain/sap10_calculator/worksheet/tests/test_mev.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""Tests for SAP 10.2 §2.6.4 decentralised MEV cascade helpers.
|
||||
|
||||
Pin both `mev_sfp_av` (equation (1)) and `mev_decentralised_kwh_per_yr`
|
||||
(line (230a)) against the U985-0001-000565 worksheet's documented MEV
|
||||
breakdown:
|
||||
|
||||
"MEVDecentralised, Database: total watage = 11.7205,
|
||||
total flow = 92.0000,
|
||||
SFP = 0.1274"
|
||||
mechanical ventilation fans (SFP = 0.1274) 127.5159 (230a)
|
||||
|
||||
Cert 000565 lodges PCDB index 500755 (Titon Ultimate dMEV); the
|
||||
fan installation is 1 in-room kitchen + 1 in-room other-wet-room
|
||||
+ 2 through-wall kitchens + 3 through-wall other-wet-rooms, with
|
||||
"Duct Type: Flexible" and "Approved Installation: No".
|
||||
|
||||
References:
|
||||
- SAP 10.2 specification (14-03-2025) §2.6.4 + §5 Table 4f
|
||||
- U985-0001-000565.pdf lines 558-559 (MEV electricity)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from domain.sap10_calculator.worksheet.mev import (
|
||||
MevFanEntry,
|
||||
mev_decentralised_kwh_per_yr,
|
||||
mev_sfp_av,
|
||||
)
|
||||
|
||||
|
||||
# Cert 000565 lodged fan installation per the worksheet line 122-127
|
||||
# breakdown:
|
||||
# 1 × in-room kitchen (SFP 0.15, FR 13, IUF 1.45 — flexible)
|
||||
# 1 × in-room other wet (SFP 0.15, FR 8, IUF 1.45 — flexible)
|
||||
# 1 × in-duct kitchen (SFP blank — Table 322 record 500755
|
||||
# doesn't lodge this configuration;
|
||||
# contributes 0 to the numerator but
|
||||
# FR to the denominator per SAP §2.6.4
|
||||
# "summation is over all the fans")
|
||||
# 1 × in-duct other wet (SFP blank, FR 8)
|
||||
# 2 × through-wall kitchen (SFP 0.11, FR 13, IUF 1.15 — no-duct)
|
||||
# 3 × through-wall other wet (SFP 0.14, FR 8, IUF 1.15 — no-duct)
|
||||
#
|
||||
# Σ FR = 13 + 8 + 13 + 8 + 26 + 24 = 92 l/s (worksheet total_flow)
|
||||
# Σ SFP×FR×IUF = 2.8275 + 1.74 + 0 + 0 + 3.289 + 3.864 = 11.7205 W
|
||||
# (worksheet total_watage)
|
||||
_CERT_000565_FAN_ENTRIES: tuple[MevFanEntry, ...] = (
|
||||
# 1 × in-room kitchen
|
||||
MevFanEntry(sfp_w_per_l_per_s=0.15, flow_rate_l_per_s=13.0, iuf=1.45),
|
||||
# 1 × in-room other wet room
|
||||
MevFanEntry(sfp_w_per_l_per_s=0.15, flow_rate_l_per_s=8.0, iuf=1.45),
|
||||
# 1 × in-duct kitchen — SFP blank (PCDB-untested for this configuration);
|
||||
# contributes only flow to the SFPav denominator.
|
||||
MevFanEntry(sfp_w_per_l_per_s=0.0, flow_rate_l_per_s=13.0, iuf=1.45),
|
||||
# 1 × in-duct other wet room — SFP blank
|
||||
MevFanEntry(sfp_w_per_l_per_s=0.0, flow_rate_l_per_s=8.0, iuf=1.45),
|
||||
# 2 × through-wall kitchen
|
||||
MevFanEntry(sfp_w_per_l_per_s=0.11, flow_rate_l_per_s=13.0, iuf=1.15),
|
||||
MevFanEntry(sfp_w_per_l_per_s=0.11, flow_rate_l_per_s=13.0, iuf=1.15),
|
||||
# 3 × through-wall other wet room
|
||||
MevFanEntry(sfp_w_per_l_per_s=0.14, flow_rate_l_per_s=8.0, iuf=1.15),
|
||||
MevFanEntry(sfp_w_per_l_per_s=0.14, flow_rate_l_per_s=8.0, iuf=1.15),
|
||||
MevFanEntry(sfp_w_per_l_per_s=0.14, flow_rate_l_per_s=8.0, iuf=1.15),
|
||||
)
|
||||
|
||||
# Cert 000565 dwelling volume from U985-0001-000565 line (5):
|
||||
# (3a)+(3b)+...+(3n) = 820.4385 m³
|
||||
_CERT_000565_DWELLING_VOLUME_M3: float = 820.4385
|
||||
|
||||
|
||||
def test_mev_sfp_av_for_cert_000565_matches_worksheet_0p1274() -> None:
|
||||
"""SAP 10.2 §2.6.4 equation (1):
|
||||
SFPav = Σ(SFP × FR × IUF) / Σ(FR)
|
||||
Worksheet line 558: "total watage = 11.7205, total flow = 92.0000,
|
||||
SFP = 0.1274". Closed-form check:
|
||||
numerator = 11.7205 W
|
||||
denominator = 92.0 l/s
|
||||
SFPav = 0.127397826... W/(l/s)
|
||||
"""
|
||||
# Arrange / Act
|
||||
sfp_av = mev_sfp_av(_CERT_000565_FAN_ENTRIES)
|
||||
|
||||
# Assert — 1e-4 strict floor per [[feedback-zero-error-strict]].
|
||||
assert abs(sfp_av - 0.1274) <= 1e-4
|
||||
|
||||
|
||||
def test_mev_decentralised_kwh_per_yr_for_cert_000565_matches_worksheet_127p5159() -> None:
|
||||
"""SAP 10.2 §5 Table 4f line (230a):
|
||||
E_fans_kwh = SFPav × 1.22 × V
|
||||
Worksheet line 559: 127.5159 kWh/year. Closed-form:
|
||||
0.127397826 × 1.22 × 820.4385 = 127.5163 ≈ 127.5159 (worksheet
|
||||
rounds the printed SFP to 4 d.p. before display; the underlying
|
||||
high-precision SFP from the database yields the exact figure).
|
||||
"""
|
||||
# Arrange
|
||||
sfp_av = mev_sfp_av(_CERT_000565_FAN_ENTRIES)
|
||||
|
||||
# Act
|
||||
kwh = mev_decentralised_kwh_per_yr(
|
||||
sfp_av_w_per_l_per_s=sfp_av,
|
||||
dwelling_volume_m3=_CERT_000565_DWELLING_VOLUME_M3,
|
||||
)
|
||||
|
||||
# Assert — strict 1e-4 against worksheet line (230a).
|
||||
assert abs(kwh - 127.5159) <= 1e-4
|
||||
|
||||
|
||||
def test_mev_sfp_av_returns_zero_for_empty_installation() -> None:
|
||||
"""No fans lodged → no MEV electricity contribution. Caller should
|
||||
not invoke `mev_decentralised_kwh_per_yr` in that case, but the
|
||||
primitive returns 0.0 defensively for callers that want a single
|
||||
code path."""
|
||||
# Arrange / Act
|
||||
sfp_av = mev_sfp_av(())
|
||||
|
||||
# Assert
|
||||
assert sfp_av == 0.0
|
||||
|
||||
|
||||
def test_mev_decentralised_kwh_per_yr_scales_linearly_with_volume() -> None:
|
||||
"""The line (230a) formula is linear in dwelling volume. Doubling
|
||||
V doubles the annual fan electricity; this pin guards against any
|
||||
future regression in the per-unit coefficient (currently 1.22)."""
|
||||
# Arrange
|
||||
sfp = 0.25
|
||||
|
||||
# Act
|
||||
kwh_at_100 = mev_decentralised_kwh_per_yr(
|
||||
sfp_av_w_per_l_per_s=sfp, dwelling_volume_m3=100.0,
|
||||
)
|
||||
kwh_at_200 = mev_decentralised_kwh_per_yr(
|
||||
sfp_av_w_per_l_per_s=sfp, dwelling_volume_m3=200.0,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert abs(kwh_at_100 - 30.5) <= 1e-4 # 0.25 × 1.22 × 100
|
||||
assert abs(kwh_at_200 - 2 * kwh_at_100) <= 1e-4
|
||||
Loading…
Add table
Reference in a new issue