From fe59c4d8a2ee7f03ca53411deb7e1a4963442160 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 3 Jun 2026 17:57:22 +0000 Subject: [PATCH] =?UTF-8?q?S0380.208:=20case=207=20combi=20e2e=20fixture?= =?UTF-8?q?=20=E2=80=94=20condensing-oil-combi=20path=20validated=20exact?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds simulated case 7: case 6 (P960-0001-001431) with the heating swapped to a CONDENSING OIL COMBI (SAP code 130, Table 4b 82/73) and the cylinder removed — combi instantaneous DHW (WHC 901), Table 3a keep-hot combi loss (61) = 600 kWh/yr, no primary/storage loss, boiler interlock PRESENT (no −5pp). This is the heating archetype golden cert 0240-0200-5706-2365-8010 uses, which case 6 (SAP code 127, a *regular* condensing oil boiler + cylinder) never exercised. The cascade reproduces the case-7 worksheet EXACTLY at abs=1e-4 on every top-level SapResult output with ZERO calculator changes: (211) 7865.4304 (213) 7556.9821 (219) 3496.8121 (98c) 12646.3783 (255) 1123.3372 (257) 1.9631 (272) 5738.9315 (258) 73 This validates the SAP 10.2 Appendix D Eq D1 combi efficiency blend + Table 3a keep-hot combi loss + Table 4b code 130 (82/73) path, and exonerates the combi mechanism as the source of 0240's API-path residual — which therefore lives in 0240's fabric/demand or the API mapper. Test-only slice (no impl change). New fixture file: 0 pyright errors. Co-Authored-By: Claude Opus 4.8 --- .../tests/fixtures/Summary_001431_case7.pdf | Bin 0 -> 93369 bytes .../_elmhurst_worksheet_001431_case7.py | 134 ++++++++++++++++++ .../worksheet/test_e2e_elmhurst_sap_score.py | 20 +++ 3 files changed, 154 insertions(+) create mode 100644 backend/documents_parser/tests/fixtures/Summary_001431_case7.pdf create mode 100644 tests/domain/sap10_calculator/worksheet/_elmhurst_worksheet_001431_case7.py diff --git a/backend/documents_parser/tests/fixtures/Summary_001431_case7.pdf b/backend/documents_parser/tests/fixtures/Summary_001431_case7.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8c42d24a7ec7231d29a66c7152f7edbbaa825720 GIT binary patch literal 93369 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�zIBcu5uj-lCFwFix3P;?6jIYu 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 100 L every month so the "no keep-hot" + fu-scaling collapses to 1.0). +- (59)m primary loss = 0 and storage loss = 0 (combi, no cylinder). +- (211) space-heating fuel main 1 = 7865.4304. +- (213) space-heating fuel main 2 = 7556.9821. +- (219) water-heating fuel = 3496.8121. +- (64) HW demand total = 2712.0619 (smaller dwelling than 0240's + 2842.82 — case 7 validates the combi *mechanism*, not 0240's absolute + demand). + +Per [[feedback-zero-error-strict]]: e2e pins are abs=1e-4 against the PDF +(see test_e2e_elmhurst_sap_score.py::_FIXTURE_PINS["001431_case7"]). + +Source: user-simulated PDFs at `sap worksheets/golden fixture +debugging/simulated case 7/`. Summary mirrored into the tracked +`backend/documents_parser/tests/fixtures/Summary_001431_case7.pdf`. +""" + +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_001431_case7.pdf" +) + +# Worksheet (211)/(213) per-system space-heating fuel (kWh/yr). Both mains +# are condensing oil combis (SAP code 130, Table 4b 82/73) at base +# efficiency — interlock present (combi + room thermostat, no cylinder), +# so NO −5pp penalty (the case-6 boiler+cylinder had no cylinder stat → a +# −5pp penalty; the combi removes it). +LINE_211_MAIN_1_FUEL_KWH: Final[float] = 7865.4304 +LINE_213_MAIN_2_FUEL_KWH: Final[float] = 7556.9821 + +# Worksheet (219) water-heating fuel (kWh/yr). Combi instantaneous DHW +# (WHC 901) — SAP 10.2 Appendix D Eq D1 blends the monthly water-heater +# efficiency (217)m by the DHW boiler's (204) space share; Table 3a +# keep-hot combi loss (61) = 600 kWh/yr; no primary/storage loss. +LINE_219_HOT_WATER_FUEL_KWH: Final[float] = 3496.8121 + +# Worksheet (206)/(207) main space-heating efficiency — base 82, no +# −5pp (interlock present). Watch these if the pin ever regresses: a +# silent interlock flip drops them to 77/68. +LINE_206_MAIN_1_EFFICIENCY_PCT: Final[float] = 82.0 +LINE_207_MAIN_2_EFFICIENCY_PCT: Final[float] = 82.0 + + +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 (mirror of the case-6 helper).""" + 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-7 Summary through extractor + mapper.""" + 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 93df288e..1d36e443 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 @@ -43,6 +43,7 @@ from tests.domain.sap10_calculator.worksheet import ( _elmhurst_worksheet_001431_6035 as _w001431_6035, _elmhurst_worksheet_001431_case5 as _w001431_case5, _elmhurst_worksheet_001431_case6 as _w001431_case6, + _elmhurst_worksheet_001431_case7 as _w001431_case7, ) from tests.domain.sap10_calculator.worksheet._elmhurst_fixtures import ( ALL_FIXTURES as _ELMHURST_FIXTURES, @@ -259,6 +260,24 @@ _FIXTURE_PINS: Final[dict[str, FixtureCascadePins]] = { lighting_kwh_per_yr=357.6571, pumps_fans_kwh_per_yr=356.0, ), + # Mapper-driven cohort entry — Summary_001431_case7.pdf → extractor → + # mapper → calculator. Case 6 with the heating swapped to a CONDENSING + # OIL COMBI (SAP code 130, Table 4b 82/73) with NO cylinder — combi + # instantaneous DHW (WHC 901), Table 3a keep-hot combi loss (61), no + # primary/storage loss, boiler interlock PRESENT (no −5pp). Validates + # the combi HW + space efficiency path that golden cert 0240 uses; + # reproduces every line ref EXACTLY with no calculator change. + # main_heating_fuel_kwh_per_yr is the (211)+(213) two-system sum. + "001431_case7": FixtureCascadePins( + sap_score=73, sap_score_continuous=72.6153, ecf=1.9631, + total_fuel_cost_gbp=1123.3372, co2_kg_per_yr=5738.9315, + space_heating_kwh_per_yr=12646.3783, + main_heating_fuel_kwh_per_yr=15422.4125, + secondary_heating_fuel_kwh_per_yr=0.0, + hot_water_kwh_per_yr=3496.8121, + lighting_kwh_per_yr=357.6571, + pumps_fans_kwh_per_yr=356.0, + ), } @@ -276,6 +295,7 @@ _FIXTURE_MODULES: Final[dict[str, ModuleType]] = { "001431_6035": _w001431_6035, "001431_case5": _w001431_case5, "001431_case6": _w001431_case6, + "001431_case7": _w001431_case7, }