docs: lighting eligibility, overlay + pricing design (ADR-0023 + CONTEXT)

Resolved in a grill-with-docs pass. recommend_lighting converts ALL non-LED
bulbs (incandescent + CFL + low-energy-unknown) to LED — all the way to LED, not
the legacy "fill to low energy", because SAP §12-1 rates LED efficacy (100)
above LEL (80) / CFL (55). A free Optimiser candidate (it improves SAP), unlike
ventilation's forced dependency. Its overlay is the first whole-dwelling,
top-level surface: a LightingOverlay carrying the four bulb-count fields by
their exact EPC names, folded directly onto EpcPropertyData (led=total, others
0). Priced per-bulb x non-LED count, contingency 0.26, measure_type
low_energy_lighting (MEASURE_MAP-aligned; "LED" in the description). Validation:
real before/after cascade pins (zero-existing-LEDs + some-existing-LEDs) at 1e-4,
clean (no fabric coupling). Ground-truth confirmed: 20 incandescent -> 20 LED
drops lighting (232) 783.7 -> 232.7 kWh/yr.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-05 12:02:40 +00:00
parent 07dbaa5361
commit b460f81233
2 changed files with 46 additions and 0 deletions

View file

@ -250,6 +250,10 @@ _Avoid_: "roof insulation" (name the specific Measure — loft / sloping-ceiling
The rule fixing the single glazing Measure the **Windows** Recommendation offers. We upgrade **only single-glazed windows** (a pragmatic scope — already-double/secondary/triple windows are left alone), **all of them together** as one Measure. Planning status **hard-picks** the Measure (not a choice the Optimiser makes — ADR-0022): unrestricted → **double glazing** (replace the units); a **conservation area** / **listed** / **heritage** building → **secondary glazing** (an internal second pane, since the external units can't be replaced). Priced at a flat **average price per window** × the count of single-glazed windows (we have per-window areas but no size-varying prices, so size is ignored). When the dwelling has no single-glazed windows, no Recommendation is offered.
_Avoid_: "windows" as a Measure (name **double glazing** / **secondary glazing**); pricing glazing by area (it's per-window count × average)
**Lighting Eligibility**:
The rule fixing the single lighting Measure the **Lighting** Recommendation offers. We convert **all non-LED bulbs** (incandescent + CFL + low-energy-unknown) to **LED** — all the way to LED, not the legacy "fill to low energy", because SAP rates LED efficacy above CFL (ADR-0023). One Measure, no planning gate (lighting isn't planning-restricted). Offered only when the dwelling lodges at least one non-LED bulb; a dwelling already all-LED, or one that lodged **no** bulb counts (nothing to size against), gets no Recommendation. Unlike the fabric measures it is a **whole-dwelling** Measure — its **Simulation Overlay** writes the four top-level bulb counts directly (`led = total`, others 0), the first overlay surface that isn't a building part / window / system sub-object. Priced at a flat **average price per bulb** × the count of non-LED bulbs replaced. A free Optimiser candidate (it *improves* SAP), contrast the forced ventilation **Measure Dependency**.
_Avoid_: "low energy lighting" as the upgrade target (we go to **LED**); treating it as a forced dependency (it is a free candidate); pricing by floor area (it's per-bulb count × average)
### Valuation
**Property Valuation**:

View file

@ -0,0 +1,42 @@
# Lighting Eligibility, Overlay, and Pricing
The lighting Recommendation Generator offers an LED upgrade. Three decisions are load-bearing and non-obvious: that it converts **all** non-LED bulbs to LED (not the legacy "fill to low-energy"), that its Simulation Overlay is the first **whole-dwelling, top-level** surface, and that it is a **free Optimiser candidate** rather than a forced dependency like ventilation.
## Decision
**One dispatching generator, one Measure, all non-LED bulbs to LED.** `recommend_lighting(epc, products)` emits one "Lighting" Recommendation with a **single** Measure Option:
- **Scope:** the dwelling's four lodged fixed-lighting bulb counts — `led_`, `cfl_`, `incandescent_`, `low_energy_fixed_lighting_bulbs_count` (the last is RdSAP "low energy, LED/CFL unknown", LEL). The Measure converts **every non-LED bulb** (incandescent + CFL + LEL) to **LED**. Trigger: `incandescent + cfl + low_energy_unknown > 0`. When that sum is zero — already all-LED, **or** no bulb counts lodged (the calculator's L5b/L8c fallback case, where we have no inventory to size against) — no Recommendation is offered.
- **All the way to LED, not "low energy".** SAP 10.2 RdSAP §12-1 rates lamp efficacy LED **100** > LEL **80** > CFL **55** > incandescent **11.2** lm/W, so converting *every* non-LED type — including CFL and LEL — strictly improves efficacy and lighting energy (Appendix L, worksheet line (232)). The legacy generator only filled outlets to "low energy" and treated CFL as already-efficient; we go to LED for the larger, honest SAP gain.
- **Free Optimiser candidate**, run in `_candidate_recommendations` alongside the fabric measures — **not** a forced Measure Dependency. Ventilation is forced precisely because it only ever *costs* SAP; an LED upgrade *improves* SAP at low cost, so the Optimiser should be free to keep or leave it for least-cost-to-target. (Validated on the real cert: 20 incandescent → 20 LED drops lighting (232) from 783.7 → 232.7 kWh/yr.)
**The overlay is the first whole-dwelling, top-level surface.** Unlike walls/roofs/floors (per `SapBuildingPart`) or glazing (per `sap_windows` index), the bulb counts live **top-level on `EpcPropertyData`**. `EpcSimulation` gains `lighting: Optional[LightingOverlay]` (the 4th overlay surface, after building parts, windows, ventilation):
```python
@dataclass(frozen=True)
class LightingOverlay: # all-optional partial; counts are absolute
led_fixed_lighting_bulbs_count: Optional[int] = None
cfl_fixed_lighting_bulbs_count: Optional[int] = None
incandescent_fixed_lighting_bulbs_count: Optional[int] = None
low_energy_fixed_lighting_bulbs_count: Optional[int] = None
```
The field names match the EPC exactly so the applicator folds by `setattr` (as `_fold_ventilation` does), writing **directly onto the result `EpcPropertyData`** — no nested object, simpler than ventilation. The counts are **absolute target states** (like `wall_insulation_thickness=100`): the generator reads the baseline, sums `total`, and emits `led=total, cfl=0, incandescent=0, low_energy=0`. This handles both scenarios uniformly — "zero existing LEDs" (`led 0→total`) and "some existing LEDs" (`led tops up to total`).
**Pricing: flat per-bulb price × count of non-LED bulbs.** `Cost.total = (incandescent + cfl + low_energy_unknown) × product.unit_cost_per_m2`, reusing that field as a per-bulb price exactly as glazing reuses it per-window and ventilation per-unit; `contingency_rate = 0.26` (the legacy `low_energy_lighting` rate). LEL bulbs are priced too, since they're converted.
**`measure_type = "low_energy_lighting"`.** Although the Measure installs LED specifically, `measure_type` is a cross-cutting catalogue classification that `MEASURE_MAP` / `Funding` and reporting key on, so we keep the established legacy name and put "LED" in the Option **description** and the all-LED overlay — not in a new `led_lighting` type the rest of the system wouldn't recognise.
## Considered options
- **Fill to 100% "low energy" (legacy behaviour), leaving CFL as-is.** Rejected: the calculator rates LED above CFL (100 vs 55 lm/W), so going all-LED is a strictly larger, truthful SAP gain — and the real Elmhurst after-cert lodges LED, not a CFL/LEL mix.
- **A forced Measure Dependency (like ventilation).** Rejected: lighting *improves* SAP, so there is a genuine cost/benefit choice for the Optimiser — a free candidate is honest; a forced injection is not.
- **`measure_type = "led_lighting"`.** Rejected: precise but divergent — `MEASURE_MAP`/`Funding` and legacy reporting key on `low_energy_lighting`; the precision lives in the description instead.
- **A leaner overlay (single `all_led` flag the applicator expands).** Rejected: the four absolute counts mirror the EPC by name, keep the generic by-name fold, and stay greppable — consistent with every other overlay carrying real EPC fields.
## Consequences
- `EpcSimulation` grows its first **whole-dwelling, top-level** overlay surface (`lighting`) — the applicator's fold writes onto the `EpcPropertyData` directly rather than a nested object or a targeted part/window.
- Lighting is wired into the free candidate pool (`_candidate_recommendations`), priced in the catalogue and contingency table under `low_energy_lighting`, and added to the `_GENERATOR_MEASURE_TYPES` forcing test.
- Validation uses real Elmhurst before/after certs (two scenarios: "zero existing LEDs" and "some existing LEDs") as 1e-4 cascade pins. Lighting changes only bulb counts → Appendix L (232), with **no fabric coupling** (contrast glazing's draught-proofing/frame-factor), so the pins are expected to close cleanly with no xfail.
- A genuinely all-incandescent dwelling that lodged **zero** bulb counts (a data gap) gets no Recommendation — a data-completeness limitation, not a modelling choice, since we refuse to fabricate an outlet count.