From f3e3494bf70d76163b4d468bd67b007884daa57c Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 24 Jun 2026 08:50:19 +0000 Subject: [PATCH] =?UTF-8?q?test(worksheet):=20pin=20simulated=20case=2052?= =?UTF-8?q?=20=E2=80=94=20regular=20gas=20boiler=20+=20cylinder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the mapper-driven e2e cascade pin for "simulated case 52" (000565 semi + regular non-combi mains-gas boiler SAP 102 + 160 L foam cylinder heated from the main, no cylinder stat, uninsulated primary pipework, standard tariff). Routes the Summary PDF through extractor + mapper + calculator like the other 000565 / 001431_case* fixtures. This closes the last untested branch of the cylinder/water chain: the SAP 10.2 §4 cylinder storage loss (Table 2/2a/2b lines 51-55) + the Table 3 PRIMARY circuit loss (59, uninsulated pipework + no stat) that combi/immersion fixtures don't reach. All 11 SAP-result fields reconcile to the U985 worksheet EXACTLY with no calculator change — SAP 57.2904 (=57), cost £911.1973, water 3929.7635 kWh — confirming the cylinder-loss derivation is correct. Summary mirrored to the tracked fixtures dir so the test doesn't depend on the unstaged `sap worksheets/` workspace. pyright strict gate not run locally (pyright not installed in this container). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tests/fixtures/Summary_000565_case52.pdf | Bin 0 -> 81285 bytes .../_elmhurst_worksheet_000565_case52.py | 110 ++++++++++++++++++ .../worksheet/test_e2e_elmhurst_sap_score.py | 19 +++ 3 files changed, 129 insertions(+) create mode 100644 backend/documents_parser/tests/fixtures/Summary_000565_case52.pdf create mode 100644 tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_000565_case52.py diff --git a/backend/documents_parser/tests/fixtures/Summary_000565_case52.pdf b/backend/documents_parser/tests/fixtures/Summary_000565_case52.pdf new file mode 100644 index 0000000000000000000000000000000000000000..82d1891f8696c3cafcee7d208ce43d28cde5022c GIT binary patch literal 81285 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

