Dispatch PasHub mechanical ventilation into the §2 ACH cascade 🟩

The surveyed "Ventilation type" label never reached
mechanical_ventilation_kind, so ventilation_from_cert silently defaulted every
mechanical system to NATURAL (24d), dropping its ACH heat loss — 76/205
cohort fixtures lodge dMEV/MEV/PIV (issue #1590 bug 4). Mapping mirrors the
gov-API _API_MECHANICAL_VENTILATION_TO_KIND table (PIV-from-loft is
"as natural"). Harness: within-0.5 12.2% -> 18.5%, MAE 2.538 -> 1.730 — the
campaign's largest single move; floors ratcheted to 0.18 / 1.75.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-14 21:45:24 +00:00
parent 5220b51961
commit 090e16ddc2
2 changed files with 39 additions and 2 deletions

View file

@ -80,7 +80,11 @@ _MANIFEST_PATH = _FIXTURES_DIR / "manifest.json"
# Both are tracked for verification (pre_sap correctness vs a deeper modelling
# gap) — see docs/HANDOVER_PASHUB_838_PROBLEM_PROPERTIES.md. Re-baselined to the
# full-coverage aggregate; ratchet back DOWN as those two are resolved.
_MIN_WITHIN_HALF: float = 0.12
# Ratcheted 0.12 -> 0.18 / 2.55 -> 1.75 by #1590 bug 4 (mechanical ventilation
# dispatched into the §2 ACH cascade instead of defaulting to NATURAL): 76/205
# cohort fixtures lodge a mechanical system; observed 18.5% within-0.5,
# MAE 1.730 — the campaign's largest single move.
_MIN_WITHIN_HALF: float = 0.18
# Ratcheted 2.71 -> 2.64 by #1590 bug 2 (roof "Insulation At: None" -> explicit
# zero thickness): observed MAE 2.632 — the two ~7-SAP roof over-raters
# (507665533138 / 507670893756) correcting is exactly 14/205 ≈ 0.07 of MAE.
@ -89,7 +93,7 @@ _MIN_WITHIN_HALF: float = 0.12
# under-rate closing.
# Ratcheted 2.58 -> 2.55 by #1590 bug 3 (PV Connection label -> gov-API int:
# "Not connected" no longer silently credited): observed MAE 2.538.
_MAX_SAP_MAE: float = 2.55
_MAX_SAP_MAE: float = 1.75
_KNOWN_GAP_REASON = (
"pashub `from_site_notes` mapper does not int-code a main-heating "

View file

@ -6142,9 +6142,42 @@ def _map_sap_heating(
)
# PasHub surveyed "Ventilation type" label → `MechanicalVentilationKind` enum
# name for the §2 cascade dispatch, mirroring the gov-API table
# `_API_MECHANICAL_VENTILATION_TO_KIND` (dMEV=3 / MEV=2 / PIV-outside=6 →
# EXTRACT_OR_PIV_OUTSIDE; PIV-from-loft=5 is "as natural" → None). Left None,
# `ventilation_from_cert` silently defaults a mechanical system to NATURAL
# (24d), dropping its ACH heat loss (issue #1590 bug 4).
_PASHUB_VENTILATION_TYPE_TO_KIND: Dict[str, Optional[str]] = {
"Natural": None,
"Mechanical Extract - Decentralised": "EXTRACT_OR_PIV_OUTSIDE",
"Mechanical Extract - Centralised": "EXTRACT_OR_PIV_OUTSIDE",
"Positive Input - From Outside": "EXTRACT_OR_PIV_OUTSIDE",
"Positive Input - From Loft (As Natural)": None,
}
def _pashub_mechanical_ventilation_kind(
ventilation_type: Optional[str],
) -> Optional[str]:
"""Resolve a PasHub surveyed Ventilation type label to the
`MechanicalVentilationKind` enum name at the mapper boundary (ADR-0015).
Blank stays None (natural); a non-empty label the lookup does not cover
strict-raises `UnmappedPasHubLabel` so the gap is fixed here, never
silently under-stating ventilation heat loss downstream."""
if not ventilation_type:
return None
if ventilation_type not in _PASHUB_VENTILATION_TYPE_TO_KIND:
raise UnmappedPasHubLabel("ventilation type", ventilation_type)
return _PASHUB_VENTILATION_TYPE_TO_KIND[ventilation_type]
def _map_sap_ventilation(ventilation: Ventilation) -> SapVentilation:
return SapVentilation(
ventilation_type=ventilation.ventilation_type,
mechanical_ventilation_kind=_pashub_mechanical_ventilation_kind(
ventilation.ventilation_type
),
draught_lobby=ventilation.draught_lobby,
pressure_test=ventilation.pressure_test,
open_flues_count=ventilation.number_of_open_flues,