"""Ventilation heat-loss W/K from SAP10.2 §C + RdSAP10 §5 / Table 5. The ventilation feature complements `envelope_heat_loss_w_per_k` (conduction + thermal bridging only). Catastrophic-low-SAP homes are dominated by infiltration that the conduction model can't see: open chimneys, no draught-proofing, leaky old windows. Tracer-bullet scope: ACH_total = ACH_struct + (chimney_m3_h / volume_m3) − DP_reduction W/K = ACH_total × volume_m3 × 0.33 Where 0.33 = ρ_air × c_p_air in kWh/(m³·K) at typical UK indoor conditions. Scope explicitly deferred: - Mechanical ventilation (MVHR / MEV) — `mechanical_ventilation` is 100% null in the 250k corpus, so no signal to act on yet. - Pressure-test override — `pressure_test` is also 100% null (see slice 18e candidate in HANDOFF §7-D). - Open flues / passive vents / extract fans / flueless gas fires — read off `sap_ventilation` which itself is sparsely populated. """ from __future__ import annotations from typing import Final, Optional _STRUCTURAL_ACH_MASONRY: Final[float] = 0.35 _STRUCTURAL_ACH_TIMBER: Final[float] = 0.25 _DRAUGHT_PROOFING_MAX_REDUCTION: Final[float] = 0.05 _OPEN_CHIMNEY_M3_PER_H: Final[float] = 40.0 _AIR_HEAT_CAPACITY_KWH_PER_M3_K: Final[float] = 0.33 def ventilation_heat_loss_w_per_k( total_floor_area_m2: float, avg_room_height_m: float, *, is_timber_frame: bool, open_chimneys_count: Optional[int], window_pct_draught_proofed: Optional[float], ) -> float: """SAP10.2 §C ventilation heat-loss in W/K, never null. Linear sum: structural infiltration ACH + chimneys ACH − draught-proofing reduction, multiplied by dwelling volume × 0.33. """ if total_floor_area_m2 <= 0 or avg_room_height_m <= 0: return 0.0 volume_m3 = total_floor_area_m2 * avg_room_height_m struct_ach = _STRUCTURAL_ACH_TIMBER if is_timber_frame else _STRUCTURAL_ACH_MASONRY chimney_m3_h = (open_chimneys_count or 0) * _OPEN_CHIMNEY_M3_PER_H chimney_ach = chimney_m3_h / volume_m3 dp_share = (window_pct_draught_proofed or 0.0) / 100.0 dp_reduction = _DRAUGHT_PROOFING_MAX_REDUCTION * dp_share total_ach = max(0.0, struct_ach - dp_reduction) + chimney_ach return total_ach * volume_m3 * _AIR_HEAT_CAPACITY_KWH_PER_M3_K