"""Tests for ventilation_heat_loss_w_per_k. SAP10.2 §C + RdSAP10 §5 / Table 5: air change rate per hour (ACH) from structural infiltration + openings minus draught-proofing reduction, then W/K = ACH * volume_m3 * 0.33. Volume is total floor area * average storey height. Open chimneys, blocked chimneys, and flueless gas fires contribute fixed m³/h additions; draught-proofed windows reduce the structural baseline. """ import pytest from domain.sap10_ml.ventilation import ventilation_heat_loss_w_per_k def test_ventilation_bare_masonry_no_openings_returns_structural_baseline_only() -> None: # Arrange — 80 m² × 2.5 m = 200 m³, masonry struct ACH = 0.35, # 100% draught-proofed windows -> 0.05 reduction => 0.30 ACH total. # 0.30 * 200 * 0.33 = 19.8 W/K. # Act result = ventilation_heat_loss_w_per_k( total_floor_area_m2=80.0, avg_room_height_m=2.5, is_timber_frame=False, open_chimneys_count=0, window_pct_draught_proofed=100.0, ) # Assert assert result == pytest.approx(19.8, abs=0.5) def test_ventilation_timber_frame_no_openings_lower_struct_baseline() -> None: # Arrange — same volume but timber frame -> struct ACH = 0.25. # 100% DP -> 0.20 ACH; 0.20 * 200 * 0.33 = 13.2 W/K. # Act result = ventilation_heat_loss_w_per_k( total_floor_area_m2=80.0, avg_room_height_m=2.5, is_timber_frame=True, open_chimneys_count=0, window_pct_draught_proofed=100.0, ) # Assert assert result == pytest.approx(13.2, abs=0.5) def test_ventilation_open_chimney_adds_40_m3_per_h() -> None: # Arrange — masonry, 100% DP (0.30 base ACH), one open chimney adds 40 m³/h. # In 200 m³ volume that's 0.20 ACH, so total = 0.50. # 0.50 * 200 * 0.33 = 33.0 W/K. # Act result = ventilation_heat_loss_w_per_k( total_floor_area_m2=80.0, avg_room_height_m=2.5, is_timber_frame=False, open_chimneys_count=1, window_pct_draught_proofed=100.0, ) # Assert assert result == pytest.approx(33.0, abs=0.5) def test_ventilation_no_draught_proofing_raises_structural_share() -> None: # Arrange — masonry, 0% DP -> 0.35 ACH unreduced; no openings. # 0.35 * 200 * 0.33 = 23.1 W/K. # Act result = ventilation_heat_loss_w_per_k( total_floor_area_m2=80.0, avg_room_height_m=2.5, is_timber_frame=False, open_chimneys_count=0, window_pct_draught_proofed=0.0, ) # Assert assert result == pytest.approx(23.1, abs=0.5) def test_ventilation_heritage_home_with_two_chimneys_and_no_dp() -> None: # Arrange — catastrophic heritage profile: TFA 100, room_h 2.5 (vol 250), # masonry, 2 open chimneys (80 m³/h), 0% draught-proofed. # struct=0.35, openings=80/250=0.32, total=0.67 ACH. # 0.67 * 250 * 0.33 = 55.3 W/K. # Act result = ventilation_heat_loss_w_per_k( total_floor_area_m2=100.0, avg_room_height_m=2.5, is_timber_frame=False, open_chimneys_count=2, window_pct_draught_proofed=0.0, ) # Assert assert result == pytest.approx(55.3, abs=1.0) def test_ventilation_returns_zero_when_floor_area_is_zero() -> None: # Arrange — no volume, no air loss. # Act result = ventilation_heat_loss_w_per_k( total_floor_area_m2=0.0, avg_room_height_m=2.5, is_timber_frame=False, open_chimneys_count=1, window_pct_draught_proofed=0.0, ) # Assert assert result == 0.0 def test_ventilation_handles_null_chimney_count_as_zero() -> None: # Arrange — open_chimneys_count is None on ~70% of the corpus; treat as 0. # Act result = ventilation_heat_loss_w_per_k( total_floor_area_m2=80.0, avg_room_height_m=2.5, is_timber_frame=False, open_chimneys_count=None, window_pct_draught_proofed=100.0, ) # Assert — same as zero-chimney case (19.8 W/K). assert result == pytest.approx(19.8, abs=0.5) def test_ventilation_handles_null_dp_share_as_zero() -> None: # Arrange — null draught-proofing share treated as no draught-proofing. # Act result = ventilation_heat_loss_w_per_k( total_floor_area_m2=80.0, avg_room_height_m=2.5, is_timber_frame=False, open_chimneys_count=0, window_pct_draught_proofed=None, ) # Assert — falls back to full 0.35 struct ACH -> 23.1 W/K. assert result == pytest.approx(23.1, abs=0.5)