From 07ed871f7b66e42a6ff7ba8202683cb3f378bb8f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sun, 24 May 2026 22:36:22 +0000 Subject: [PATCH] Slice 56: Elmhurst floor exposed to external air routes through u_exposed_floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_is_floor_exposed_to_unheated_space` previously only matched "U Above unheated space" (semi-exposed floor over a porch / car-park). Cert 001479 Ext2 §9 lodges "Location: E To external air" — a 1.92 m² cantilevered exposed timber floor (the upper-storey extension hanging out over the garden). The worksheet's §3 `Exposed floor Ext2 … 1.92, 1.20, 1.20` pins this surface as U=1.20 via Table 20. Pre-slice the mapper missed the "external air" lodgement entirely; `is_exposed_floor=False` routed Ext2's ground SapFloorDimension through the BS EN ISO 13370 ground-floor cascade (default U≈0.5), mis-modelling a fully-exposed cantilever as a slab on soil. Both lodgement strings ("above unheated", "external air") now trigger the Table 20 path. Function docstring updated; name kept to minimise the diff (refactor candidate for a future slice). Cohort 6 certs all still green at 1e-4 (none lodge external-air floors); cert 001479 cascade SAP 61.90 → 61.93 (+0.03), modest upward move toward the 69.0094 target. Co-Authored-By: Claude Opus 4.7 --- .../tests/test_summary_pdf_mapper_chain.py | 20 +++++++++++++++++++ datatypes/epc/domain/mapper.py | 16 ++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py index 2e74aa30..ceff10a8 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -286,3 +286,23 @@ def test_summary_001479_main_party_wall_construction_is_cavity_unfilled() -> Non # Assert assert epc.sap_building_parts[0].party_wall_construction == 4 + + +def test_summary_001479_ext2_floor_is_exposed_to_external_air() -> None: + # Arrange — cert 001479 Ext2 §9 lodges "Location: E To external air" + # — a cantilevered exposed timber floor (the upper-storey extension + # over the back garden). The worksheet's §3 row `Exposed floor Ext2 + # … 1.92, 1.20, 1.20` pins this as U=1.20 via Table 20. Pre-slice the + # mapper only routed "U Above unheated space" through `is_exposed_ + # floor=True`; "E To external air" fell through to the BS EN ISO + # 13370 ground-floor cascade, dropping the lodged exposure entirely. + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_001479_PDF) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + + # Act + epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) + + # Assert + ext2 = epc.sap_building_parts[2] + assert ext2.floor_type == "To external air" + assert ext2.sap_floor_dimensions[0].is_exposed_floor is True diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 07a461e7..a9c7f94f 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -1880,11 +1880,17 @@ _UPPER_FLOOR_HEIGHT_ADD_M: float = 0.25 def _is_floor_exposed_to_unheated_space(location: Optional[str]) -> bool: - """True when the floor sits above an unheated space (lodged by the - Elmhurst surveyor as 'U Above unheated space'). The cascade routes - these through `u_exposed_floor` rather than the BS EN ISO 13370 - ground-floor formula.""" - return location is not None and "above unheated" in location.lower() + """True when the floor sits above an unheated space OR is exposed + directly to external air. Both lodgements route through + `u_exposed_floor` (Table 20) rather than the BS EN ISO 13370 ground- + floor formula. Elmhurst lodges either 'U Above unheated space' + (extension over a porch / car-park) or 'E To external air' (cantile- + vered floor of an upper-storey extension, first seen on cert 001479 + Ext2 — a 1.92 m² exposed timber floor at U=1.20).""" + if location is None: + return False + lower = location.lower() + return "above unheated" in lower or "external air" in lower def _extract_age_band(age_range: str) -> str: