mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(ventilation): dMEV takes lodged 0 intermittent fans, not the Table 5 default (SAP 10.2 §2)
Chasing the space-heating demand gap on "simulated case 48" (main 691 + Unknown meter + 903 dual immersion): our SAP 55 vs Elmhurst 57. Every §10a cost line already matched to the penny; the residual was demand — our space-heating energy 3849.8 kWh vs Elmhurst 3513.8 (+9.6%). Traced through the worksheet: our ventilation heat loss (38) ran ~35.5 W/K vs Elmhurst 27.76 — we were adding 20 m3/h of intermittent extract fans (the Table 5 age-band default) on a dwelling with a decentralised mechanical extract (dMEV) system that lodges 0 fans. SAP 10.2 §2 (PDF p.13): a whole-house mechanical EXTRACT system provides extraction via the (23a) 0.5 system air-change rate; the lodged intermittent extract-fan count (7a) is then explicit — a lodged 0 means 0 (the dMEV is the ventilation), NOT "unknown". The Table 5 default is an unknown-fallback for NATURALLY ventilated dwellings only, so it must not be substituted here. Fix: for EXTRACT_OR_PIV_OUTSIDE, take vc.intermittent_fans as-is (no age-band default). Worksheet-proven on two dMEV builds of cert 000565: "case 48" lodges (7a)=0 -> our SAP 55 -> 57 EXACT; the original 000565 fixture lodges (7a)=2 and keeps 2 (its e2e pins are unchanged). An earlier draft that forced fans=0 broke 000565 (which legitimately has 2) — corrected to "lodged as-is". within-0.5 72.5% -> 72.6%, MAE 0.789 -> 0.788; CO2/PE unchanged. The fix also reduces a systematic under-rating bias in the 21-cert dMEV cohort (median dSAP -0.22 -> -0.08). Scoped to EXTRACT_OR_PIV_OUTSIDE; balanced MVHR/MV kinds left untouched pending their own worksheet. SAP-schema regression test_18_0_0 pin 80 -> 81 (closer to its lodged 84, same cause). Spec-pinned in test_cert_to_inputs (dMEV-lodged-0 vs natural-default). pyright not installed in this container -- strict type gate not run locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fd0530159f
commit
4db05e843c
4 changed files with 69 additions and 2 deletions
|
|
@ -506,7 +506,10 @@ class TestFromSapSchema16_2:
|
|||
epc = EpcPropertyDataMapper.from_api_response(load("sap_18_0_0.json"))
|
||||
assert isinstance(epc, EpcPropertyData)
|
||||
assert epc.uprn == 10094601287
|
||||
assert Sap10Calculator().calculate(epc).sap_score == 80 # lodged 84
|
||||
# 80 -> 81 (closer to lodged 84) after the dMEV intermittent-fan fix:
|
||||
# this cert is EXTRACT_OR_PIV_OUTSIDE with a lodged 0 fans, so the
|
||||
# Table 5 age-band default is no longer substituted (SAP 10.2 §2 (7a)).
|
||||
assert Sap10Calculator().calculate(epc).sap_score == 81 # lodged 84
|
||||
|
||||
def test_16_0_dispatches_via_16_x_path_with_tenure_default(self) -> None:
|
||||
# SAP-Schema-16.0 is the same reduced-field 16.x shape; it omits the
|
||||
|
|
|
|||
|
|
@ -5090,6 +5090,17 @@ def ventilation_from_cert(
|
|||
# can override via a future plumbing slice; the spec default
|
||||
# is what every MEV / MV / MVHR cohort cert lodges today.
|
||||
mv_system_ach = 0.5
|
||||
# For a whole-house mechanical EXTRACT system (MEV / dMEV) the
|
||||
# lodged intermittent extract-fan count (7a) is taken AS-IS — the
|
||||
# Table 5 age-band default must NOT be substituted for a lodged 0.
|
||||
# On a mechanically-ventilated dwelling the fan count is explicit
|
||||
# (the dMEV is the ventilation), so 0 means 0, not "unknown".
|
||||
# Worksheet-proven on two dMEV builds of 000565: "case 48" lodges
|
||||
# (7a)=0 → SAP 57 exact, while the original 000565 fixture lodges
|
||||
# (7a)=2 → unchanged. Scoped to EXTRACT_OR_PIV_OUTSIDE; balanced
|
||||
# MVHR/MV kinds are left untouched pending their own worksheet.
|
||||
if mv_kind is MechanicalVentilationKind.EXTRACT_OR_PIV_OUTSIDE:
|
||||
intermittent_fans = vc.intermittent_fans
|
||||
return ventilation_from_inputs(
|
||||
volume_m3=vol,
|
||||
storey_count=storeys,
|
||||
|
|
|
|||
|
|
@ -1762,6 +1762,56 @@ def test_ventilation_from_cert_applies_table_5_default_when_lodged_zero() -> Non
|
|||
)
|
||||
|
||||
|
||||
def test_ventilation_from_cert_dmev_takes_lodged_zero_fans_not_table_5_default() -> None:
|
||||
# Arrange — on a dwelling with a whole-house mechanical EXTRACT system
|
||||
# (dMEV → EXTRACT_OR_PIV_OUTSIDE) the lodged intermittent extract-fan
|
||||
# count (7a) is explicit: a lodged 0 means 0 (the dMEV is the
|
||||
# ventilation), NOT "unknown". The SAP 10.2 §2 Table 5 age-band default
|
||||
# must NOT be substituted as it would on a NATURALLY ventilated dwelling.
|
||||
# Worksheet-proven: two dMEV builds of cert 000565 — "simulated case 48"
|
||||
# lodges (7a)=0 and Elmhurst's worksheet uses 0 (→ SAP 57 exact), while
|
||||
# the original 000565 fixture lodges (7a)=2 and Elmhurst uses 2. The same
|
||||
# Table 5 default that this age-G NATURAL case applies (1 fan above) must
|
||||
# be suppressed here.
|
||||
from domain.sap10_calculator.worksheet.ventilation import (
|
||||
MechanicalVentilationKind,
|
||||
)
|
||||
age_g_part = make_building_part(
|
||||
floor_dimensions=[
|
||||
make_floor_dimension(total_floor_area_m2=45.0, floor=0),
|
||||
make_floor_dimension(total_floor_area_m2=45.0, floor=1),
|
||||
],
|
||||
construction_age_band='G',
|
||||
)
|
||||
base = _typical_semi_detached_epc()
|
||||
epc_dmev = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
region_code="1",
|
||||
sap_building_parts=[age_g_part],
|
||||
sap_windows=base.sap_windows,
|
||||
sap_heating=base.sap_heating,
|
||||
sap_ventilation=SapVentilation(
|
||||
extract_fans_count=0,
|
||||
mechanical_ventilation_kind=(
|
||||
MechanicalVentilationKind.EXTRACT_OR_PIV_OUTSIDE.name
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# Act
|
||||
v = ventilation_from_cert(epc_dmev)
|
||||
|
||||
# Assert — 0 intermittent fans contribute 0 to (8), so openings ACH = 0
|
||||
# (no Table 5 age-G default fan); the mechanical system is applied via
|
||||
# the separate (23a) 0.5 system air-change rate instead.
|
||||
assert abs(v.openings_ach) <= 1e-9, (
|
||||
f"openings ACH {v.openings_ach:.6f} should be 0 for a dMEV cert "
|
||||
f"lodging 0 fans (no Table 5 default substituted)"
|
||||
)
|
||||
assert v.mv_system_ach == 0.5
|
||||
|
||||
|
||||
def test_ventilation_from_cert_uses_lodged_fans_below_age_default_not_floored() -> None:
|
||||
# Arrange — RdSAP 10 §4.1 Table 5 (PDF p.28): "Number of extract fans if
|
||||
# known; if unknown: [age-band default]." The default is an UNKNOWN-
|
||||
|
|
|
|||
|
|
@ -197,7 +197,10 @@ _MIN_WITHIN_HALF_SAP = 0.72
|
|||
# 0.793 -> 0.789 via the §12 Unknown-meter + dual-electric-immersion off-peak
|
||||
# trigger (RdSAP 10 PDF p.62): Apartment 241 (main 691 + 903 dual immersion)
|
||||
# -5.38 -> -1.05. Worksheet-validated on "simulated case 48" (Elmhurst SAP 57,
|
||||
# 10-Hour Off Peak). within-0.5 holds 72.5%.
|
||||
# 10-Hour Off Peak). Then 0.789 -> 0.788 (within-0.5 72.5% -> 72.6%) via the
|
||||
# dMEV intermittent-fan fix: an EXTRACT_OR_PIV_OUTSIDE cert lodging 0 fans now
|
||||
# takes 0 (not the Table 5 age-band default) — same "case 48" worksheet closes
|
||||
# its space-heating demand to land SAP 57 exact.
|
||||
_MAX_SAP_MAE = 0.79
|
||||
_MAX_CO2_MAE_TONNES = 0.09 # t CO2 / yr vs co2_emissions_current
|
||||
_MAX_PE_PER_M2_MAE = 3.7 # kWh / m2 / yr vs energy_consumption_current
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue