- Add SAP-accuracy sample for uprn_10093116543 (epc.json, elmhurst_inputs.md, summary/worksheet PDFs) - Persist hyde viewer stack (xvfb/fluxbox/x11vnc/novnc/websockify) and Playwright chromium in the backend devcontainer; forward noVNC 6080 - Broaden .claude/settings.local.json allowlist (display/python/grep/tail) - In-progress campaign mapper/cert_to_inputs work carried from prior cert Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.8 KiB
Full-SAP 17.1 → EpcPropertyData mapper — decisions log
Running log of load-bearing mapping decisions made during the grilling session.
Each becomes an ADR (docs/adr/) when the mapper lands. Corpus evidence:
backend/epc_api/json_samples/SAP-Schema-17.1/corpus.jsonl (1,000 real certs).
Scope (locked): map full-SAP 17.1 → a valid EpcPropertyData so the existing
RdSAP Sap10Calculator runs end-to-end. Accurate-enough geometry/inputs is the
bar; score reconciliation vs lodged is a later, expert-led step — do NOT tune
the mapping to hit the lodged rating, and do NOT widen tolerances.
D1 — Heat-loss perimeter & party walls (from measured wall areas)
Decision: full-SAP lodges no perimeter; derive it from measured per-wall areas
classified by wall_type:
{1,2,3}exposed →heat_loss_perimeter_m= Σarea ÷ storey_height4party →party_wall_length_m= Σarea ÷ storey_height5internal partition → discarded- any other code →
raise UnmappedApiCode("wall_type", code)(fail loud)
wall_type is a ~99%-clean classifier (corpus). Accept the ~1% tail where a party
wall is mis-typed as 2 (Common wall/Stair Wall); revisit with name hints only
if the later score comparison flags it. Geometric 4·√(footprint) is fallback only
(no usable walls). Detail + worked examples: perimeter_decision.md.
D2 — Window / door / roof opening collapse
Decision: join sap_openings → sap_opening_types by name (100% join in corpus),
then route by opening-type .type:
4vertical window →sap_windows(SapWindow: width, height,u_value→window_transmission_details, orientation, glazing_type, frame_factor)5roof window / rooflight →sap_roof_windows(SapRoofWindow: area=w×h, u_value, orientation, pitch, frame_factor, g⊥←solar_transmittance)1,2,3doors →door_count= #door-openings;insulated_door_u_value= area-weighted door U (only 46/1000 certs lodge >1 distinct door U); doors treated insulated (new-build)- any other opening-type code →
raise UnmappedApiCode("sap_opening_type", code)
Lower-risk than the handoff implied: the engine has first-class fields for all three opening kinds, and full-SAP supplies richer per-opening geometry than RdSAP.
D3 — Living-area fraction (back-solve room count)
Decision: full-SAP measures living_area but the engine only reads it via
habitable_rooms_count → Table 27. Mapper-only: set habitable_rooms_count to
the room count whose Table 27 fraction is closest to the measured living_area/TFA,
so the existing engine path reproduces the measured fraction.
Known caveat (to go back to): habitable_rooms_count is also read for the
Table 5 extract-fan minimum (max(measured_fans, room_count_default),
cert_to_inputs.py:4534). A back-solved count can raise that floor. Bounded
(fans 1–4) and moot for the 445/1000 mechanically-ventilated certs; bites only
in the natural-vent subset where measured fans < default. v1: accept (keeps the
decision mapper-only). Principled future fix: suppress the Table-5 "unknown" default
for full-SAP, since its ventilation is measured, not unknown.
D4 — Fabric U-values: carry descriptions through; age_band is fallback only
Pivotal finding: the engine's u_wall/u_floor/u_roof first parse the element
description for "Average thermal transmittance X W/m²K" and return that measured U
directly, bypassing the age-band cascade (rdsap_uvalues.py:546; wired at
heat_transmission.py:589,779). The parser docstring: "On full-SAP certs the
assessor enters the BS EN ISO 6946 result directly here in lieu of using the cascade."
Corpus: walls parse to measured U 984/1000; floors 692 (rest = "(other premises below)" internal, no loss); roofs 715 (rest = "(other premises above)" internal).
Decision: map full-SAP top-level walls/floors/roofs energy-element lists via
the existing _map_energy_elements (preserves description). The measured fabric U then
flows into the engine — it is NOT discarded. This reverses the initial "RdSAP engine
re-derives from fabricated band-M and over-rates" concern for ~98% of walls.
construction_age_band is therefore a low-stakes fallback, not the primary fabric
driver. Fabricate band M from construction_year/assessment_date (new-build era);
used only for the ~2% of walls with no parseable U and for secondary age-band logic
(cylinder-insulation default, suspended-timber-floor sealed/unsealed branch, Table 5 fan
default). Fail loud (UnmappedApiCode) only if neither a year nor a usable band can
be established. (Note: this also shrinks the D3 living-area physics-gap framing — the
score is far more faithful than the early no-engine estimate suggested.)
Corpus validation (1,000 certs, full chain through Sap10Calculator)
Run after slices 1–7 + D5/D6 landed (from_api_response → Sap10Calculator):
- 727/1000 (72.7%) run end-to-end; 0 calculator failures — where a cert maps, it calculates cleanly (robust mapping, no engine crashes).
- Initial 273 map-failures were all one variant: code-based heating
(
main_heating_category6/10 — community heating, heat pumps) lodgingmain_heating_codeinstead of explicitheat_emitter_type/main_fuel_type. RESOLVED (slice D6b): mappedmain_heating_code → sap_main_heating_code(a known variant the calculator already consumes), defaulting the absent explicit fields. Corpus mappability now 1000/1000 (100%); calc 1000/1000. - Score divergence (calc − lodged, full mapping, n=1000): median −8, mean −11.4; within ±10 = 55%. The engine systematically under-rates full-SAP certs. This is deferred to a separate SAP-calc verification task (mapping fidelity is the deliverable here; the score is not). Known drivers, in rough order: (a) measured MEV ventilation priced as added extract loss (−4 isolated on the sample); (b) code-based heating (276 certs) currently defaults the absent fuel/emitter to 0 — these systems map (no crash) but are not yet fully modelled, so their scores are unreliable by mapping, not calc; (c) fabricated RdSAP proxies (age band, back-solved room count). The mapper is never tuned to the lodged value; pins are the observed score.
Open / not yet grilled
- D5
ventilation_type(1/5/6/8…) →MechanicalVentilationKind(NATURAL/MEV/MVHR/PIV);air_permeability→ AP4 - D6 heating: full-SAP
sap_heating.main_heating_detailsshape vs RdSAP field names - D7 wall/roof/floor
constructioncode mapping (full-SAPwall_type→ domainwall_constructionfor U-derivation) - D8
_clear_basement_flag_when_system_builtapplicability - D9 dwelling_type/built_form/property_type/tenure code mapping; 11/1000 null
built_form - D10 schema dataclass optionality (corpus presence:
sap_flat_details44%,design_water_use94%, etc.)