Model/domain/sap10_ml/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

56 lines
2.2 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.

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