mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
The calculator tests lived under domain/sap10_calculator/{tests,worksheet/
tests,rdsap/tests,climate/tests,validation/tests}, none of which are in
pytest.ini testpaths — so CI (which collects tests/) never ran them. Relocate
all five dirs to tests/domain/sap10_calculator/{,worksheet,rdsap,climate,
validation}, mirroring the tests/domain/property_baseline/ convention, so the
cascade-pin / golden / e2e conformance suites run in CI.
Mechanics:
- git mv preserves history (110 files).
- Flattening the trailing /tests keeps each file's depth-to-repo-root
identical, so all 16 repo-root parents[4] fixture refs stay valid. Only
test_pcdb_etl.py's parents[1] (→ pcdb data) and one hardcoded absolute
golden-fixture path in test_cert_to_inputs.py needed rebasing.
- Cross-imports rewritten domain.sap10_calculator.worksheet.tests →
tests.domain.sap10_calculator.worksheet (21 files incl. the external
importer backend/documents_parser/tests/test_summary_pdf_mapper_chain.py).
- Golden-fixture path strings in test_summary_pdf_mapper_chain.py +
scripts/fetch_cohort2_api_jsons.py updated to the new location (the JSONs
moved with the rdsap tests).
load_cells / gitignored worksheet xlsx: the xlsx-pinned tests (test_dimensions
/ ventilation / water_heating) read 2026-05-19-17-18 RdSap10Worksheet.xlsx,
which is gitignored (.gitignore `*.xlsx`) and so absent in CI. _xlsx_loader.
load_cells now pytest.skip()s when the file is absent, so those tests run
locally and skip cleanly in CI instead of erroring — no new CI failures from
the move, and the gitignore policy is respected.
Verified: tests/domain/sap10_calculator + backend/documents_parser +
tests/domain/property_baseline = 2248 pass, 1 skipped; pyright resolves the
new import paths with zero import-resolution errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
137 lines
5.4 KiB
Python
137 lines
5.4 KiB
Python
"""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
|