docs(profile-case34): mark the space-demand residual closed (450e33e1)

The §2 (13) draught-lobby fix landed the +46.3 kWh space-heating over-count
on the worksheet; the tracked diagnostic's header and run-banner now reflect
the closed state (Δ +0.0036 SAP, sub-2dp-rounding) instead of the open gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-11 09:01:41 +00:00
parent 450e33e15d
commit b7d283cd3a

95
scripts/profile_case34.py Normal file
View file

@ -0,0 +1,95 @@
"""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()