diff --git a/backend/documents_parser/elmhurst_extractor.py b/backend/documents_parser/elmhurst_extractor.py index 12f4d3de..ed35bef1 100644 --- a/backend/documents_parser/elmhurst_extractor.py +++ b/backend/documents_parser/elmhurst_extractor.py @@ -523,7 +523,10 @@ class ElmhurstSiteNotesExtractor: insulation = t elif t in ("Mineral or EPS", "PUR", "PIR"): insulation_type = t - elif t in ("Party", "Sheltered", "Connected to heated space"): + elif t in ( + "Party", "Sheltered", "Exposed", + "Connected", "Connected to heated space", + ): gable_type = t return RoomInRoofSurface( name=name, 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 f4408fa3..2f371559 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -509,6 +509,79 @@ def test_summary_9501_rr_gable_walls_route_to_external_walls_hlc() -> None: assert abs(ht.walls_w_per_k - worksheet_walls_w_per_k) <= 1e-2 +def test_summary_000565_extractor_recognises_exposed_and_connected_gable_types() -> None: + """Summary PDF §8.1 Room(s) in Roof per-surface table lists the + gable-wall environment column with one of four published values: + + Party → §8.1 party-wall row + Sheltered → §8.1 sheltered external row + Exposed → §8.1 exposed external row + Connected (to heated space) → §8.1 internal partition + + Per RdSAP 10 §3.10 (PDF p.30-35) "Detailed Room-in-Roof" + Table 4 + (p.22) "Heat-loss surface variants": + + - Exposed gable wall → external wall at the lodged U-value (or + the BP main-wall U when the lodged value is the default) + - Sheltered gable wall → external wall at the lodged U-value + - Party gable wall → party wall at U=0.25 (Table 4 row 2) + - Connected gable wall → internal partition to heated space, + NOT a heat-loss surface (drops from external + party totals) + + The extractor was only capturing `gable_type ∈ {"Party", + "Sheltered", "Connected to heated space"}` — neither `"Exposed"` + (every external gable on cert 000565) nor the plain `"Connected"` + string (the actual lodging used in Summary PDFs vs the verbose + "Connected to heated space") was recognised. Both fell through + with `gable_type=None`, masking the downstream cascade gap (cert + 000565 BP[0] Main Gable Wall 1 is lodged "Exposed" at U=0.35 but + extracted as untyped → mapper routes to `gable_wall` (party at + U=0.25) — see worksheet "Roof room Main Gable Wall 1" line at + U=0.35). + + This pin asserts the extractor surfaces the lodged environment + column verbatim. The downstream mapper + cascade behaviour stays + unchanged until follow-up slices use the new field — this is a + pure extractor data-completion step (no test pins move). + """ + # Arrange + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000565_PDF) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + + # Act — Main BP gables; Ext1/Ext2 gables expose both "Connected" + # and "Exposed" values from the cert lodging. + rir_main = site_notes.room_in_roof + main_surfaces = {s.name: s for s in (rir_main.surfaces if rir_main else [])} + rir_ext1 = ( + site_notes.extensions[0].room_in_roof + if site_notes.extensions and len(site_notes.extensions) > 0 + else None + ) + ext1_surfaces = {s.name: s for s in (rir_ext1.surfaces if rir_ext1 else [])} + + # Assert + # Main BP[0]: Gable Wall 1 lodged "Exposed" (default U 0.35); Gable + # Wall 2 lodged "Sheltered" (default U 0.30). + assert main_surfaces["Gable Wall 1"].gable_type == "Exposed", ( + f"Main Gable Wall 1 gable_type = " + f"{main_surfaces['Gable Wall 1'].gable_type!r}; expected 'Exposed'" + ) + assert main_surfaces["Gable Wall 2"].gable_type == "Sheltered", ( + f"Main Gable Wall 2 gable_type = " + f"{main_surfaces['Gable Wall 2'].gable_type!r}; expected 'Sheltered'" + ) + # Ext1 BP[1]: Gable Wall 1 lodged "Connected" (internal partition); + # Gable Wall 2 lodged "Exposed" (default U 1.70). + assert ext1_surfaces["Gable Wall 1"].gable_type == "Connected", ( + f"Ext1 Gable Wall 1 gable_type = " + f"{ext1_surfaces['Gable Wall 1'].gable_type!r}; expected 'Connected'" + ) + assert ext1_surfaces["Gable Wall 2"].gable_type == "Exposed", ( + f"Ext1 Gable Wall 2 gable_type = " + f"{ext1_surfaces['Gable Wall 2'].gable_type!r}; expected 'Exposed'" + ) + + def test_summary_9501_pv_array_surfaced_from_elmhurst_section_19() -> None: # Arrange — cert 9501's Elmhurst §19.0 PV section lodges measured # array detail (2.36 kWp, South-West orientation, 45° elevation, diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 82a6450b..d0fedb63 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3277,12 +3277,19 @@ def _map_elmhurst_rir_surface( if kind == "gable_wall" and surface.gable_type == "Sheltered": kind = "gable_wall_external" u_value_override = surface.default_u_value - elif kind == "gable_wall" and surface.gable_type is None and is_flat: + elif ( + kind == "gable_wall" + and surface.gable_type in (None, "Exposed") + and is_flat + ): # Flat with RR: gables are external by default (top of block, # no neighbour above). Lodge as gable_wall_external with no # u_value override so the cascade falls through to the main- # wall U (`uw` in heat_transmission.py:674) — matches cert # 9501's worksheet treatment of both gable walls at U=1.7. + # Per Summary PDF schema the gable env column reads "Exposed" + # for the same case the legacy heuristic detected via None; + # both lodging shapes route here. kind = "gable_wall_external" area_m2 = _round_half_up_2dp(surface.length_m, surface.height_m) if kind in ("gable_wall", "gable_wall_external"):