|DM;gGuoG^ooz95w&cm(Yh?R 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 MB8j=V@~ literal 0 HcmV?d00001 diff --git a/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_000565_case52.py b/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_000565_case52.py new file mode 100644 index 00000000..8f368476 --- /dev/null +++ b/tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_000565_case52.py @@ -0,0 +1,110 @@ +"""Mapper-driven cascade pin against the Elmhurst U985-0001-000565 +"simulated case 52" worksheet — a REGULAR (non-combi) mains-gas boiler +feeding a hot-water CYLINDER, on standard tariff. + +Like 000565 / the _rr cases, this fixture does NOT hand-build the +EpcPropertyData: it routes the Summary PDF through +ElmhurstSiteNotesExtractor + from_elmhurst_site_notes so the SAP-result +pin grid exercises the WHOLE extractor + mapper + calculator pipeline. + +This case was hand-built (Khalim) to ground-truth the LAST untested +branch of the cylinder/water-heating chain that the combi/immersion +fixtures don't reach: a regular boiler + cylinder heated from the main, +exercising SAP 10.2 §4 — + - Cylinder storage loss via Table 2 loss factor (51) 0.0181, Table 2a + volume factor (52) 0.9086, Table 2b temperature factor (53) 0.7020 + (NO cylinder thermostat) → (55) 1.8466. + - PRIMARY circuit loss (59) 128.3772 (winter) — the Table 3 path for + UNINSULATED primary pipework + no cylinder stat. Case 50 (immersion, + no boiler) couldn't reach this branch. + - Combi loss (61) correctly 0 (regular boiler, not a combi). +The whole chain reconciles to the U985 worksheet EXACTLY with no +calculator change — it pins the cylinder-loss derivation as correct. + +Cert shape: 000565 semi shell, single main = mains-gas REGULAR boiler +(SAP code 102, control 2106 programmer + room stat + TRVs), water +heating from main (WHC 901) via a 160 L foam-insulated cylinder (no +cylinder stat, uninsulated primary pipework, in heated space), one +instantaneous electric shower, no secondary, no PV, standard tariff. + +Source: user-simulated PDFs at `sap worksheets/golden fixture debugging/ +simulated case 52/`. The Summary is mirrored into the tracked +`backend/documents_parser/tests/fixtures/Summary_000565_case52.pdf` so the +test runs without depending on the unstaged workspace. + +Worksheet pin targets (U985-0001-000565 block 1 — existing dwelling SAP): +- SAP rating 57 (258); continuous 57.2904; ECF 3.0616 (257) +- Total fuel cost £911.1973 (255) +- Total CO2 3834.8434 kg/year (272) +- Space heating 10563.5170 kWh/year ((98c)) +- Main 1 fuel 13371.5405 kWh/year (211) +- Secondary fuel 0.0 kWh/year (215) +- Hot water fuel 3929.7635 kWh/year (219) +- Lighting 435.3204 kWh/year (232) +- Pumps/fans 401.6384 kWh/year (231) + +Per [[feedback-zero-error-strict]] + [[feedback-e2e-validation- +philosophy]]: pins are abs=1e-4 against the worksheet PDF. The pin +values live in `test_e2e_elmhurst_sap_score._FIXTURE_PINS`. +""" + +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 + +# parents[0]=worksheet/, [1]=sap10_calculator/, [2]=domain/, [3]=tests/, +# [4]=repo root. +_SUMMARY_PDF: Final[Path] = ( + Path(__file__).resolve().parents[4] + / "backend" / "documents_parser" / "tests" / "fixtures" + / "Summary_000565_case52.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 the other `_elmhurst_worksheet_*` fixtures. + """ + 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 the simulated case-52 Summary through extractor + mapper. + No hand-built EpcPropertyData — the extractor and mapper are part of + the test target. + """ + 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/tests/domain/sap10_calculator/worksheet/test_e2e_elmhurst_sap_score.py b/tests/domain/sap10_calculator/worksheet/test_e2e_elmhurst_sap_score.py index 1625bcbc..13b1b1bb 100644 --- a/tests/domain/sap10_calculator/worksheet/test_e2e_elmhurst_sap_score.py +++ b/tests/domain/sap10_calculator/worksheet/test_e2e_elmhurst_sap_score.py @@ -48,6 +48,7 @@ from tests.domain.sap10_calculator.worksheet import ( _elmhurst_worksheet_001431_case6 as _w001431_case6, _elmhurst_worksheet_001431_case7 as _w001431_case7, _elmhurst_worksheet_001431_case20 as _w001431_case20, + _elmhurst_worksheet_000565_case52 as _w000565_case52, ) from tests.domain.sap10_calculator.worksheet._elmhurst_fixtures import ( ALL_FIXTURES as _ELMHURST_FIXTURES, @@ -296,6 +297,23 @@ _FIXTURE_PINS: Final[dict[str, FixtureCascadePins]] = { lighting_kwh_per_yr=246.3083, pumps_fans_kwh_per_yr=0.0, ), + # Mapper-driven — Summary_000565_case52.pdf → extractor → mapper → + # calculator. Regular (non-combi) mains-gas boiler (SAP 102) + a + # 160 L foam cylinder heated from the main (WHC 901), no cylinder + # stat + uninsulated primary pipework, standard tariff. Validates the + # cylinder storage loss (51-55) + PRIMARY loss (59) chain — the + # branch immersion/combi fixtures can't reach. Reconciles to the + # worksheet EXACTLY with no calculator change. + "000565_case52": FixtureCascadePins( + sap_score=57, sap_score_continuous=57.2904, ecf=3.0616, + total_fuel_cost_gbp=911.1973, co2_kg_per_yr=3834.8434, + space_heating_kwh_per_yr=10563.5170, + main_heating_fuel_kwh_per_yr=13371.5405, + secondary_heating_fuel_kwh_per_yr=0.0, + hot_water_kwh_per_yr=3929.7635, + lighting_kwh_per_yr=435.3204, + pumps_fans_kwh_per_yr=401.6384, + ), } @@ -315,6 +333,7 @@ _FIXTURE_MODULES: Final[dict[str, ModuleType]] = { "001431_case6": _w001431_case6, "001431_case7": _w001431_case7, "001431_case20": _w001431_case20, + "000565_case52": _w000565_case52, }