- 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.6 KiB
Decision brief: deriving heat-loss perimeter for full-SAP 17.1
For: mapper design sign-off · Date: 2026-06-15 · Corpus: 1,000 real SAP-Schema-17.1 certs (backend/epc_api/json_samples/SAP-Schema-17.1/corpus.jsonl)
TL;DR — recommendation
Derive perimeter and party-wall length from the measured per-wall areas full-SAP lodges, classified by wall_type:
wall_type |
meaning | feeds | engine effect |
|---|---|---|---|
| 1, 2, 3 | external / exposed | heat_loss_perimeter_m = Σarea ÷ storey-height |
wall + ground-floor heat loss |
| 4 | party | party_wall_length_m = Σarea ÷ storey-height |
party-wall heat loss (≈0 to heated neighbour) |
| 5 | internal partition | discarded | none (not part of the envelope) |
Geometric 4·√(footprint) is the fallback only (a part lodges no walls / no storey height).
Why this and not geometric: wall_type is a ~99%-clean classifier (evidence below), and for attached dwellings (flats + semis + terraces = the majority) the measured areas separate exposed from party, which geometric cannot — geometric assumes 4 exposed sides and over-states heat loss for anything sharing a wall.
Why this is the #1 decision
The RdSAP engine reconstructs the entire envelope from perimeter:
- gross wall area =
Σ(heat_loss_perimeter × storey_height)—worksheet/dimensions.py:124 - ground-floor heat loss =
f(area, perimeter)—rdsap/cert_to_inputs.py:4420 - party walls billed separately via
party_wall_length_m—worksheet/dimensions.py:168
The existing RdSAP path reads both straight from the cert (mapper.py:3556-3557). RdSAP certs lodge them; full-SAP certs lodge neither. So full-SAP must derive them, or the engine sees a wall-less dwelling → heat loss ≈ 0 → nonsense (band-A) score.
Evidence 1 — wall_type cleanly classifies exposed / party / internal
Across all walls in the 1,000-cert corpus:
wall_type |
n | names containing party/common/etc | reading |
|---|---|---|---|
| 1 | 5 | 0% | external (basement) |
| 2 | 1,657 | 1% | external (external wall 1/2/3, wall 1) |
| 3 | 109 | 1% | external |
| 4 | 738 | 81% | party (party wall 0, party, common) |
| 5 | 588 | 0% | internal partition (internal wall, stud, block) |
Type 4 → party and type 5 → internal are unambiguous; types 1/2/3 → external leak party names only ~1% of the time.
Evidence 2 — total footprint is recoverable two independent ways
Geometric 4·√(footprint)·height vs the measured wall-area footprint agree well, so the total envelope size is low-risk; the only thing that needs the wall_type split is which part of it is exposed vs party:
| stock | geometric ÷ measured (median) | p10–p90 | off by >30% |
|---|---|---|---|
| flats (n=279) | 0.94 | 0.79–1.10 | 5% over / 5% under |
| houses (n=564) | 0.96 | 0.88–1.02 | 3% over / 2% under |
Worked examples — Method A (geometric) vs Method B (measured areas) vs reality
Reality = the measured external-wall area the cert actually lodges (what we want the engine's perimeter × height to reproduce).
House — Semi-detached, 69 m², 2 storeys, height 2.44 m
| wall (name / type) | area m² | class |
|---|---|---|
| External Wall 1 (t2) | 83.4 | exposed |
| Party Wall 0 (t4) | 37.3 | party |
| 3× Internal Wall (t5) | 155.4 | internal |
- Reality (exposed only): 83.4 m²
- Method A geometric: 23.5 m perim → implied wall 114.4 m² ❌ (+37%: counts the party side as exposed)
- Method B
wall_type: exposed 83.4 ÷ 2.44 → 17.1 m/storey, implied wall 83.4 m² ✅; party 37.3 →party_wall_length✅
Flat — Ground-floor flat, 73 m², 1 storey, height 2.38 m
| wall (name / type) | area m² | class |
|---|---|---|
| walls (t2) | 50.9 | exposed |
| Common wall (t2) | 19.8 | exposed* |
| Part wall (t4) | 17.0 | party |
- Method A geometric: 34.2 m → implied wall 81.3 m²
- Method B
wall_type: exposed 70.7 ÷ 2.38 → 29.7 m, party 17.0 → party length - Caveat: "Common wall" is a party wall mis-typed as 2 — the ~1% leakage case. Method B treats it as exposed (slightly over-counts); a name-hint refinement could catch it, at the cost of fragility.
Sample cert 0862-…-2325 (lodged 83), Ground-floor flat, 68 m², height 2.4 m
| wall (name / type) | area m² | class |
|---|---|---|
| Brickwork (t2) | 13.1 | exposed |
| Weatherboarding (t2) | 47.0 | exposed |
| Sole Plate Detail (t2) | 3.1 | exposed* (junction detail) |
| Stair Wall (t2) | 28.4 | exposed* (likely party, mis-typed) |
| Stud Walls (t5) | 126.0 | internal |
- Method B
wall_type: exposed 91.5 ÷ 2.4 → 38.1 m. If "Stair Wall" is really party, true exposed ≈ 63 m² → 26 m. This single cert is one of the messy ~1%.
The residual risk (and the open sub-question for you)
wall_type mis-labels a party wall as external ~1% of the time (the "Common wall" / "Stair Wall" cases). Options for that tail:
- Trust
wall_typeonly — simplest, ~1% of certs slightly over-count exposed area (conservative: under-rates a few flats). Recommended for v1. wall_type+ name regex (party|common|separating) to reclassify type-2 party walls — catches the tail, but free-text names are fragile and could mis-grab a genuinely-external "common entrance wall".- Geometric for houses, measured for flats — more code paths; the data shows measured wins for both, so this adds complexity without clear gain.
My pick: option 1 for v1 (trust wall_type), revisit with name hints only if the score comparison later flags the affected certs.
DECISION (2026-06-15) — trust wall_type, fail loudly
Resolved: Option 1. Classify by wall_type only:
{1, 2, 3}→ exposed (→heat_loss_perimeter_m)4→ party (→party_wall_length_m)5→ internal partition (discarded)- any other code →
raise UnmappedApiCode("wall_type", code)— the established strict-raise pattern (mapper.py:3102etc.). No silent default.
We deliberately accept the ~1% tail where a party wall is mis-typed as 2 (Common wall / Stair Wall) — it is not caught by a name heuristic in v1. The loud raise is reserved for genuinely unknown codes so new wall variants force an explicit decision rather than mis-routing.
To revisit (the "go back to it" marker): if the later expert score comparison flags certs with party walls mis-typed as 2, add the name-regex refinement (option 2). An ADR (docs/adr/) records this when the mapper lands. The exposed/party split is the load-bearing call; the ADR will cite this brief.