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>
5.8 KiB
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_recommendationsalongside 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/Fundingand legacy reporting key onlow_energy_lighting; the precision lives in the description instead.- A leaner overlay (single
all_ledflag 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
EpcSimulationgrows its first whole-dwelling, top-level overlay surface (lighting) — the applicator's fold writes onto theEpcPropertyDatadirectly 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 underlow_energy_lighting, and added to the_GENERATOR_MEASURE_TYPESforcing 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.