Model/domain/sap10_ml/tests/test_ventilation.py
Khalim Conn-Kowlessar 68401c517a refactor: lift-and-shift packages/domain/src/domain/ml → domain/sap10_ml
Sibling migration to the sap10_calculator move — `domain.ml` now lives
at the root-level layout (`domain/sap10_ml/`) matching the pattern
already used by `domain.addresses`, `domain.tasks`, `domain.postcode`,
and `domain.sap10_calculator`.

Changes:

- `git mv packages/domain/src/domain/ml → domain/sap10_ml` (19 files;
  history preserved).
- Subpackage rename: `domain.ml` → `domain.sap10_ml`. 32 references
  rewritten across .py and .md files: 11 internal + 21 external
  (datatypes/epc/domain/mapper.py, 14 files in domain/sap10_calculator,
  2 backend tests, 2 ADRs, 1 README, 1 design doc).
- Path-string updates: `pytest.ini` testpath
  `packages/domain/src/domain/ml/tests` → `domain/sap10_ml/tests` so
  ML tests stay in the default auto-discovered sweep. `CONTEXT.md`
  also updated.

`packages/domain/src/domain/` is now empty — the workspace `domain/`
tree has been fully migrated. Together with the `domain/__init__.py`
deletions from the sap10_calculator commit (29ac35cc), `domain` is
now a single root-level namespace package with subpackages
{addresses, sap10_calculator, sap10_ml, tasks} + the standalone
`postcode.py` module.

Verified:

- Focused sweep (backend mapper-chain + sap10_calculator worksheet
  e2e + golden fixtures): 99 passed / 19 failed — identical baseline.
- Wider sweep (all sap10_calculator + sap10_ml): 1654 passed / 20
  failed (same pre-existing failures).
- domain/sap10_ml/tests: 210/210 PASSED at new path.
- Pyright net-zero: heat_transmission.py 13, cert_to_inputs.py 35,
  mapper.py 33, rdsap_uvalues.py 1 (all unchanged from baseline).

Note: `packages/domain/pyproject.toml` still declares
`packages = ["src/domain"]` for the hatchling wheel — that target
directory is now empty and the wheel build is effectively a no-op.
Retiring the workspace package or repointing the wheel is a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 13:01:35 +00:00

149 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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