mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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>
149 lines
4.4 KiB
Python
149 lines
4.4 KiB
Python
"""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)
|