"""Decompose simulated case 34 (electric-storage corridor flat) vs its dr87/P960 worksheet, channel by channel. Routes the tracked fixture `backend/documents_parser/tests/fixtures/Summary_case34_storage_flat.pdf` through extractor -> mapper -> calculator (Summary path) and prints the §3 heat-transmission breakdown + the space-heating demand / ventilation / gains / MIT intermediates against the worksheet line refs. The fabric (33), bridging (36), (31) and the door channel are EXACT after HEAD c10881ae. The +46.3 kWh/yr (+0.41%) space-heating over-count was the §2 (13) draught lobby: a corridor flat assumes a lobby (0.0), not the 0.05 no-lobby penalty (RdSAP 10 spec p.30) — closed at HEAD 450e33e1, which lands effective air change (25)m on the worksheet (avg 0.6024) and SAP at 35.3130 vs ws 35.3094 (Δ +0.0036, sub-2dp-rounding). The residual now sits at the worksheet's own 2dp display floor (walls -0.017 W/K). Use this to audit. PYTHONPATH=/workspaces/model python scripts/profile_case34.py """ 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, heat_transmission_section_from_cert, ) _FIXTURE = ( Path(__file__).parent.parent / "backend/documents_parser/tests/fixtures/Summary_case34_storage_flat.pdf" ) def _pages(pdf: Path) -> list[str]: info = subprocess.run( ["pdfinfo", str(pdf)], capture_output=True, text=True, check=True ).stdout pc = int(re.search(r"Pages:\s+(\d+)", info).group(1)) # type: ignore[union-attr] pages: list[str] = [] for i in range(1, pc + 1): layout = subprocess.run( ["pdftotext", "-layout", "-f", str(i), "-l", str(i), str(pdf), "-"], capture_output=True, text=True, check=True, ).stdout toks: list[str] = [] for line in layout.splitlines(): if not line.strip(): toks.append("") continue toks.extend([p for p in re.split(r"\s{2,}", line.strip()) if p]) pages.append("\n".join(toks)) return pages def main() -> None: sn = ElmhurstSiteNotesExtractor(_pages(_FIXTURE)).extract() epc = EpcPropertyDataMapper.from_elmhurst_site_notes(sn) ht = heat_transmission_section_from_cert(epc) print("== §3 HEAT TRANSMISSION (all EXACT at c10881ae) ==") print(f"(31) ext elem area = {ht.total_external_element_area_m2:.4f} | ws 120.369") print(f"(29a) walls = {ht.walls_w_per_k:.4f} | ws 30.902") print(f"(30) roof = {ht.roof_w_per_k:.4f} | ws 91.954") print(f"(28a) floor = {ht.floor_w_per_k:.4f} | ws 27.986") print(f"(26) doors = {ht.doors_w_per_k:.4f} | ws 8.14 (corridor 1.4 + ext 3.0)") print(f"(33) fabric = {ht.fabric_heat_loss_w_per_k:.4f} | ws 207.484") print(f"(36) bridging = {ht.thermal_bridging_w_per_k:.4f} | ws 18.054") print(f"(37) total fabric = {ht.total_w_per_k:.4f} | ws 225.538") res = calculate_sap_from_inputs(cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)) im = res.intermediate print("\n== RESIDUAL CLOSED at 450e33e1: was +46.3 kWh, now -0.96 kWh ==") keys = [ "useful_space_heating_kwh_per_yr", # ws (98) = 11357.24 "infiltration_ach", "infiltration_w_per_k", "internal_gains_annual_avg_w", # ws (73)/(84) "mean_internal_temp_annual_avg_c", # ws (85)-(93) ] for k in keys: if k in im: print(f" {k} = {round(im[k], 4)}") print( "\nworksheet targets: (98) space heat=11357.24 | (35) TMP=250 |" " (25)m eff ach 0.46-0.64 | (20) shelter=0.85 | (19) sheltered sides=2" ) print(f"\nSAP cont = {res.sap_score_continuous:.4f} | ws 35.3094 | " f"Δ = {res.sap_score_continuous - 35.3094:+.4f}") if __name__ == "__main__": main()