From e51fcb74ca71486bc67854e56dac91a68e03673e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 22:03:52 +0000 Subject: [PATCH] Slice S0380.52: cert 000565 Elmhurst-only mapper-driven cascade pin + glazing-label coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User pivot at end of prior session: don't hand-build EpcPropertyData fixtures — route Summary PDFs through `EpcPropertyDataMapper.from_ elmhurst_site_notes` so the pin grid exercises extractor + mapper + calculator, and each new Elmhurst doc grows mapper coverage instead of bespoke fixture code. New fixture cert 000565 is a stress-test cert (5 building parts, age mix A→J, conservatory with heaters, curtain wall, basement walls, mixed party-wall constructions) that surfaces many uncommon cascade paths absent from the cohort-2 + ASHP corpus. Mapper coverage extended for 3 Elmhurst §11 glazing labels surfaced on this cert (per RdSAP-Schema-21.0.1, `datatypes/epc/domain/ epc_codes.csv` glazed_type rows): "Triple between 2002 and 2021": 9 (RdSAP-21 schema row 9 — triple glazing, installed 2002-2022 in EAW; `_G_PERPENDICULAR_BY_ GLAZING_TYPE[9] = 0.68`, `_G_LIGHT_BY_GLAZING_CODE[9] = 0.70`) "Single glazing": 1 (alias of bare "Single"; cascade g_L = 0.90, g⊥ = 0.85 per SAP 10.2 Table 6b) "Double glazing, known data": 3 (Elmhurst lodgement of RdSAP-21 schema row 7 "double, known data"; manufacturer U-value and g-value lodged via WindowTransmissionDetails override the cascade's defaults — grouped under code 3 with other unknown- date DG variants for cascade-equivalence on g_L/g⊥) Per [[feedback-e2e-validation-philosophy]] + [[feedback-zero-error- strict]]: pin tolerances are abs=1e-4 against U985-0001-000565.pdf Block 1 line refs (pinned: SAP int + SAP continuous + ECF + total fuel cost + CO2 + space heating + main 1 fuel + secondary fuel + hot water + lighting + pumps/fans). Outcome: 1/11 pin green (`secondary_heating_fuel_kwh_per_yr = 0`); 10 pins are now named calculator-gap residuals to fix in subsequent slices: main_heating_fuel_kwh_per_yr +27,665.01 kWh/yr (heat-pump SAP code 224 + gas combi via WHC 914 "from second main"; cascade probably runs ASHP for DHW instead of routing through gas combi) hot_water_kwh_per_yr +164.88 kWh/yr (FGHRS / solar HW / Table 3a no-keep-hot for the gas combi DHW path) lighting_kwh_per_yr -236.19 kWh/yr (RdSAP §12-1 bulb- count cascade; 27 total / 7 low-energy / 20 incandescent lodged) pumps_fans_kwh_per_yr -122.52 kWh/yr (cascade defaults to 130; expected 252.52 = MEV PCDF 500755 + flue + solar pump) Cohort regression check: 472 pass + 10 expected 000565 failures. Pyright net-zero (32 errors before, 32 after). Co-Authored-By: Claude Opus 4.7 --- .../tests/fixtures/Summary_000565.pdf | Bin 0 -> 125870 bytes datatypes/epc/domain/mapper.py | 20 +++ .../tests/_elmhurst_worksheet_000565.py | 126 ++++++++++++++++++ .../tests/test_e2e_elmhurst_sap_score.py | 16 +++ 4 files changed, 162 insertions(+) create mode 100644 backend/documents_parser/tests/fixtures/Summary_000565.pdf create mode 100644 domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000565.py diff --git a/backend/documents_parser/tests/fixtures/Summary_000565.pdf b/backend/documents_parser/tests/fixtures/Summary_000565.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8d31885babaced0d2a5de33e9250941f4864e135 GIT binary patch literal 125870 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 Xdp$e*zrHdc6U)oSA(E4e$cp|ShxY4e literal 0 HcmV?d00001 diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 348d27b9..1e5cec98 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3724,10 +3724,23 @@ def _elmhurst_cylinder_insulation_code( # fixing the upstream extractor is deferred to a future slice. _ELMHURST_GLAZING_LABEL_TO_SAP10: Dict[str, int] = { "Single": 1, + # Elmhurst §11 lodgement variant of the bare "Single" form — surfaced + # on cert 000565 Window 3 (Wood frame, U=3.35, g=0.85). Same enum as + # "Single" per Table U2 code 1; g_L=0.90 / g⊥=0.85. + "Single glazing": 1, "Double pre 2002": 2, "Double between 2002 and 2021": 3, "Double with unknown install date": 3, "Double with unknown 16 mm or install date more": 3, + # Elmhurst §11 lodgement of RdSAP-21 schema row 7 "double, known + # data" — manufacturer U-value and g-value are lodged on the + # SapWindow's `WindowTransmissionDetails` so the cascade reads + # those directly. The glazing_type code only affects the §5 + # (66)..(67) daylight factor where g_L=0.80 across all DG variants + # ({2, 3, 13}) — grouped under code 3 with the other unknown-date + # DG variants for cascade-equivalence. First seen on cert 000565 + # Window 6 (Main, U=2.00, g=0.72). + "Double glazing, known data": 3, "Double post or during 2022": 5, "Triple post or during 2022": 6, # One window in cert 2636 (Summary_000898.pdf) lodges the year- @@ -3737,6 +3750,13 @@ _ELMHURST_GLAZING_LABEL_TO_SAP10: Dict[str, int] = { # Treated as the same enum as the full form per worksheet # "Triple glazed" lodging on cert 2636's dr87-0001-000898.pdf. "Triple post or during": 6, + # RdSAP-Schema-21 row "triple glazing, installed 2002-2022 in EAW" + # (epc_codes.csv code 9 — RdSAP-21 schema extension). First seen on + # cert 000565 Window 2 (Summary_000565.pdf §11 row 2, manufacturer + # U=2.00, g=0.72). Cascade's `_G_PERPENDICULAR_BY_GLAZING_TYPE` + # row 9 returns Table 6b triple-glazed g⊥=0.68; the lodged + # solar_transmittance=0.72 overrides per worksheet-pinned value. + "Triple between 2002 and 2021": 9, "Secondary": 7, } diff --git a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000565.py b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000565.py new file mode 100644 index 00000000..774b148c --- /dev/null +++ b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000565.py @@ -0,0 +1,126 @@ +"""Mapper-driven cascade pin against Elmhurst U985-0001-000565. + +Unlike the 6 cohort fixtures (000474/000477/000480/000487/000490/ +000516), this fixture does NOT hand-build the EpcPropertyData. It +routes the Summary_000565.pdf through ElmhurstSiteNotesExtractor + +EpcPropertyDataMapper.from_elmhurst_site_notes so the SAP-result pin +grid exercises the WHOLE extractor + mapper + calculator pipeline. + +Failing SAP-result pins surface gaps in any of the three layers: +- Extractor: lodgement fields not parsed from the Summary PDF +- Mapper: code-to-int translations missing from the dispatch dicts +- Calculator: cascade gaps (e.g. CF cavity-filled party-wall U=0.20 + from Table 15 row 3 has no SAP10 wall_construction code today) + +Each failing pin localises to one of the three and becomes its own +slice. As more Elmhurst Summary PDFs land, the mapper will handle +them automatically rather than per-cert hand-building. + +Source: PDF supplied by user 2026-05-28 at `sap worksheets/extended +test case/`; mirrored into the tracked +`backend/documents_parser/tests/fixtures/Summary_000565.pdf` so the +test runs without depending on the unstaged user workspace. + +Cert shape (Summary §1-19): House, Enclosed End-Terrace, 4 heated +storeys, TFA 319.91 m², 5 building parts (Main + 4 extensions). Age +mix A→J. Heat pump SAP code 224 + gas combi (PCDB 15100 Vaillant +Ecotec plus 415) providing DHW only via water_heating_code 914 +("from second main system"). Solar HW (3 m² flat-panel, W, +30° elevation), FGHRS (Zenex SuperFlow index 60063), MEV +decentralised (PCDF 500755). Conservatory thermally separated WITH +fixed heaters. Curtain Wall Post-2023 (Ext2), basement walls +(Ext3+Ext4), CF cavity-filled party wall (Ext1), CU cavity-unfilled +party wall (Main). RR on every part with mixed age bands. + +Worksheet pin targets (U985-0001-000565.pdf, Block 1 — energy rating): +- SAP value 28.5087 (line 257) → SAP rating 29 (line 258) +- Energy cost factor 5.3866 (line 257) +- Total fuel cost £4680.2593 (line 255, Table 12 prices) +- CO2 6447.6263 kg/year (line 272) +- Space heating 59008.3499 kWh/year (line 98c) +- Main 1 fuel 34710.7941 kWh/year (line 211) — ASHP electricity +- Secondary fuel 0.0 (line 215) +- Hot water fuel 3755.0288 kWh/year (line 219) — gas combi via WHC 914 +- Lighting 1384.8353 kWh/year (line 232) +- Pumps/fans 252.5159 kWh/year (line 231) — MEV 127.5 + flue 45 + solar 80 + +Per [[feedback-zero-error-strict]] + [[feedback-e2e-validation- +philosophy]]: pins are abs=1e-4 against the worksheet PDF. Failing +pins are named extractor / mapper / calculator gaps to fix. +""" + +from __future__ import annotations + +import re +import subprocess +from pathlib import Path +from typing import Final + +from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor +from datatypes.epc.domain.epc_property_data import EpcPropertyData +from datatypes.epc.domain.mapper import EpcPropertyDataMapper + + +# Repo root → backend fixtures. parents[0]=tests/, parents[1]=worksheet/, +# parents[2]=sap10_calculator/, parents[3]=domain/, parents[4]=repo root. +_SUMMARY_PDF: Final[Path] = ( + Path(__file__).resolve().parents[4] + / "backend" / "documents_parser" / "tests" / "fixtures" + / "Summary_000565.pdf" +) + + +def _summary_pdf_to_textract_style_pages(pdf_path: Path) -> list[str]: + """Convert a Summary PDF into the per-page text format the + ElmhurstSiteNotesExtractor expects (label\\nvalue sequences). + + Mirror of the helper in `backend/documents_parser/tests/ + test_summary_pdf_mapper_chain.py::_summary_pdf_to_textract_style_ + pages`. Duplicated here rather than imported across the test/ + fixture boundary; the canonical version lives next to its callers + and this fixture module is the only e2e harness consumer. + + `pdftotext -layout` preserves the spatial pairing of label and + value on each line; we split each line on 2+ spaces to surface + the label/value tokens, then concatenate them back into a single + newline-delimited stream per page. + """ + info = subprocess.run( + ["pdfinfo", str(pdf_path)], capture_output=True, text=True, check=True, + ).stdout + m = re.search(r"Pages:\s+(\d+)", info) + if m is None: + raise RuntimeError(f"Could not parse page count from {pdf_path}") + page_count = int(m.group(1)) + + pages: list[str] = [] + for i in range(1, page_count + 1): + layout = subprocess.run( + [ + "pdftotext", "-layout", "-f", str(i), "-l", str(i), + str(pdf_path), "-", + ], + capture_output=True, text=True, check=True, + ).stdout + tokens: list[str] = [] + for line in layout.splitlines(): + if not line.strip(): + tokens.append("") + continue + parts = [p for p in re.split(r"\s{2,}", line.strip()) if p] + tokens.extend(parts) + pages.append("\n".join(tokens)) + return pages + + +def build_epc() -> EpcPropertyData: + """Route Summary_000565.pdf through extractor + mapper. + + No hand-built EpcPropertyData. The Elmhurst extractor and the + mapper are part of the test target — failing SAP-result pins + surface gaps in any of the three layers (extractor, mapper, + calculator). Each gap becomes its own follow-up slice. + """ + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_PDF) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + return EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) diff --git a/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py b/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py index 514c9c41..514c438b 100644 --- a/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py +++ b/domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py @@ -33,6 +33,7 @@ from domain.sap10_calculator.worksheet.tests import ( _elmhurst_worksheet_000487 as _w000487, _elmhurst_worksheet_000490 as _w000490, _elmhurst_worksheet_000516 as _w000516, + _elmhurst_worksheet_000565 as _w000565, ) from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ( ALL_FIXTURES as _ELMHURST_FIXTURES, @@ -129,6 +130,20 @@ _FIXTURE_PINS: Final[dict[str, FixtureCascadePins]] = { lighting_kwh_per_yr=230.8853, pumps_fans_kwh_per_yr=160.0, ), + # Mapper-driven cohort entry — Summary_000565.pdf → extractor → + # mapper → calculator. 5 BPs, heat pump + gas combi DHW via WHC 914, + # solar HW, FGHRS, conservatory with heaters, curtain wall, basement + # walls. Pins are worksheet PDF Block 1 (energy-rating) line refs. + "000565": FixtureCascadePins( + sap_score=29, sap_score_continuous=28.5087, ecf=5.3866, + total_fuel_cost_gbp=4680.2593, co2_kg_per_yr=6447.6263, + space_heating_kwh_per_yr=59008.3499, + main_heating_fuel_kwh_per_yr=34710.7941, + secondary_heating_fuel_kwh_per_yr=0.0, + hot_water_kwh_per_yr=3755.0288, + lighting_kwh_per_yr=1384.8353, + pumps_fans_kwh_per_yr=252.5159, + ), } @@ -139,6 +154,7 @@ _FIXTURE_MODULES: Final[dict[str, ModuleType]] = { "000487": _w000487, "000490": _w000490, "000516": _w000516, + "000565": _w000565, }