From 5939520b0d5b958bf741ea5964a84e8b1a5a1008 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 17 Jun 2026 17:04:44 +0000 Subject: [PATCH] =?UTF-8?q?Add=20a=20cell-by-cell=20inspector=20for=20land?= =?UTF-8?q?lord-override=20=E2=86=92=20effective-EPC=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/inspect_overlay.py | 118 +++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 scripts/inspect_overlay.py diff --git a/scripts/inspect_overlay.py b/scripts/inspect_overlay.py new file mode 100644 index 00000000..e9f3ea34 --- /dev/null +++ b/scripts/inspect_overlay.py @@ -0,0 +1,118 @@ +"""Step-through inspector: did a Property's Landlord Overrides map correctly? + +Run cell-by-cell in VS Code (each `# %%` is a cell — ▶ Run Cell / Shift+Enter), +or top-to-bottom with `PYTHONPATH=. python -m scripts.inspect_overlay`. + +For one PROPERTY_ID it shows: the `property_overrides` rows, whether each mapped +to a Simulation Overlay (or is a silent no-op), the lodged-vs-effective main +wall codes the calculator scores, and the SAP delta the overlay produces. EPC is +fetched LIVE from the gov API by UPRN (same as run_modelling_e2e); nothing is +written. Edit PROPERTY_ID in the second cell and re-run. +""" + +# %% 1 — setup: env, DB engine, gov-EPC client +from __future__ import annotations + +import os +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(_REPO_ROOT)) + +for _raw in (_REPO_ROOT / "backend" / ".env").read_text(encoding="utf-8").splitlines(): + _line = _raw.strip() + if _line and not _line.startswith("#") and "=" in _line: + _k, _v = _line.split("=", 1) + os.environ.setdefault(_k.strip(), _v.strip().strip('"').strip("'")) + +from sqlalchemy import create_engine, text +from sqlmodel import Session + +from datatypes.epc.domain.epc_property_data import ( + BuildingPartIdentifier, + EpcPropertyData, +) +from domain.epc.wall_type_overlay import wall_overlay_for +from domain.property.property import Property, PropertyIdentity +from domain.sap10_calculator.calculator import Sap10Calculator +from infrastructure.epc_client.epc_client_service import EpcClientService +from repositories.property.landlord_override_overlays import overlays_from +from repositories.property.property_overrides_postgres_reader import ( + PropertyOverridesPostgresReader, +) + +_engine = create_engine( + f"postgresql+psycopg2://{os.environ['DB_USERNAME']}:{os.environ['DB_PASSWORD']}" + f"@{os.environ['DB_HOST']}:{os.environ['DB_PORT']}/{os.environ['DB_NAME']}" +) +_epc_client = EpcClientService(os.environ["OPEN_EPC_API_TOKEN"]) +_reader = PropertyOverridesPostgresReader(lambda: Session(_engine)) + + +def _main_wall(epc: EpcPropertyData) -> object: + """The MAIN building part — its wall_construction / wall_insulation_type are + what the calculator turns into the wall U-value.""" + return next( + p for p in epc.sap_building_parts if p.identifier is BuildingPartIdentifier.MAIN + ) + + +# %% 2 — pick the property, resolve its UPRN +PROPERTY_ID = 709672 # <-- edit me + +with _engine.connect() as _conn: + _row = _conn.execute( + text("SELECT uprn, address FROM property WHERE id = :id"), + {"id": PROPERTY_ID}, + ).fetchone() +assert _row is not None, f"property {PROPERTY_ID} not found" +uprn, address = int(_row[0]), _row[1] +print(f"property {PROPERTY_ID} · uprn {uprn} · {address}") + +# %% 3 — fetch the lodged EPC live from the gov API +epc = _epc_client.get_by_uprn(uprn) +assert epc is not None, f"no EPC found for uprn {uprn}" +print(f"lodged EPC: {epc.property_type=} {epc.built_form=}") +print(f"lodged main wall: {_main_wall(epc)!r}") + +# %% 4 — the property_overrides rows the finaliser wrote +overrides = _reader.overrides_for(PROPERTY_ID) +print(f"{len(overrides.rows)} override row(s):") +for r in overrides.rows: + print(f" part {r.building_part} · {r.override_component} = {r.override_value!r}") + +# %% 5 — per-row mapping: did each override produce an overlay, or is it a no-op? +for r in overrides.rows: + if r.override_component == "wall_type": + sim = wall_overlay_for(r.override_value, r.building_part) + if sim is None: + print(f" wall_type {r.override_value!r} -> NO-OP (material/state unmapped)") + else: + bp = next(iter(sim.building_parts.values())) + print( + f" wall_type {r.override_value!r} -> " + f"wall_construction={bp.wall_construction} " + f"wall_insulation_type={bp.wall_insulation_type}" + ) + else: + print(f" {r.override_component} {r.override_value!r} -> not overlaid (tracer is wall-only)") + +# %% 6 — fold the overrides into the Effective EPC +overlays = overlays_from(overrides) +prop = Property( + identity=PropertyIdentity(portfolio_id=0, postcode="", address="", uprn=uprn), + epc=epc, + landlord_overrides=overlays, +) +effective = prop.effective_epc +print(f"{len(overlays)} overlay(s) folded · source_path={prop.source_path}") + +# %% 7 — lodged vs effective: the codes the calculator scores +print(f"lodged main wall: {_main_wall(epc)!r}") +print(f"effective main wall: {_main_wall(effective)!r}") + +# %% 8 — the SAP delta the overlay produces (the whole point) +lodged_sap = Sap10Calculator().calculate(epc).sap_score +effective_sap = Sap10Calculator().calculate(effective).sap_score +print(f"SAP lodged={lodged_sap} effective={effective_sap} delta={effective_sap - lodged_sap:+d}")