diff --git a/backend/documents_parser/elmhurst_extractor.py b/backend/documents_parser/elmhurst_extractor.py index 6f7e4936..196291b4 100644 --- a/backend/documents_parser/elmhurst_extractor.py +++ b/backend/documents_parser/elmhurst_extractor.py @@ -1352,6 +1352,10 @@ class ElmhurstSiteNotesExtractor: air_permeability_ap4_m3_h_m2 = float(ap4_raw.split()[0]) except (ValueError, IndexError): 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 # body so the global "Type" labels in §14 / §15 can't shadow it. mv_lines = self._section_lines( @@ -1400,6 +1404,7 @@ class ElmhurstSiteNotesExtractor: mechanical_ventilation=self._bool_val("Mechanical Ventilation"), pressure_test_method=self._str_val("Test Method"), 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_pcdf_reference=mev_pcdf_reference, wet_rooms_count=wet_rooms_count, diff --git a/datatypes/epc/domain/epc_property_data.py b/datatypes/epc/domain/epc_property_data.py index c12d87f0..f69ede90 100644 --- a/datatypes/epc/domain/epc_property_data.py +++ b/datatypes/epc/domain/epc_property_data.py @@ -202,6 +202,10 @@ class SapVentilation: # 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)`. 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" # string mapped to the `MechanicalVentilationKind` enum name (e.g. # "EXTRACT_OR_PIV_OUTSIDE" for MEV decentralised). The cascade uses diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index acd24efd..514803ec 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -6918,5 +6918,6 @@ def _map_elmhurst_ventilation( else (False if has_suspended_timber_floor else None) ), 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), ) diff --git a/datatypes/epc/surveys/elmhurst_site_notes.py b/datatypes/epc/surveys/elmhurst_site_notes.py index 02f27849..3b179253 100644 --- a/datatypes/epc/surveys/elmhurst_site_notes.py +++ b/datatypes/epc/surveys/elmhurst_site_notes.py @@ -202,6 +202,9 @@ class VentilationAndCooling: # SAP 10.2 §2 (17a) AP4 reading from §12.2 "Pressure Test Result # (AP4)" — only present when `pressure_test_method == "Pulse"`. 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 # extract, decentralised (MEV dc)". None when `mechanical_ventilation # is False` (no MV system). diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index f4a86295..c8a59fcf 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -4986,6 +4986,9 @@ def ventilation_from_cert( # cascade's `(18) = 0.263 × AP4^0.924 + (8)` formula; absent value # falls through to the components-based (16) ach. 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 # effective-ach formula. The Elmhurst mapper translates the lodged # "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), 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_ap50=ap50, mv_kind=mv_kind, mv_system_ach=mv_system_ach, **wind_kwargs, diff --git a/tests/domain/sap10_calculator/worksheet/test_section_cascade_pins.py b/tests/domain/sap10_calculator/worksheet/test_section_cascade_pins.py index d4725df9..19b9d7dc 100644 --- a/tests/domain/sap10_calculator/worksheet/test_section_cascade_pins.py +++ b/tests/domain/sap10_calculator/worksheet/test_section_cascade_pins.py @@ -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: """Simulated case 6's §14.1 Main Heating2 lodges its OWN emitter ("Underfloor Heating") and control ("SAP code 2110, ...") — the two