Builds the frozen validation corpus: samples postcodes from the register, then
caches each postcode's full cohort of raw cert payloads (the shape
from_api_response consumes), grouped by postcode, resumably. Reads the token
from backend/.env; cache dir /tmp/epc_prediction_corpus (EPC_PREDICTION_CORPUS
override). IO plumbing, not test-driven. Pairs with the leave-one-out harness.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reusable per-schema profiler: glazed_area band mix, Validation Cohort size,
observed-vs-predicted band glazing/floor ratio, and the ND/str sentinels that
drive schema widening. Regenerates the ADR-0028 transfer-check table from any
harvested corpus.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The new gov EPC API (api.get-energy-performance-data..., Bearer auth) returns
403 "Bad authentication header" with EPC_AUTH_TOKEN but 200 with
OPEN_EPC_API_TOKEN — the token name is misleading (it is the Bearer token for
the new API, not the open-data API). Verified live against
/api/domestic/search. Unblocks the live EPC fetch in run_modelling_e2e.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slice 5 (local run sources the DB, read-only) + slice 6 (optional persist),
landing together as one script rewrite (the persist path is interleaved with
the compute path).
The same local computation now runs whether or not the result is stored:
- Both modes price against the live `material` catalogue (read-only
ProductPostgresRepository over one shared Session) and model against a real
Scenario read from the DB (--scenario-id; its goal_value drives the band,
rejected if null) — so the inspected recommendations are exactly what gets
stored. The JSON sample catalogue is no longer used by this script.
- --measures restricts the run to a comma-separated considered_measures
allowlist (e.g. high_heat_retention_storage_heaters,solar_pv).
- --persist writes the inputs (EPC + spatial + solar) and the *same* computed
Plan via the production repos in one PostgresUnitOfWork, then commits
(idempotent: PlanPostgresRepository replaces by (property_id, scenario_id)).
Gated: --persist requires --scenario-id and --portfolio-id. Default is
inspect-only — no DB writes.
harness.console.run_modelling gains `products` and `scenario` overrides (the
seam the script drives); defaults unchanged, so existing callers are
unaffected. Suite 257 pass + 3 xfail; pyright clean; --help/guard/measure
parsing verified. Not yet executed against the DB (awaiting property_ids +
write-confirm).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per Property the inspection script now resolves the UPRN's spatial
reference from the Ordnance Survey Open-UPRN parquet in S3
(GeospatialS3Repository over a boto3 ParquetReader) and threads both
levers into run_modelling:
- planning_restrictions: the conservation/listed/heritage flags that gate
the wall + solar measures (ADR-0019/0020).
- solar_insights: a live Google Solar buildingInsights fetch keyed on the
reference coordinates, so the Solar PV Options can fire (ADR-0026).
Mirrors IngestionOrchestrator._fetch's coords->solar flow. Degrades
gracefully per Property: a UPRN S3 doesn't cover -> unrestricted/no-solar;
a point Google has no coverage for (BuildingInsightsNotFoundError) ->
no-solar; both still modelled. --no-solar skips the Google leg. A context
note (restrictions; solar) is printed and written to the md/csv summary.
Verified live: spatial_for + solar fetch round-trip on real UPRNs (S3 via
ambient ~/.aws creds, pyarrow reads parquet bytes). pyright clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Revives the local recommendation-inspection flow for specific Properties.
`scripts/run_modelling_e2e.py` reads each Property's UPRN from the DB
(read-only), fetches the latest EPC live from the gov EPC API by UPRN, runs the
Modelling stage in memory (all Generators → Optimiser → costed, attributed
Plan), and prints a per-Property plan table + writes a Markdown/CSV summary.
Persists nothing — purely for inspection.
The local DB's Properties have no linked ingested EPC (epc_property.property_id
is NULL for all rows; Ingestion's source clients are stubbed, #1136), so the
EPC must be fetched inline rather than read back. Builds the connection from the
`DB_*` env vars in backend/.env and the EPC token from `EPC_AUTH_TOKEN`.
Threads optional solar insights through harness `run_modelling` (so Solar PV
Options can fire once coordinates are wired) and adds the `solar_pv` catalogue
row. Solar + planning restrictions + DB persistence are noted follow-ups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls in 42 commits of calculator/mapper accuracy fixes from the per-cert
mapper-validation and floor/roof/heating fronts.
Conflict resolutions:
- mapper `_is_elmhurst_roof_window`: main dropped the branch's "wall location →
vertical" guard (it broke cert 000516's rooflight), but that re-broke cert
001431's two External-wall U>3.0 windows (which must stay vertical). The two
certs lodge a BYTE-IDENTICAL §11 row, so neither location nor U separates
them — the real discriminator is the room-in-roof context. Replaced the
unconditional U>3.0 backstop with one gated on the BP having a room-in-roof
(`_elmhurst_bp_has_room_in_roof`): 000516's Main BP has a "Room in roof type
1" (→ rooflight), 001431's does not (→ vertical). Validated against BOTH —
full Elmhurst worksheet suite 1038 pass + the 001431 window-extraction pin.
- property_postgres_repository: kept main's `ids_by_uprn` method + the branch's
`_restrictions_of` helper.
- sap_fuel.py: the branch relocated it to domain/billing/ (already carrying
main's to_table_32_code normalization), so kept the old path deleted.
Fallout from main's fabric fixes (validated by the boiler-3 real-cert pin which
still reproduces at delta 0):
- re-pinned the boiler-1 + boiler-instant-hw ASHP snapshot scores;
- main's §14.2 gas-boiler main-fuel derivation resolved the BGB/102 baseline
gap, so `test_gas_boiler_instant_hw_before_baselines` is now a passing test
(was an xfail tripwire).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Joins each computed cert's signed error (eval _results.csv) with a rich
feature set extracted from its RAW API JSON (not the mapped
EpcPropertyData), then ranks (feature, value) buckets by error carried
and by |mean signed| bias. Surfaces systematic API-path handling gaps —
a field the mapper silently drops still shows as an error-correlated
bucket. Companion to eval_api_sap_accuracy.py / decompose_api_cost_error.py.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror of eval_api_sap_accuracy.py that decomposes each cert's SAP error
into per-component energy/cost deltas WITHOUT generating an Elmhurst
worksheet. Calibrates the consumer price from the certs we already get
right (gas £0.0809/kWh n=291, elec £0.2839/kWh n=326 over |SAP err|<0.4),
then for every cert compares our_component_kWh × price to the lodged
heating_cost_current / hot_water_cost_current / lighting_cost_current and
back-calculates a numeric energy target (lodged_cost / price).
Clusters errors by (component × direction). On the 905-cert sample this
reveals heat:high (we over-state heating energy → under-rate SAP) as the
dominant broken cluster: 332 certs, only 36.7% within 0.5. Output CSV at
<cache>/_cost_decomposition.csv.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first cut of elmhurst_input_sheet.py introspected the `schema`
dataclasses (rdsap_schema_*.py) but the mapper emits the `epc_property_data`
domain types, whose fields differ (wall_thickness_mm not wall_thickness;
total_floor_area_m2 not total_floor_area; frame_material not pvc_frame;
cylinder_insulation_thickness_mm; SapRoomInRoof has gable_*_length_m not
insulation/roof_room_connected). Worse, the getattr-with-None-default helper
printed None over real data, nearly sending a debug session chasing a
non-existent "dimensions dropped" mapper bug on cert 2100 (the dims map
fine; that cert's error is elsewhere). Switched to direct attribute access
so a future rename fails loudly, fixed every field name against the live
domain objects, and added roof_construction_type / floor_type for context.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reconstructs the per-cert "Elmhurst SAP input sheet" generator that the
API-accuracy debugging loop relied on (the worked example survives at
'sap worksheets/golden fixture debugging/6035_elmhurst_input_sheet.md'); the
original was a throwaway and never committed. Companion to
eval_api_sap_accuracy.py: once that names a worst-offender cert, this dumps
the codes the mapper hands the calculator (from_api_response → EpcPropertyData)
in the 6035 layout — header, lodged element descriptions, building parts +
dimensions, windows, doors/heating/water/vent — plus the lodged reference
outputs and OUR continuous SAP next to the lodged value, to read side-by-side
with the Elmhurst Summary / P960 worksheet PDF.
Reads the fetch_2026_epc_sample.py cache (EPC_SAMPLE_CACHE, default
/tmp/epc_2026_sample). `--out-dir` writes <cert>_elmhurst_input_sheet.md.
Pyright strict clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three reusable scripts (each with a purpose/usage docstring) for wide-scale
testing of the calculator's API front-end against the GOV.UK EPB register —
the toolkit behind the 1000-cert study (docs/HANDOVER_API_SAMPLE_ACCURACY.md):
fetch_2026_epc_sample.py — sample cert numbers across a date window
(random pages) + download full schema-21 JSON
to a cache; resumable, 429/5xx backoff.
eval_api_sap_accuracy.py — % within 0.5 SAP, error histogram, worst-40,
and the mapper/calculator raise breakdown.
analyse_api_sap_clusters.py — error grouped by property + heating type to
locate clusters (electric heating, flats, PV).
Cache dir defaults to /tmp/epc_2026_sample, overridable via EPC_SAMPLE_CACHE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fetch_epc_bulk_sample streams certificates-<year>.json out of the bulk ZIP via
range requests, keeps the first N SAP-version matches, and writes each cert's
inner document to <out>/<cert>.json for run_property_report. Stops after N, so
only the member prefix transfers, not the 15.7 GB archive (RangeFile.bytes_read
reports the true transfer vs the absolute ZIP offset). Verified on 2026: 100
SAP-10.2 certs -> report ran 81 scorable (MAE 2.03), 46 flagged, 19 raises
(11 full-SAP schema 19.1.0, 7 unmapped floor_construction 0/3, 1 missing
post_town) — real shadow-validation signal vs the curated golden 57.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
run_property_report builds the three-section Markdown+CSV report over a dir of
API-shaped EPC JSON, offline (defaults to the golden 57: 57/57 scorable, MAE
0.54, 6 flagged |Δ|>0.5). fetch_epc_dump pulls raw cert JSON from the live API
by --uprn/--postcode (picking the latest cert per match, skipping existing
files), mirroring fetch_cohort2's proven HTTP shape and reading
OPEN_EPC_API_TOKEN. Report artifacts + epc_dump/ are gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CertResult now carries its Plan (with flat baseline/post-SAP/measures
properties), and `format_cohort_csv` renders one browsable row per cert
(SAP transition, band, measures, cost, bill saving, valuation %, error).
`scripts/run_modelling_cohort.py` is turnkey: no args runs the committed
golden cohort, prints a sense-check table for the first measure-bearing
certs (a capped preview so a large dump doesn't flood the terminal), the
summary, and writes modelling_cohort.csv (gitignored). Point it at the
EPC dump when it lands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`harness.cohort.run_cohort(paths)` parses each API-shaped EPC JSON with
from_api_response and models it via run_modelling — no database, no
network — capturing per-cert errors instead of aborting the sweep, plus
`format_cohort_summary`. A thin `scripts/run_modelling_cohort.py` CLI
points it at a directory. Proven over the 57 golden API certs: 56 ran
offline, 15 produced measures, 1 errored (COAL has no Fuel Rates entry —
a BillDerivation coverage gap, not a harness one). Ready for the EPC dump.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The calculator tests lived under domain/sap10_calculator/{tests,worksheet/
tests,rdsap/tests,climate/tests,validation/tests}, none of which are in
pytest.ini testpaths — so CI (which collects tests/) never ran them. Relocate
all five dirs to tests/domain/sap10_calculator/{,worksheet,rdsap,climate,
validation}, mirroring the tests/domain/property_baseline/ convention, so the
cascade-pin / golden / e2e conformance suites run in CI.
Mechanics:
- git mv preserves history (110 files).
- Flattening the trailing /tests keeps each file's depth-to-repo-root
identical, so all 16 repo-root parents[4] fixture refs stay valid. Only
test_pcdb_etl.py's parents[1] (→ pcdb data) and one hardcoded absolute
golden-fixture path in test_cert_to_inputs.py needed rebasing.
- Cross-imports rewritten domain.sap10_calculator.worksheet.tests →
tests.domain.sap10_calculator.worksheet (21 files incl. the external
importer backend/documents_parser/tests/test_summary_pdf_mapper_chain.py).
- Golden-fixture path strings in test_summary_pdf_mapper_chain.py +
scripts/fetch_cohort2_api_jsons.py updated to the new location (the JSONs
moved with the rdsap tests).
load_cells / gitignored worksheet xlsx: the xlsx-pinned tests (test_dimensions
/ ventilation / water_heating) read 2026-05-19-17-18 RdSap10Worksheet.xlsx,
which is gitignored (.gitignore `*.xlsx`) and so absent in CI. _xlsx_loader.
load_cells now pytest.skip()s when the file is absent, so those tests run
locally and skip cleanly in CI instead of erroring — no new CI failures from
the move, and the gitignore policy is respected.
Verified: tests/domain/sap10_calculator + backend/documents_parser +
tests/domain/property_baseline = 2248 pass, 1 skipped; pyright resolves the
new import paths with zero import-resolution errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the EpcClientService package (client + _retry + exceptions + tests) from
the dying backend/ tree to infrastructure/epc_client/ as the New-EPC-API Fetcher;
update the two callers (address2UPRN, a script). All 14 client tests pass.
Add SolarRepository port + SolarPostgresRepository persisting Google Solar
building insights as JSONB (solar_building_insights table), one row per Property.
The EPC repo half of this slice already landed in #1129. pyright strict clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds scripts/fetch_cohort2_api_jsons.py (throwaway one-off) plus 38
golden fixtures under domain/sap10_calculator/rdsap/tests/fixtures/golden/
covering every cert in "sap worksheets/additional with api 2/".
Each JSON is the inner `data` payload from the gov.uk EPB
/api/certificate endpoint — the same shape EpcPropertyDataMapper
.from_api_response consumes today.
Required prerequisite for Slice B (parametrized API-path chain test
that mirrors the cohort-2 Summary-path sweep at 1e-4 vs worksheet).
Per the cross-mapper-parity primitive: API EPC and Elmhurst EPC must
produce SAP within 1e-4 of each other and of the worksheet — the SAP
cascade is the load-bearing equivalence check.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>