Add a cell-by-cell inspector for landlord-override → effective-EPC mapping

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-17 17:04:44 +00:00
parent 86b5387a05
commit 5939520b0d

118
scripts/inspect_overlay.py Normal file
View file

@ -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}")