Model/scripts/hyde/mapping_decisions.md
Jun-te Kim 2f0eb49eee Checkpoint: UPRN 10093116543 Elmhurst build + devcontainer VNC/Playwright + perms
- 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>
2026-06-16 15:21:56 +00:00

6.8 KiB
Raw Blame History

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_height
  • 4 party → party_wall_length_m = Σarea ÷ storey_height
  • 5 internal 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_openingssap_opening_types by name (100% join in corpus), then route by opening-type .type:

  • 4 vertical window → sap_windows (SapWindow: width, height, u_valuewindow_transmission_details, orientation, glazing_type, frame_factor)
  • 5 roof window / rooflight → sap_roof_windows (SapRoofWindow: area=w×h, u_value, orientation, pitch, frame_factor, g⊥←solar_transmittance)
  • 1,2,3 doors → 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 14) 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 17 + D5/D6 landed (from_api_responseSap10Calculator):

  • 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_category 6/10 — community heating, heat pumps) lodging main_heating_code instead of explicit heat_emitter_type/main_fuel_type. RESOLVED (slice D6b): mapped main_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_details shape vs RdSAP field names
  • D7 wall/roof/floor construction code mapping (full-SAP wall_type → domain wall_construction for U-derivation)
  • D8 _clear_basement_flag_when_system_built applicability
  • D9 dwelling_type/built_form/property_type/tenure code mapping; 11/1000 null built_form
  • D10 schema dataclass optionality (corpus presence: sap_flat_details 44%, design_water_use 94%, etc.)