mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Reduced-field window U: heat_transmission derived the synthesised-window raw U from u_window(all None) -> the 2.5 placeholder regardless of glazing. Now routes the (uniform) glazing_type code through u_window (RdSAP Table 24) so e.g. double pre-2002 reads 2.8, not 2.5. Only the pre-SAP10 reduced-field path is affected (21.0.1 certs carry per-window U upstream) — the RdSAP-21.0.1 corpus gauge is unchanged at 66.9% within-0.5. test_real_cert_sap_accuracy: pin uprn_10002468137 (RdSAP-17.1, all-electric storage heaters) at SAP 61, validated against Elmhurst on identical inputs (dual off-peak immersion, 110 L cylinder, 2 baths). Our engine reproduces Elmhurst's fuel cost to the penny; lodged 55 is the old SAP-2012 schema. Tooling to grow the accuracy corpus: - scripts/fetch_real_life_epc_sample.py — capture a cert by UPRN into the corpus. - scripts/compare_epc_paths.py — diff gov-API vs Elmhurst-summary EpcPropertyData and run both through the engine, localising mapper vs calculator differences. - skill validate-cert-sap-accuracy — the end-to-end loop (capture -> Elmhurst inputs -> human builds -> compare -> reconcile -> pin in the test). - skill epc-to-elmhurst-rdsap-inputs reference: corrected immersion (code 1=dual), cylinder size (code 2 = Normal/110 L), and bath-count (WWHRS sub-tab) mappings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
130 lines
4.4 KiB
Python
130 lines
4.4 KiB
Python
"""Capture a real EPC certificate by UPRN for the SAP accuracy test suite.
|
|
|
|
Resolves a UPRN to its latest lodged certificate via the GOV.UK EPB
|
|
register, downloads the full ``data`` payload (the exact shape
|
|
``EpcPropertyDataMapper.from_api_response`` consumes), and freezes it
|
|
under the schema-bucketed sample tree the accuracy test reads:
|
|
|
|
backend/epc_api/json_samples/real_life_examples/<schema_type>/uprn_<uprn>/epc.json
|
|
|
|
It also prints the lodged SAP rating and what ``Sap10Calculator``
|
|
currently produces, so a new case can be added to
|
|
``tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py`` with
|
|
the right ``schema`` / ``sap_score`` straight away.
|
|
|
|
USAGE
|
|
-----
|
|
PYTHONPATH=/workspaces/model python scripts/fetch_real_life_epc_sample.py <uprn> [<uprn> ...]
|
|
|
|
Token is read from ``backend/.env`` (``OPEN_EPC_API_TOKEN``, falling
|
|
back to ``EPC_AUTH_TOKEN``). Re-running overwrites the sample.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import sys
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from dotenv import load_dotenv
|
|
|
|
_BASE = "https://api.get-energy-performance-data.communities.gov.uk"
|
|
_SAMPLES_ROOT = pathlib.Path(
|
|
"backend/epc_api/json_samples/real_life_examples"
|
|
)
|
|
|
|
|
|
def _headers() -> dict[str, str]:
|
|
load_dotenv("backend/.env")
|
|
token = os.environ.get("OPEN_EPC_API_TOKEN") or os.environ["EPC_AUTH_TOKEN"]
|
|
return {"Authorization": f"Bearer {token}", "Accept": "application/json"}
|
|
|
|
|
|
def _latest_cert_number(uprn: int, headers: dict[str, str]) -> str:
|
|
resp = httpx.get(
|
|
f"{_BASE}/api/domestic/search",
|
|
params={"uprn": uprn},
|
|
headers=headers,
|
|
timeout=30.0,
|
|
)
|
|
resp.raise_for_status()
|
|
rows: list[dict[str, Any]] = resp.json().get("data", [])
|
|
if not rows:
|
|
raise SystemExit(f"UPRN {uprn}: no certificates found")
|
|
latest = max(rows, key=lambda r: r["registrationDate"])
|
|
return str(latest["certificateNumber"])
|
|
|
|
|
|
def _fetch_cert_data(cert_num: str, headers: dict[str, str]) -> dict[str, Any]:
|
|
resp = httpx.get(
|
|
f"{_BASE}/api/certificate",
|
|
params={"certificate_number": cert_num},
|
|
headers=headers,
|
|
timeout=30.0,
|
|
)
|
|
resp.raise_for_status()
|
|
data: dict[str, Any] = resp.json()["data"]
|
|
return data
|
|
|
|
|
|
def _report(uprn: int, cert_num: str, data: dict[str, Any]) -> None:
|
|
"""Print lodged rating + current calculator output for the captured cert."""
|
|
from infrastructure.epc_client.epc_client_service import EpcClientService
|
|
from domain.sap10_calculator.calculator import Sap10Calculator
|
|
from unittest.mock import patch
|
|
|
|
def _mock(*_a: object, **_k: object) -> httpx.Response:
|
|
return httpx.Response(
|
|
200, json={"data": data}, request=httpx.Request("GET", "x")
|
|
)
|
|
|
|
print(f" schema_type : {data.get('schema_type')}")
|
|
print(f" lodged rating : {data.get('energy_rating_current')}")
|
|
|
|
service = EpcClientService(auth_token="test-token")
|
|
try:
|
|
with patch("httpx.get", side_effect=_mock):
|
|
epc = service.get_by_certificate_number(cert_num)
|
|
except ValueError as exc:
|
|
# Full-SAP (vs RdSAP) certs aren't supported by the mapper, so the
|
|
# calculator front-end can't consume them. Captured for reference
|
|
# but NOT addable to the RdSAP accuracy suite.
|
|
print(f" NOT MAPPABLE : {exc}")
|
|
return
|
|
result = Sap10Calculator().calculate(epc)
|
|
|
|
print(f" calc sap_score : {result.sap_score}")
|
|
print(f" space_heating_kwh : {result.space_heating_kwh_per_yr:.4f}")
|
|
print(f" main_heating_kwh : {result.main_heating_fuel_kwh_per_yr:.4f}")
|
|
print(f" hot_water_kwh : {result.hot_water_kwh_per_yr:.4f}")
|
|
print(f" co2_kg_per_yr : {result.co2_kg_per_yr:.4f}")
|
|
|
|
|
|
def capture(uprn: int) -> None:
|
|
headers = _headers()
|
|
cert_num = _latest_cert_number(uprn, headers)
|
|
data = _fetch_cert_data(cert_num, headers)
|
|
|
|
schema_type = str(data.get("schema_type") or "unknown-schema")
|
|
out_dir = _SAMPLES_ROOT / schema_type / f"uprn_{uprn}"
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
out = out_dir / "epc.json"
|
|
out.write_text(json.dumps(data, indent=2))
|
|
|
|
print(f"UPRN {uprn} -> cert {cert_num}")
|
|
print(f" wrote : {out}")
|
|
_report(uprn, cert_num, data)
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) < 2:
|
|
raise SystemExit(__doc__)
|
|
for arg in sys.argv[1:]:
|
|
capture(int(arg))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|