diff --git a/domain/sap10_calculator/worksheet/mev.py b/domain/sap10_calculator/worksheet/mev.py new file mode 100644 index 00000000..3784d133 --- /dev/null +++ b/domain/sap10_calculator/worksheet/mev.py @@ -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 diff --git a/domain/sap10_calculator/worksheet/tests/test_mev.py b/domain/sap10_calculator/worksheet/tests/test_mev.py new file mode 100644 index 00000000..64967f88 --- /dev/null +++ b/domain/sap10_calculator/worksheet/tests/test_mev.py @@ -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