5.9 KiB
Landlord-Override EPC overlay (epc_with_overlay)
When a Property has a lodged EPC and resolved Landlord Overrides, the Effective EPC is the lodged EPC with those overrides applied as a simulation, so the SAP10 calculator scores a picture that reflects what the landlord knows beyond the cert (typically before a survey). This records how that overlay works and refines ADR-0031 decision 3, which deferred it ("Landlord Overrides overlay is a later slice"). Terms in CONTEXT.md (Effective EPC, Landlord Overrides, Rebaselining, Validation Cohort).
Status
Accepted (design). Refines ADR-0031 dec-3. Implementation: tracer slice
(wall_type) first, coverage expanded per component.
Decisions
1. A Landlord Override is a Simulation Overlay, folded onto the lodged EPC
The overlay reuses the existing EpcSimulation / overlay_applicator machinery
that every modelling Measure already uses to mutate an EpcPropertyData
(domain/modelling/simulation.py). "The landlord says the wall is now insulated"
and "a Measure insulates the wall" become one code path: a resolved override
maps to an EpcSimulation, and effective_epc's epc_with_overlay branch folds
it onto the lodged epc. CONTEXT.md already frames overrides as "a simulation on
the EpcPropertyData" — this makes that literal. Rejected: bespoke field-writing
onto a copied EpcPropertyData, which would duplicate the fold and invent a
second way to mutate an EPC.
2. The Property's override slot holds domain overlays; the repository maps to them
Property gains a landlord-overrides slot typed as domain EpcSimulations,
not the repository's ResolvedPropertyOverrides (which would make domain/
import repositories/ and invert the dependency). PropertyPostgresRepository
hydrates the slot by reading the faithful value-space snapshot
(PropertyOverridesReader) and mapping each override value → an EpcSimulation —
the same place it already hydrates epc / predicted_epc.
3. The fold is partial and per-component
property_overrides is partial — only the components a portfolio actually
supplied are present (a Property may carry property_type + wall_type but no
roof_type). The overlay applies only the overlays derived from rows that
exist; every other field stays lodged-as-is. This falls out of the faithful
reader, which returns only the rows present.
4. WallType / RoofType decompose into SAP int codes — the calculator reads codes, not descriptions
The SAP calculator derives the wall U-value from the wall_construction and
wall_insulation_type int codes, never from walls[].description
(cert_to_inputs.py). So a wall_type override only moves the score once
translated into those codes. The translation is tractable because a WallType
value is material × insulation state: material → wall_construction (codes 1-5)
and state → wall_insulation_type (1 external, 3 internal, 4 as-built, 7 filled),
the code sets already documented in solid_wall_recommendation.py. The override's
building_part maps directly to the overlay's BuildingPartIdentifier
(0 → MAIN, 1 → EXTENSION_1, …). RoofType resolves only for the explicit
"Pitched, N mm loft insulation" family → roof_insulation_thickness; roofs
with no clean loft depth (flat, room-in-roof, "another premises above") produce
no overlay.
property_type / built_form_type are whole-dwelling categorical
corrections, not building-part fabric, so they set the top-level
EpcSimulation.property_type / built_form (alongside the existing
whole-dwelling lighting/heating overlays), folded by apply_simulations. They
are written as the landlord text value ("House", "Park home", "Semi-Detached")
— property_type consumers tolerate text and the calculator's park-home check is
text-only. Correction to the first draft of this decision: property_type is
not metadata — it drives party-wall heat loss (heat_transmission.py
_is_flat_or_maisonette) and ASHP/solar/wall eligibility, so a correction
moves both the SAP score and the measure menu. built_form has no calculator
consumer today (it feeds the ML transform + reporting), so its overlay is
currently inert at the SAP layer but kept for picture-completeness.
The "(assumed) insulated" / "partial insulation (assumed)" WallType states
are deferred: 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.
5. An overlaid Property is excluded from the Validation Cohort — on divergence, not source path
Producing a more accurate score than the cert is the point: the calculated
Effective Performance deliberately diverges from the Lodged Performance (which is
preserved). That divergence means an overlaid Property is no longer a clean
calc(effective_epc)-vs-lodged control, so it is excluded from the Validation
Cohort — exactly as a predicted-source Property is. The exclusion signal is
divergence ("≥1 override was applied", the overlay slot is non-empty), not
source_path: epc_with_overlay also covers plain-EPC Properties with no
overrides, which are valid validation targets. This reuses the same divergence
signal CONTEXT.md already names as a Rebaselining trigger. No Validation-Cohort
code path exists today, so this is a recorded rule, not code in this slice.
Consequences
EpcPropertyDatais untouched — provenance stays structural on the Property (ADR-0031 dec-3), and every downstream consumer is oblivious to the overlay.- The override→
EpcSimulationmapping is the real deliverable and grows per component;wall_typeis the tracer (highest SAP impact, existing code-set prior art).roof_typeand fullWallTypecoverage follow. - Precedence is unchanged (CONTEXT.md): the overlay lives entirely inside the
epc_with_overlaybranch and never applies when Site Notes are the source.