diff --git a/backend/documents_parser/tests/test_room_heater_worksheet_001431.py b/backend/documents_parser/tests/test_room_heater_worksheet_001431.py new file mode 100644 index 00000000..e7892678 --- /dev/null +++ b/backend/documents_parser/tests/test_room_heater_worksheet_001431.py @@ -0,0 +1,101 @@ +"""Worksheet pins for the cat-10 electric-room-heater dwelling (ref 001431). + +Fixture: `sap worksheets/Recommendations Elmhurst Files/main heating/high +heat retention storage heaters/electric room heaters/before/` — Summary +(site-notes input) + P960 (the `(1)..(286)` worksheet ground truth). The +dwelling lodges main `sap_main_heating_code=691` (electric room heaters), +control `2601`, an `18 Hour` meter, and water heating `sap_code=909` +(electric instantaneous, single-point at the point of use — NO cylinder, +NO solar, NO WWHRS). + +Per [[feedback-worksheet-not-api-reference]] + [[feedback-zero-error-strict]] +the worksheet PDF is the 1e-4 target. Each pin below is a P960 line ref +transcribed to 4 d.p. and asserted via `abs(x - y) <= 1e-4` against the +extractor → mapper → cascade output. + +Because the SAP 10.2 worksheet computes the rating block (UK-average +climate, Table 12 regulated prices) separately from the EPC block +(postcode climate, Table 32 prices), the rating-mode cascade +(`cert_to_inputs`) is pinned against the rating block and the demand-mode +cascade (`cert_to_demand_inputs`) against the EPC block. +""" + +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + +from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor +from datatypes.epc.domain.mapper import EpcPropertyDataMapper +from domain.sap10_calculator.calculator import calculate_sap_from_inputs +from domain.sap10_calculator.rdsap.cert_to_inputs import ( + SAP_10_2_SPEC_PRICES, + cert_to_inputs, +) + +_FIXTURE_DIR = ( + Path(__file__).parents[3] + / "sap worksheets/Recommendations Elmhurst Files/main heating" + / "high heat retention storage heaters/electric room heaters/before" +) + +# P960 line ref (219) "Water heating fuel used" — rating block. The water +# heater is electric (efficiency (216) = 100 %), so (219) == (64) output. +_WORKSHEET_LINE_219_WATER_FUEL_KWH = 1770.2313 + +_ABS_TOLERANCE = 0.0001 + + +def _summary_pdf_to_pages(pdf: Path) -> list[str]: + """Summary PDF → one Textract-style token string per page (the same + `pdftotext -layout` → whitespace-split preprocessing the rest of the + documents_parser chain tests use).""" + page_count_text = subprocess.run( + ["pdfinfo", str(pdf)], capture_output=True, text=True + ).stdout + page_count_match = re.search(r"Pages:\s+(\d+)", page_count_text) + assert page_count_match is not None, f"no page count in {pdf}" + page_count = int(page_count_match.group(1)) + pages: list[str] = [] + for page_index in range(1, page_count + 1): + layout = subprocess.run( + [ + "pdftotext", "-layout", + "-f", str(page_index), "-l", str(page_index), + str(pdf), "-", + ], + capture_output=True, + text=True, + ).stdout + pages.append( + "\n".join( + token + for line in layout.splitlines() + for token in re.split(r"\s{2,}", line.strip()) + if token + ) + ) + return pages + + +def test_electric_room_heater_water_fuel_matches_worksheet_line_219() -> None: + # Arrange — route the before/ Summary through the full extractor → + # mapper → rating cascade. Water heating SAP code 909 is a single- + # point electric instantaneous heater at the point of use, so per + # SAP 10.2 §4 (p.23, l.1416) it has NO distribution loss: worksheet + # (46)m = 0 and (62)m = 0.85 × (45)m collapses to the (219) fuel. + summary_pdf = next(_FIXTURE_DIR.glob("Summary_*.pdf")) + pages = _summary_pdf_to_pages(summary_pdf) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) + expected_water_fuel_kwh = _WORKSHEET_LINE_219_WATER_FUEL_KWH + + # Act + rating = calculate_sap_from_inputs( + cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES), + ) + actual_water_fuel_kwh = rating.hot_water_kwh_per_yr + + # Assert + assert abs(actual_water_fuel_kwh - expected_water_fuel_kwh) <= _ABS_TOLERANCE diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 95fb2d75..a0b833a7 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -5653,6 +5653,7 @@ def _water_heating_worksheet_and_gains( primary_loss_monthly_kwh_override=primary_loss_override, has_electric_shower=has_electric_shower, electric_shower_count=electric_shower_count, + is_instantaneous_at_point_of_use=is_instantaneous, ) solar_hw_override = _solar_hw_monthly_override( epc=epc, @@ -5670,6 +5671,7 @@ def _water_heating_worksheet_and_gains( solar_water_heating_monthly_kwh_override=solar_hw_override, has_electric_shower=has_electric_shower, electric_shower_count=electric_shower_count, + is_instantaneous_at_point_of_use=is_instantaneous, ) return wh_result, wh_result.heat_gains_monthly_kwh diff --git a/domain/sap10_calculator/worksheet/water_heating.py b/domain/sap10_calculator/worksheet/water_heating.py index dec71237..52272d84 100644 --- a/domain/sap10_calculator/worksheet/water_heating.py +++ b/domain/sap10_calculator/worksheet/water_heating.py @@ -843,6 +843,7 @@ def water_heating_from_cert( electric_shower_monthly_kwh_override: Optional[tuple[float, ...]] = None, has_electric_shower: bool = False, electric_shower_count: int = 0, + is_instantaneous_at_point_of_use: bool = False, ) -> WaterHeatingResult: """SAP 10.2 §4 orchestrator — chain every line ref from (42) through (65) for a combi-gas dwelling with optional PCDB-backed combi loss. @@ -912,7 +913,7 @@ def water_heating_from_cert( ) distribution = distribution_loss_monthly_kwh( monthly_energy_content_kwh=energy_content, - is_instantaneous_at_point_of_use=False, + is_instantaneous_at_point_of_use=is_instantaneous_at_point_of_use, ) combi = ( combi_loss_monthly_kwh_override diff --git a/sap worksheets/Recommendations Elmhurst Files/main heating/high heat retention storage heaters/electric room heaters/before/P960-0001-001431 - 2026-06-02T152212.419.pdf b/sap worksheets/Recommendations Elmhurst Files/main heating/high heat retention storage heaters/electric room heaters/before/P960-0001-001431 - 2026-06-02T152212.419.pdf new file mode 100644 index 00000000..022d726f Binary files /dev/null and b/sap worksheets/Recommendations Elmhurst Files/main heating/high heat retention storage heaters/electric room heaters/before/P960-0001-001431 - 2026-06-02T152212.419.pdf differ diff --git a/sap worksheets/Recommendations Elmhurst Files/main heating/high heat retention storage heaters/electric room heaters/before/Summary_001431 (1).pdf b/sap worksheets/Recommendations Elmhurst Files/main heating/high heat retention storage heaters/electric room heaters/before/Summary_001431 (1).pdf new file mode 100644 index 00000000..c092d639 Binary files /dev/null and b/sap worksheets/Recommendations Elmhurst Files/main heating/high heat retention storage heaters/electric room heaters/before/Summary_001431 (1).pdf differ