mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(ventilation): read Blower Door AP50 pressure test (Summary)
SAP 10.2 §2 (17)-(18): a measured/design air permeability at 50 Pa from a
Blower Door test routes infiltration via `(18) = AP50/20 + (8)`, in
preference to the components-based (16) estimate. The Elmhurst extractor
read only the AP4 ("Pulse") column of §12.2, so a Blower Door result
(§12.2 "Pressure Test Result (AP50)") fell through to the structural-
infiltration default — over-counting ventilation heat loss.
Surfaced by simulated case 44 (AP50 4.50): effective air change rate was
0.81 vs the worksheet's 0.58 (+38% ventilation loss). The cascade already
supports `air_permeability_ap50` (preferred over AP4); this wires the read
end to end (extractor → ElmhurstSiteNotes → SapVentilation → cert_to_inputs).
Pinned against the case-44 P960 §2 at abs=1e-4: (18) infiltration 0.3417
(= 4.5/20 + 0.1167) and (25) Jan effective ach 0.5812. Worksheet harness
stays 47/47 0-raised.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d4d2b222fc
commit
fe3bf4eaed
6 changed files with 37 additions and 0 deletions
|
|
@ -1352,6 +1352,10 @@ class ElmhurstSiteNotesExtractor:
|
||||||
air_permeability_ap4_m3_h_m2 = float(ap4_raw.split()[0])
|
air_permeability_ap4_m3_h_m2 = float(ap4_raw.split()[0])
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
air_permeability_ap4_m3_h_m2 = None
|
air_permeability_ap4_m3_h_m2 = None
|
||||||
|
# SAP 10.2 §2 (17) "Measured/design AP50" from a Blower Door test.
|
||||||
|
# Routes the cascade's (18) via `AP50 / 20 + (8)` (preferred over
|
||||||
|
# AP4). Absent when the test method is "Not available".
|
||||||
|
ap50_raw = self._local_float(pressure_lines, "Pressure Test Result (AP50)")
|
||||||
# Summary §12.1 "Mechanical Ventilation Type" — scoped to §12.1
|
# Summary §12.1 "Mechanical Ventilation Type" — scoped to §12.1
|
||||||
# body so the global "Type" labels in §14 / §15 can't shadow it.
|
# body so the global "Type" labels in §14 / §15 can't shadow it.
|
||||||
mv_lines = self._section_lines(
|
mv_lines = self._section_lines(
|
||||||
|
|
@ -1400,6 +1404,7 @@ class ElmhurstSiteNotesExtractor:
|
||||||
mechanical_ventilation=self._bool_val("Mechanical Ventilation"),
|
mechanical_ventilation=self._bool_val("Mechanical Ventilation"),
|
||||||
pressure_test_method=self._str_val("Test Method"),
|
pressure_test_method=self._str_val("Test Method"),
|
||||||
air_permeability_ap4_m3_h_m2=air_permeability_ap4_m3_h_m2,
|
air_permeability_ap4_m3_h_m2=air_permeability_ap4_m3_h_m2,
|
||||||
|
air_permeability_ap50_m3_h_m2=ap50_raw,
|
||||||
mechanical_ventilation_type=mechanical_ventilation_type,
|
mechanical_ventilation_type=mechanical_ventilation_type,
|
||||||
mechanical_ventilation_pcdf_reference=mev_pcdf_reference,
|
mechanical_ventilation_pcdf_reference=mev_pcdf_reference,
|
||||||
wet_rooms_count=wet_rooms_count,
|
wet_rooms_count=wet_rooms_count,
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,10 @@ class SapVentilation:
|
||||||
# Pulse pressure test, m³/h per m² of envelope area. When present the
|
# Pulse pressure test, m³/h per m² of envelope area. When present the
|
||||||
# cascade routes (18) via the AP4 formula `0.263 × AP4^0.924 + (8)`.
|
# cascade routes (18) via the AP4 formula `0.263 × AP4^0.924 + (8)`.
|
||||||
air_permeability_ap4_m3_h_m2: Optional[float] = None
|
air_permeability_ap4_m3_h_m2: Optional[float] = None
|
||||||
|
# SAP 10.2 §2 (17) — air permeability at 50 Pa from a Blower Door test,
|
||||||
|
# m³/h per m² of envelope area. When present the cascade routes (18)
|
||||||
|
# via `AP50 / 20 + (8)` (preferred over AP4).
|
||||||
|
air_permeability_ap50_m3_h_m2: Optional[float] = None
|
||||||
# SAP 10.2 §2 (23a)/(24a..d) — Elmhurst "Mechanical Ventilation Type"
|
# SAP 10.2 §2 (23a)/(24a..d) — Elmhurst "Mechanical Ventilation Type"
|
||||||
# string mapped to the `MechanicalVentilationKind` enum name (e.g.
|
# string mapped to the `MechanicalVentilationKind` enum name (e.g.
|
||||||
# "EXTRACT_OR_PIV_OUTSIDE" for MEV decentralised). The cascade uses
|
# "EXTRACT_OR_PIV_OUTSIDE" for MEV decentralised). The cascade uses
|
||||||
|
|
|
||||||
|
|
@ -6918,5 +6918,6 @@ def _map_elmhurst_ventilation(
|
||||||
else (False if has_suspended_timber_floor else None)
|
else (False if has_suspended_timber_floor else None)
|
||||||
),
|
),
|
||||||
air_permeability_ap4_m3_h_m2=v.air_permeability_ap4_m3_h_m2,
|
air_permeability_ap4_m3_h_m2=v.air_permeability_ap4_m3_h_m2,
|
||||||
|
air_permeability_ap50_m3_h_m2=v.air_permeability_ap50_m3_h_m2,
|
||||||
mechanical_ventilation_kind=_elmhurst_mv_kind(v.mechanical_ventilation_type),
|
mechanical_ventilation_kind=_elmhurst_mv_kind(v.mechanical_ventilation_type),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,9 @@ class VentilationAndCooling:
|
||||||
# SAP 10.2 §2 (17a) AP4 reading from §12.2 "Pressure Test Result
|
# SAP 10.2 §2 (17a) AP4 reading from §12.2 "Pressure Test Result
|
||||||
# (AP4)" — only present when `pressure_test_method == "Pulse"`.
|
# (AP4)" — only present when `pressure_test_method == "Pulse"`.
|
||||||
air_permeability_ap4_m3_h_m2: Optional[float] = None
|
air_permeability_ap4_m3_h_m2: Optional[float] = None
|
||||||
|
# SAP 10.2 §2 (17) AP50 reading from §12.2 "Pressure Test Result
|
||||||
|
# (AP50)" — present for a Blower Door test. Routes (18) via AP50/20.
|
||||||
|
air_permeability_ap50_m3_h_m2: Optional[float] = None
|
||||||
# Summary §12.1 "Mechanical Ventilation Type" — e.g. "Mechanical
|
# Summary §12.1 "Mechanical Ventilation Type" — e.g. "Mechanical
|
||||||
# extract, decentralised (MEV dc)". None when `mechanical_ventilation
|
# extract, decentralised (MEV dc)". None when `mechanical_ventilation
|
||||||
# is False` (no MV system).
|
# is False` (no MV system).
|
||||||
|
|
|
||||||
|
|
@ -4986,6 +4986,9 @@ def ventilation_from_cert(
|
||||||
# cascade's `(18) = 0.263 × AP4^0.924 + (8)` formula; absent value
|
# cascade's `(18) = 0.263 × AP4^0.924 + (8)` formula; absent value
|
||||||
# falls through to the components-based (16) ach.
|
# falls through to the components-based (16) ach.
|
||||||
ap4 = sv.air_permeability_ap4_m3_h_m2 if sv is not None else None
|
ap4 = sv.air_permeability_ap4_m3_h_m2 if sv is not None else None
|
||||||
|
# SAP 10.2 §2 (17) — AP50 Blower Door reading routes (18) via
|
||||||
|
# `AP50 / 20 + (8)`, preferred over AP4 when both are lodged.
|
||||||
|
ap50 = sv.air_permeability_ap50_m3_h_m2 if sv is not None else None
|
||||||
# SAP 10.2 §2 (23a)/(24a..d) — MV kind dispatch chooses the (25)m
|
# SAP 10.2 §2 (23a)/(24a..d) — MV kind dispatch chooses the (25)m
|
||||||
# effective-ach formula. The Elmhurst mapper translates the lodged
|
# effective-ach formula. The Elmhurst mapper translates the lodged
|
||||||
# "Mechanical Ventilation Type" string to an enum *name*; resolve
|
# "Mechanical Ventilation Type" string to an enum *name*; resolve
|
||||||
|
|
@ -5023,6 +5026,7 @@ def ventilation_from_cert(
|
||||||
window_pct_draught_proofed=float(epc.percent_draughtproofed or 0),
|
window_pct_draught_proofed=float(epc.percent_draughtproofed or 0),
|
||||||
sheltered_sides=int(sv.sheltered_sides) if sv is not None and sv.sheltered_sides is not None else 2,
|
sheltered_sides=int(sv.sheltered_sides) if sv is not None and sv.sheltered_sides is not None else 2,
|
||||||
air_permeability_ap4=ap4,
|
air_permeability_ap4=ap4,
|
||||||
|
air_permeability_ap50=ap50,
|
||||||
mv_kind=mv_kind,
|
mv_kind=mv_kind,
|
||||||
mv_system_ach=mv_system_ach,
|
mv_system_ach=mv_system_ach,
|
||||||
**wind_kwargs,
|
**wind_kwargs,
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,26 @@ def test_case44_non_separated_conservatory_fabric_matches_pdf() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_case44_blower_door_pressure_test_matches_pdf() -> None:
|
||||||
|
"""Simulated case 44 lodges a Blower Door air-pressure test
|
||||||
|
(§12.2 "Pressure Test Result (AP50) 4.50"). SAP 10.2 §2 (17)-(18):
|
||||||
|
the AP50 reading routes infiltration via `(18) = AP50/20 + (8)` =
|
||||||
|
4.5/20 + 0.1167 = 0.3417, in preference to the components-based (16)
|
||||||
|
estimate. The extractor previously read only the AP4 (Pulse) column,
|
||||||
|
so a Blower Door result fell through to the structural-infiltration
|
||||||
|
default (effective ach 0.81 vs the worksheet's 0.58 → ventilation
|
||||||
|
heat loss over-counted by ~38%)."""
|
||||||
|
# Arrange
|
||||||
|
epc = _w001431_case44.build_epc()
|
||||||
|
|
||||||
|
# Act
|
||||||
|
vent = ventilation_from_cert(epc)
|
||||||
|
|
||||||
|
# Assert — (18) infiltration + (25) Jan effective ach, at abs=1e-4.
|
||||||
|
_pin(vent.pressure_test_ach, 0.3417, "§2 (18) case44")
|
||||||
|
_pin(vent.effective_monthly_ach[0], 0.5812, "§2 (25) Jan case44")
|
||||||
|
|
||||||
|
|
||||||
def test_case6_main_2_emitter_and_control_extracted() -> None:
|
def test_case6_main_2_emitter_and_control_extracted() -> None:
|
||||||
"""Simulated case 6's §14.1 Main Heating2 lodges its OWN emitter
|
"""Simulated case 6's §14.1 Main Heating2 lodges its OWN emitter
|
||||||
("Underfloor Heating") and control ("SAP code 2110, ...") — the two
|
("Underfloor Heating") and control ("SAP code 2110, ...") — the two
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue