Model/scripts/hyde/perimeter_decision.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.6 KiB
Raw Blame History

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_mworksheet/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) p10p90 off by >30%
flats (n=279) 0.94 0.791.10 5% over / 5% under
houses (n=564) 0.96 0.881.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:

  1. Trust wall_type only — simplest, ~1% of certs slightly over-count exposed area (conservative: under-rates a few flats). Recommended for v1.
  2. 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".
  3. 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:3102 etc.). 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.