mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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:
parent
6ac67a4c6f
commit
fe59c4d8a2
3 changed files with 154 additions and 0 deletions
BIN
backend/documents_parser/tests/fixtures/Summary_001431_case7.pdf
vendored
Normal file
BIN
backend/documents_parser/tests/fixtures/Summary_001431_case7.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue