slice S-B21: wind shelter factor on infiltration (SAP §2)

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-18 18:56:35 +00:00
parent b73690fe6e
commit 7786a6e9b7
2 changed files with 15 additions and 2 deletions

View file

@ -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)

View file

@ -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,