From 7786a6e9b70d458cd67d74f9e63a441c97250d8b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 18 May 2026 18:56:35 +0000 Subject: [PATCH] =?UTF-8?q?slice=20S-B21:=20wind=20shelter=20factor=20on?= =?UTF-8?q?=20infiltration=20(SAP=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per SAP 10.3 §2 worksheet line 22 / RdSAP10 §4.1: effective infiltration = raw_ACH × (1 - 0.075 × sheltered_sides). Default 2 sheltered sides for typical UK terraced/semi-detached layout (the cert doesn't lodge a sheltered-sides count, so we apply the spec's typical default). infiltration_ach() gains a `sheltered_sides` kwarg defaulting to 0 (spec-pure intermediate result; existing unit tests keep that contract). cert_to_inputs passes sheltered_sides=2. Found via energy decomposition: our predicted total energy was running +15.7 kWh/m² over cert (10% over) — wind shelter knocks ~15% off infiltration, contributing to closing that gap. 300-cert parity probe: MAE 5.43 → 5.34 (-0.09) bias -0.52 → +0.29 (back near zero) within ±10: 86.3% → 86.7% Co-Authored-By: Claude Opus 4.7 --- .../domain/src/domain/sap/rdsap/cert_to_inputs.py | 4 ++++ .../domain/src/domain/sap/worksheet/ventilation.py | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py index fe18d5ec..91968326 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -720,6 +720,10 @@ def cert_to_inputs( passive_vents=vc.passive_vents, flueless_gas_fires=vc.flueless_gas_fires, window_pct_draught_proofed=float(epc.percent_draughtproofed or 0), + # SAP §2 shelter factor: 2 sheltered sides default per typical + # UK terraced/semi-detached layout. The cert doesn't lodge a + # sheltered-sides count, so we apply the spec's typical default. + sheltered_sides=2, ) main = _first_main_heating(epc) diff --git a/packages/domain/src/domain/sap/worksheet/ventilation.py b/packages/domain/src/domain/sap/worksheet/ventilation.py index 10dc26e2..ca00393e 100644 --- a/packages/domain/src/domain/sap/worksheet/ventilation.py +++ b/packages/domain/src/domain/sap/worksheet/ventilation.py @@ -70,9 +70,14 @@ def infiltration_ach( suspended_timber_floor_sealed: bool = False, has_draught_lobby: bool = False, window_pct_draught_proofed: float = 0.0, + sheltered_sides: int = 0, ) -> InfiltrationBreakdown: """Air-change rate (ach) per SAP 10.3 §2 / RdSAP10 §4.1, no pressure - test path.""" + test path. `sheltered_sides` defaults to 0 (no shelter; spec-pure + intermediate value). Callers can pass 2 (typical UK terraced / + semi-detached) to apply the SAP §2 shelter factor + (1 - 0.075 × sheltered_sides) so the returned total_ach is the + effective rate after wind shelter.""" if volume_m3 <= 0: raise ValueError(f"volume_m3 must be > 0, got {volume_m3}") openings_m3_h = ( @@ -95,7 +100,11 @@ def infiltration_ach( floor = 0.0 draught_lobby = 0.0 if has_draught_lobby else 0.05 window = 0.25 - 0.2 * (window_pct_draught_proofed / 100.0) - total = openings + additional + structural + floor + draught_lobby + window + raw_total = openings + additional + structural + floor + draught_lobby + window + # SAP §2 worksheet line 22 shelter factor: 1 - 0.075 × sheltered_sides. + # 2 sheltered sides → multiply by 0.85. + shelter_factor = 1.0 - 0.075 * max(0, min(4, sheltered_sides)) + total = raw_total * shelter_factor return InfiltrationBreakdown( openings_ach=openings, additional_ach=additional,