mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
- 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>
122 lines
6.6 KiB
Markdown
122 lines
6.6 KiB
Markdown
# 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:
|
||
|
||
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.
|