mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
added landlord overrdies
This commit is contained in:
parent
a3e2566378
commit
027ba6d89f
1 changed files with 136 additions and 0 deletions
136
docs/adr/0033-age-band-resolves-assumed-fabric-states.md
Normal file
136
docs/adr/0033-age-band-resolves-assumed-fabric-states.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Age band resolves the "(assumed)"/"as built"/"Unknown" fabric states
|
||||
|
||||
ADR-0032 dec-4 **deferred** the `WallType` "(assumed) insulated" / "partial
|
||||
insulation (assumed)" states (and, transitively, flat roofs): their
|
||||
`wall_insulation_type` is "age-inferred in RdSAP, so the code is ambiguous and
|
||||
must be pinned against the Elmhurst accuracy harness rather than guessed." This
|
||||
ADR closes that deferral. The harness has now run. Terms in CONTEXT.md (Landlord
|
||||
Overrides, Effective EPC, Validation Cohort); supersedes ADR-0032 dec-4.
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. Supersedes the deferral in ADR-0032 dec-4. Evidence:
|
||||
`scripts/hyde/uvalue_probe_{walls,roofs}.csv` (full A–M Elmhurst sweep, produced
|
||||
by `scripts/hyde/probe_uvalues.py`).
|
||||
|
||||
## Context: what the Elmhurst sweep showed
|
||||
|
||||
The SAP 10.2 spec defers RdSAP to "a separate document" (Appendix S, p.122), so
|
||||
the authoritative source for "what U-value does enum X at age Y produce" is the
|
||||
accredited Elmhurst RdSAP-10 entry tool itself. We drove it across all 13
|
||||
construction-age bands (A–M) for the two components Khalim flagged and read the
|
||||
tool's recomputed **Default U-value** for every option:
|
||||
|
||||
* **Cavity wall, As Built** `[A–M]`:
|
||||
`1.5, 1.5, 1.5, 1.5, 1.5, 1.0, 0.6, 0.6, 0.45, 0.35, 0.3, 0.28, 0.26`
|
||||
* **Cavity wall, Filled** `[A–M]`:
|
||||
`0.7, 0.7, 0.7, 0.7, 0.7, 0.4, 0.35, 0.35, 0.45, 0.35, 0.3, 0.28, 0.26`
|
||||
* **Flat roof, As Built / Unknown** `[A–M]`:
|
||||
`2.3, 2.3, 2.3, 2.3, 1.5, 0.68, 0.4, 0.35, 0.35, 0.25, 0.25, 0.18, 0.15`
|
||||
|
||||
Two facts fall out:
|
||||
|
||||
1. **Every value already lives in our tables.** As Built ≡ `(WALL_CAVITY, 0)`,
|
||||
Filled ≡ `_CAVITY_FILLED_ENG`, flat As Built/Unknown ≡ `_FLAT_ROOF_BY_AGE`
|
||||
(`domain/sap10_ml/rdsap_uvalues.py`) — exact match, all 13 bands. The U-value
|
||||
layer was never the gap.
|
||||
2. **"Unknown" computes identically to "As Built"** at every band — confirming
|
||||
Khalim's read that Unknown is handled like As Built.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. The "(assumed)" qualifier is *not* a distinct insulation code — age band is
|
||||
|
||||
ADR-0032 dec-4 worried that "partial insulation (assumed)" needs a
|
||||
`wall_insulation_type` code we couldn't name. The sweep shows why none exists:
|
||||
Elmhurst has **no "partial" enum**. "Partial insulation (assumed)" is simply
|
||||
*As Built (code 4) evaluated at band F (1976–1982)* → 1.0 W/m²K. Likewise
|
||||
"no insulation (assumed)" is As Built at an old band (A–E → 1.5) and "insulated
|
||||
(assumed)" is As Built at a newer band (G+ → ≤0.60). All three resolve to
|
||||
`wall_insulation_type = 4`; the **construction-age-band overlay** (already a
|
||||
separate, wired override component) supplies the U-value. This matches Khalim's
|
||||
bands: cavity ≤1975 uninsulated / 1976–82 partial / later insulated is exactly
|
||||
the As-Built-by-age curve above.
|
||||
|
||||
### 2. Resolve to int codes, not a computed U-value override
|
||||
|
||||
Because int codes + the existing age-band tables reproduce Elmhurst exactly,
|
||||
overlays keep emitting `wall_construction` / `wall_insulation_type` (walls) and
|
||||
`roof_construction` / `roof_insulation_thickness` (roofs) — no new
|
||||
`wall_u_value` / `roof_u_value` override path. This keeps the landlord overlay on
|
||||
the same code path as every Measure (ADR-0032 dec-1) and avoids a second way to
|
||||
move the wall/roof U-value. (Considered and rejected: writing a computed U-value
|
||||
override. It would duplicate the RdSAP tables already in the calculator and
|
||||
diverge the moment those tables are revised.)
|
||||
|
||||
### 3. Walls — extend the state map; no new machinery
|
||||
|
||||
`wall_type_overlay._STATE_INSULATION` gains the three "as built" variants
|
||||
("as built", "as built, insulated (assumed)", "as built, partial insulation
|
||||
(assumed)") → code 4, alongside the existing "no insulation (assumed)" → 4.
|
||||
These previously produced **no overlay** (silently dropping the landlord's
|
||||
correction). The age-band overlay does the rest.
|
||||
|
||||
### 4. Flat roofs — the overlay must set `roof_construction_type`
|
||||
|
||||
The calculator's flat-roof age-band path (`u_roof`, `_FLAT_ROOF_BY_AGE`) fires
|
||||
only when the roof is flat, which `heat_transmission.py` reads from the **string**
|
||||
`part.roof_construction_type` (`"flat" in roof_type_lower`, line 999) — *not* the
|
||||
int `roof_construction`. The roof overlay today sets only
|
||||
`roof_insulation_thickness`, so a flat-roof override can't reach that path. This
|
||||
ADR adds `roof_construction_type` to `BuildingPartOverlay` and has
|
||||
`roof_type_overlay` set it to `"Flat"` for the "Flat, …" family, alongside the
|
||||
existing `roof_insulation_thickness` (set when the description carries an explicit
|
||||
mm depth).
|
||||
|
||||
The same shape-flag mechanism extends to pitched roofs with no clean depth:
|
||||
|
||||
* `"Pitched, Unknown loft insulation"` → `roof_construction_type="Pitched"`,
|
||||
thickness left `None` → the pitched age-band default (`_ROOF_BY_AGE`).
|
||||
* `"Pitched, no insulation*"` → `roof_construction_type="Pitched"` **plus**
|
||||
`roof_insulation_thickness=0` → the uninsulated 2.30 (Table 16 row 0). Pitched
|
||||
"no insulation" text is load-bearing in `u_roof` (§5.11 note) — it does NOT take
|
||||
the age-band default, unlike flat. Setting `0` (not `None`) also lets this case
|
||||
override a lodged numeric thickness, which the "Unknown" cases cannot.
|
||||
|
||||
Constraint: the applicator only folds *non-None* overlay fields, so it cannot
|
||||
*clear* a lodged `roof_insulation_thickness` to `None`. For As Built / Unknown
|
||||
(no depth) the age-band default fires only when the baseline part carries no
|
||||
numeric thickness — true on the Hyde flow, where the landlord override IS the roof
|
||||
source and genuine As Built/Unknown lodgements carry `AB`/`ND`/None, not a number.
|
||||
A flat/pitched-Unknown override layered on a part that already has a numeric
|
||||
thickness keeps that thickness; that case is out of scope here and surfaced by the
|
||||
coverage audit (below) for the verify step.
|
||||
|
||||
## Consequences
|
||||
|
||||
* Previously-dropped Hyde rows now move the score: ~1,366 flat-roof overrides
|
||||
(`Flat: As Built` + `Flat: Unknown`) and every cavity "as built/partial/
|
||||
insulated (assumed)" wall. This is the point — a more accurate Effective EPC —
|
||||
and (per ADR-0032 dec-5) such overlaid Properties stay excluded from the
|
||||
Validation Cohort on the divergence signal.
|
||||
* `BuildingPartOverlay` gains a `roof_construction` field; `apply_simulations`
|
||||
must fold it like `wall_construction`.
|
||||
* The decision is pinned by tests mirroring the probe CSVs (one row per
|
||||
component × band), so a future RdSAP table revision that breaks parity trips a
|
||||
regression rather than silently drifting from Elmhurst.
|
||||
* `scripts/hyde/probe_uvalues.py` stays as the re-runnable oracle: re-sweep to
|
||||
re-confirm parity whenever the RdSAP tables or Elmhurst change.
|
||||
* A **coverage-audit pair** guards against the silent-no-op class this ADR fixes,
|
||||
and is the reusable gate for every future portfolio (Hyde is the template):
|
||||
- `scripts/hyde/audit_override_coverage.py` — enum-level: runs every override
|
||||
value through its overlay mapper and flags those that produce no overlay or
|
||||
only an inert field. Run it whenever an override enum or overlay changes.
|
||||
- `scripts/hyde/audit_hyde_rows.py` — row-level: weights the verdict by actual
|
||||
portfolio row counts and cross-checks that every "another/same dwelling above"
|
||||
party-ceiling no-op is genuinely a Flat/Maisonette (a House/Bungalow with a
|
||||
dwelling above is a data error, not a safe no-op — flagged to CSV for review).
|
||||
Contradictory properties (party-ceiling roof on a non-flat type — Hyde had 50,
|
||||
possibly houses split into flats) are **skipped entirely** by the `write` step:
|
||||
no overrides are written for them and their org_refs are recorded to
|
||||
`skipped_contradictory_properties.csv` for joint human review, rather than
|
||||
guessing which of the two conflicting landlord fields is correct.
|
||||
Known residue logged, not silently dropped: `PitchedWithSlopingCeiling: *`
|
||||
(~272 Hyde rows, needs the col-3 path), `Roof room(s)` (needs `sap_room_in_roof`),
|
||||
the `Renewables` column (no ComponentSpec — PV unmapped), and `Timber frame,
|
||||
with additional insulation` (wall, ~7 rows).
|
||||
Loading…
Add table
Reference in a new issue