mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Document pitched sloping-ceiling roof-type family (ADR-0066 + glossary)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f1e190293b
commit
fc738fa843
2 changed files with 143 additions and 0 deletions
|
|
@ -155,6 +155,10 @@ _Avoid_: "the LLM mapper is unreliable" (the failure mode is a too-small target
|
|||
The RdSAP string sentinels a cert may lodge in place of a numeric value (e.g. `roof_insulation_thickness`). They are data, not noise, and are **not interchangeable**: **`ND`** (Not Defined) — no data; resolve to the construction age band's **as-built** state (roof as-built: A–D uninsulated, E–F limited insulation, G+ insulated). **`NI`** — insulation **present** but thickness unknown (RdSAP 10 §5.11.4: score as 50 mm); never "no insulation". **`AB`** — As Built; resolve by age band like ND. The mapper deliberately passes roof sentinels through raw — the calculator's resolution depends on distinguishing them (ADR-0047). Scoring and eligibility may legitimately resolve the same sentinel differently (`NI`: 50 mm to the U-cascade, not-eligible to the roof generator).
|
||||
_Avoid_: reading `ND` as "no insulation"; reading `NI` as "no insulation" (it means the opposite); normalising sentinels at the mapper boundary; comparing a sentinel string against an int (`"ND" != 0` is always True)
|
||||
|
||||
**Pitched-Roof Insulation Geometry (loft / at-rafters / sloping ceiling)**:
|
||||
Three *distinct* RdSAP constructions a pitched roof can carry, separated by `roof_insulation_location` and scored on different U-tables — **never interchangeable**. **Loft (joists)** — `roof_insulation_location = 2`, insulation lying on the ceiling joists over a loft void; scores on Table 16 **col (1)** by thickness, Table 18 **col (1)** (`_ROOF_BY_AGE`, floored at 0.40) by age band; the `Pitched, N mm loft insulation` **RoofType** family. **At-rafters** — `roof_insulation_location = 1`, insulation on the sloping underside of the rafters over a loft; a *shallower* cavity, so Table 16 **col (2)** (200 mm → 0.29 vs the joists' 0.21) and the Table 18 rafters column; the `Pitched, insulated at rafters` member (gov-API/Summary `roof_construction` still pitched, not code 8). **Sloping ceiling** — gov-API `roof_construction = 8` (`roof_insulation_location = 7`), the ceiling *follows the slope* so there is **no loft void**: by depth it takes Table 17 **col (1a)** "insulated slope" (byte-identical to col (1) — 150 mm → 0.30 for both, Elmhurst-confirmed), and **as built / unknown** it takes Table 18 **col (3)** (`_FLAT_ROOF_BY_AGE`: 2.30 A–D, 1.50 E, 0.68 F — *not* the loft col (1) 0.40 floor); the `Pitched, sloping ceiling, …` **RoofType** family (ADR-0066). The calculator keys the sloping path on the substring `"sloping ceiling"` (or `"vaulted"`) in `roof_construction_type` (`is_pitched_sloping_ceiling`), and both the SAP10 U-cascade and `recommend_roof_insulation` read that one string — so an overlay that means "sloping ceiling" **must** set the construction string, not just a thickness. The insulation Measure differs too: loft/at-rafters → loft top-up (joists, 300 mm) or `sloping_ceiling_insulation` respectively; a sloping ceiling → `sloping_ceiling_insulation` (rafters, 100 mm).
|
||||
_Avoid_: "pitched with sloping ceiling is a room-in-roof" (it is a slope-following ceiling, `roof_construction` 8 — a habitable **Roof room(s)** is a separate construction); collapsing a sloping ceiling onto the `Pitched, N mm loft insulation` family (drops the slope shape → col (1) 0.40 floor at old bands + a loft-top-up recommendation); conflating **at-rafters** (col 2) with **sloping ceiling** (col 3 / col 1a); setting `roof_insulation_thickness` without `roof_construction_type` when the geometry is not a plain loft
|
||||
|
||||
### Modelling
|
||||
|
||||
**Effective EPC**:
|
||||
|
|
|
|||
139
docs/adr/0066-pitched-sloping-ceiling-roof-type-family.md
Normal file
139
docs/adr/0066-pitched-sloping-ceiling-roof-type-family.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# Pitched Sloping-Ceiling Gets Its Own RoofType Family, Modelled at the Slope Line (distinct from loft and from at-rafters)
|
||||
|
||||
A `PitchedWithSlopingCeiling` source roof (gov-API `roof_construction` code 8 —
|
||||
its ceiling follows the slope, so it has no loft void) was being classified by
|
||||
the **Landlord-Description Classification** onto the `Pitched, N mm loft
|
||||
insulation` family. That family's Simulation Overlay sets only
|
||||
`roof_insulation_thickness` and leaves `roof_construction_type` untouched, so
|
||||
the **Effective EPC**'s roof reads as a horizontal loft. Two consequences, both
|
||||
wrong for a slope-following ceiling:
|
||||
|
||||
- **Scoring** — with no `"sloping ceiling"` in `roof_construction_type`, the
|
||||
SAP10 Calculation's `is_pitched_sloping_ceiling` flag stays False, so an
|
||||
as-built / unknown roof takes the Table 18 **column (1)** age-band default
|
||||
(`_ROOF_BY_AGE`, floored at 0.40 for bands A–G) instead of the **column (3)**
|
||||
default (`_FLAT_ROOF_BY_AGE`: 2.30 A–D, 1.50 E, 0.68 F) that a slope-following
|
||||
ceiling requires — a 3–5.75× understatement of roof heat loss at old bands.
|
||||
It also loses the RdSAP 10 §3.8 inclined-area correction (÷cos 30°).
|
||||
- **Recommendation** — `recommend_roof_insulation` dispatches on the same
|
||||
`roof_construction_type` substring, so a roof mis-labelled as loft is offered
|
||||
a loft top-up (joists, 300 mm) rather than `sloping_ceiling_insulation`
|
||||
(rafters, 100 mm) — a measure that can't physically be installed the way the
|
||||
overlay implies.
|
||||
|
||||
The app-side enum + Drizzle migration (assessment-model#449, migration 0278)
|
||||
already ships the canonical values; this repo is the lock-step source of truth
|
||||
(ADR-0002) and must add them byte-for-byte.
|
||||
|
||||
## Decision
|
||||
|
||||
**Add a dedicated `Pitched, sloping ceiling, …` family to `RoofType`** — an
|
||||
`as built` member plus a `12 mm … 400+ mm insulation` depth ladder (17 members
|
||||
total), matching assessment-model#449 byte-for-byte. **Extends ADR-0032/0033;
|
||||
the calculator changes in none** — it already models sloping ceilings; the fix
|
||||
is to make the classifier and overlay *reach* that path.
|
||||
|
||||
**1. The overlay sets the sloping-ceiling construction string; the calculator
|
||||
does the rest.** `roof_overlay_for` resolves the new family to a
|
||||
`BuildingPartOverlay(roof_construction_type="Pitched, sloping ceiling", …)`,
|
||||
which folds onto the Effective EPC's building part and drives both the scorer
|
||||
and the recommendation generator off one authoritative string:
|
||||
|
||||
| Member | Overlay | SAP10 U-value resolution |
|
||||
|---|---|---|
|
||||
| `Pitched, sloping ceiling, as built` | construction `"Pitched, sloping ceiling"`, thickness `None` | Table 18 **col (3)** age-band default via `is_pitched_sloping_ceiling` (`_FLAT_ROOF_BY_AGE`) |
|
||||
| `Pitched, sloping ceiling, N mm insulation` | construction `"Pitched, sloping ceiling"` + `roof_insulation_thickness = N` | Table 16 col (1) / Table 17 **col (1a)** "insulated slope" thickness ladder (`_ROOF_BY_THICKNESS`); `400+` → 400 |
|
||||
|
||||
**The mm ladder scoring on col (1a) is correct, not a loft fallback.** RdSAP 10
|
||||
Table 17 col (1a) ("insulated slope – sloping ceiling, mineral wool/EPS") is
|
||||
byte-identical to the Table 16 col (1) loft ladder — 100 mm → 0.40, 150 mm →
|
||||
0.30, 200 mm → 0.21 — confirmed against accredited Elmhurst (age B + 150 mm reads
|
||||
0.30 for both geometries). The slope-vs-loft distinction bites **only at the
|
||||
age-band default** (unknown thickness), where the family diverges correctly to
|
||||
col (3). Setting the explicit construction string guarantees the resolution is a
|
||||
*deliberate* col-(1a) match, never an *accidental* loft-ladder fallthrough. This
|
||||
reproduces exactly how a lodged code-8 cert already scores
|
||||
(`_api_resolve_sloping_ceiling_thickness`), so a landlord-override sloping
|
||||
ceiling scores identically to a lodged one of the same geometry and depth.
|
||||
|
||||
**2. `as built` is sufficient at the no-depth end — no separate `Unknown` /
|
||||
`no insulation` member.** `Unknown` depth folds to `as built` (mirrors #449).
|
||||
This is both forced (a distinct member breaks byte-for-byte lock-step) and
|
||||
correct: `as built` defers to the construction age band, which is itself
|
||||
uninsulated (2.30) at pre-1950 bands A–D, 1.50 at E, 0.68 at F — so "as built"
|
||||
already collapses to "uninsulated" precisely where an old slope-following
|
||||
ceiling is. Mirrors the `…, as built` wall proposal (issue #1676 B7): the age
|
||||
band drives the insulation assumption rather than a hard-coded label.
|
||||
|
||||
**3. `Pitched, insulated at rafters` is kept distinct — this family does not
|
||||
subsume it.** RdSAP separates the two by `roof_insulation_location`: **loc 1 =
|
||||
insulation at rafters** (Table 16 **col (2)**: 200 mm → 0.29) versus **loc 7 =
|
||||
sloping ceiling** (col (3) / Table 17 col (1a): 200 mm → 0.21). Different
|
||||
constructions, different U-tables — folding them together would mis-score one to
|
||||
the other. (That `Pitched, insulated at rafters` currently produces no overlay
|
||||
is a pre-existing gap, out of scope here.)
|
||||
|
||||
**4. A deterministic classifier guard, not the LLM alone, routes the family.**
|
||||
`PitchedWithSlopingCeiling: <As Built | Unknown | N mm>` resolves via a
|
||||
`roof_sloping_ceiling_guard` (mirroring the #1376 `roof_party_ceiling_guard`),
|
||||
composed into the roof `GuardedColumnClassifier` ahead of the
|
||||
`ChatGptColumnClassifier` fallback. `As Built` / `Unknown` / empty → `as built`;
|
||||
`N mm` → the ladder member (nearest tabulated depth ≤ N; ≥ 400 → `400+`). This
|
||||
is the same defect class the party-ceiling guard was created to kill (#1376):
|
||||
the LLM resolved *identical* roof inputs inconsistently, and adding 17
|
||||
near-duplicate `sloping ceiling` members beside the loft ladder is exactly the
|
||||
ambiguity that produces that split. The guard makes the knowable, costly-if-wrong
|
||||
mapping deterministic and unbilled; its output still lands in the
|
||||
`source=classifier` reviewed cache, so the human keeps the final say.
|
||||
|
||||
## Considered options
|
||||
|
||||
- **Route the mm ladder to the Table 16 col (2) rafters ladder** (200 mm →
|
||||
0.29, the steeper "insulation at the slope" reading). Rejected: it requires a
|
||||
calculator change (the `use_rafters` gate deliberately excludes sloping
|
||||
ceilings) and a new overlay field, and it would make a landlord-override
|
||||
sloping ceiling score *differently* from an identical lodged code-8 cert
|
||||
(which routes to col (1a) today) — an override-vs-lodged divergence for zero
|
||||
accuracy gain, since Elmhurst confirms col (1a) is the accredited value.
|
||||
- **Fold `PitchedWithSlopingCeiling` into `Pitched, insulated at rafters` and
|
||||
give that member a depth ladder.** Rejected: conflates RdSAP loc 7 with loc 1
|
||||
(col (3)/(1a) with col (2)), and #449 already shipped a *separate* named
|
||||
family — reusing the rafters member would break lock-step.
|
||||
- **Add a distinct `Unknown` / `no insulation` sloping-ceiling member.**
|
||||
Rejected: breaks byte-for-byte parity with #449, and the age band already
|
||||
resolves the no-depth end to the correct (uninsulated-at-old-bands) U.
|
||||
- **Add the enum members and rely on the LLM classifier alone** (no guard).
|
||||
Rejected: does not satisfy the issue's explicit "stop routing these onto
|
||||
`Pitched, N mm loft insulation`" — the near-duplicate loft members are the
|
||||
exact ambiguity #1376 fixed with a guard; a probabilistic route across 294
|
||||
costly as-built/Unknown rows is not equivalent to a deterministic one.
|
||||
- **Guard + roof-classifier `extra_instructions` + a loft-direction eval.**
|
||||
Rejected as over-scoped: the loft-direction risk (LLM over-picking a sloping
|
||||
category for a loft description) is a review-surface concern, and CONTEXT.md
|
||||
already frames `source=classifier` as a human-reviewed cache — the backstop
|
||||
exists. Revisit if review flags real loft→sloping drift.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The 311 portfolio-796 rows (`As Built` 202, `Unknown` 92, `150 mm` 16,
|
||||
`100 mm` 1) currently on `Pitched, … loft insulation` are backfilled onto the
|
||||
new family **after** this lands and #449 is live (same dry-run → commit flow
|
||||
as the earlier 796 corrections). The 294 as-built/Unknown rows change U-value
|
||||
(col (1) → col (3) at old bands); the 17 mm-ladder rows keep their U (col (1a)
|
||||
≡ col (1)) but gain the correct sloping-ceiling shape (recommendation +
|
||||
inclined area).
|
||||
- The roof override becomes **authoritative for both scoring and
|
||||
recommendation**: an EPC-lodged loft overridden to sloping now dispatches
|
||||
`sloping_ceiling_insulation`, not a loft top-up.
|
||||
- A `Pitched, sloping ceiling, 12 mm insulation` override yields **no** roof
|
||||
Recommendation (the ADR-0021 sloping branch only fires when uninsulated —
|
||||
as-built A–D or explicit 0). This is unchanged ADR-0021 behaviour, accepted
|
||||
consciously; whether thin-but-present slopes warrant a top-up is the same
|
||||
open product question ADR-0047 left for the loft branch.
|
||||
- **Lock-step obligation (ADR-0002):** the 17 member values are the contract
|
||||
with assessment-model#449's `RoofTypeValues` / migration 0278 — any edit here
|
||||
must be mirrored there and vice versa.
|
||||
- The onboarder parity path (`EpcRoofDescriptions`,
|
||||
`backend/onboarders/mappings/parity/`) already segregates sloping ceilings via
|
||||
`map_sloping_ceiling_roof` (age band) and is **untouched** — this ADR governs
|
||||
the Landlord-Override `RoofType` path only.
|
||||
Loading…
Add table
Reference in a new issue