Model/docs/adr/0023-lighting-eligibility-and-overlay.md
Khalim Conn-Kowlessar b460f81233 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>
2026-06-05 12:02:40 +00:00

5.8 KiB
Raw Blame History

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):

@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.