S0380.208: case 7 combi e2e fixture — condensing-oil-combi path validated exact

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 17:57:22 +00:00
parent 6ac67a4c6f
commit fe59c4d8a2
3 changed files with 154 additions and 0 deletions

Binary file not shown.

View file

@ -0,0 +1,134 @@
"""Mapper-driven cascade fixture for the Elmhurst P960-0001-001431
"simulated case 7" worksheet the CONDENSING-OIL-COMBI variant of
[[case 6]], generated to validate the combi HW + space efficiency path
that golden cert 0240-0200-5706-2365-8010 exercises.
Routes the Summary PDF through ElmhurstSiteNotesExtractor +
from_elmhurst_site_notes (no hand-built EpcPropertyData) so the pin
exercises the whole extractor + mapper + calculator pipeline.
WHY THIS FIXTURE EXISTS
-----------------------
Case 6 is SAP code 127 ("Condensing oil *boiler*", regular) + a 110 L
cylinder so it never exercised the COMBI instantaneous-DHW efficiency
path. 0240 is SAP code 130 ("Condensing combi oil boiler") with NO
cylinder. Case 7 is case 6 with that single difference swapped in:
- both mains SAP code 130 (Table 4b winter 82 / summer 73);
- NO hot-water cylinder combi instantaneous DHW (WHC 901), Table 3a
keep-hot combi loss (61), no primary/storage loss;
- boiler interlock PRESENT (combi + room thermostat 2106, no cylinder)
NO 5pp penalty, base eff 82/73 the OPPOSITE of case 6.
The dual-main rads(2106, 51%) + UFH(2110, 49%) different-parts structure,
the 6 "Roof of Room" rooflights, and the fabric are unchanged from case 6.
WHAT IT PROVED
--------------
The cascade reproduces the case-7 worksheet EXACTLY at abs=1e-4 on every
top-level output with ZERO calculator changes the condensing-combi
(130) + no-cylinder + dual-main + Appendix D Eq D1 path is already
correct. This fixture is a regression lock on that path; it did NOT
require a fix. (It also exonerates the combi mechanism as the source of
0240's API-path residual — see docs/HANDOVER_0240_CLOSURE.md.)
Combi-path worksheet line refs (P960-0001-001431, Block 1):
- (206)/(207) main space-heating efficiency = 82.0000 / 82.0000 (base,
interlock present, no 5pp).
- (216) water-heater efficiency (summer base) = 73.0000.
- (217)m water-heater monthly efficiency = combi blend 73.00 80.18.
- (61)m combi loss = 50.9589 (Jan) = 600 kWh/yr flat (Table 3a
keep-hot, daily HW volume > 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)

View file

@ -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,
}