Model/domain/modelling/generators/lighting_recommendation.py
Khalim Conn-Kowlessar d58ac60d29 feat(modelling): MeasureType StrEnum as the canonical measure vocabulary
Introduce domain/modelling/measure_type.py — a StrEnum with one member per
modelled measure (the 15 the generators emit). A StrEnum so each member *is*
its string value: it persists straight into the `recommendation` varchar
column, is the optimiser's group-by key, and compares equal to the catalogue /
EPC strings — so it replaces the per-generator string constants with no
persistence or optimiser change.

Repoint every generator's measure-type constant/literal to a MeasureType
member (wall, solid_wall, roof, floor, glazing, lighting, ventilation,
heating, solar). Field annotations stay `str` for now; tightening them to
MeasureType is the next slice.

This is the enum the historical engine deferred (engine.py:970
"TODO - formalise property measure types into an enum") and the vocabulary the
forthcoming `considered_measures` allowlist will speak (mirroring the legacy
`inclusions`).

Suite green: tests/domain/modelling + orchestration + harness 253 pass + 3
xfail; pyright clean on the enum + generators.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:54:04 +00:00

64 lines
2.7 KiB
Python

"""The lighting Recommendation Generator (LED upgrade).
Detects a dwelling's non-LED fixed-lighting bulbs and emits one "Lighting"
Recommendation whose single Option converts **every** bulb to LED (ADR-0023).
SAP 10.2 RdSAP §12-1 rates lamp efficacy LED > low-energy-unknown > CFL >
incandescent, so converting every non-LED type — incandescent, CFL, and the
"low energy, type unknown" (LEL) bulbs alike — strictly improves the Appendix L
lighting energy (worksheet line (232)).
Unlike the fabric generators this is a **whole-dwelling** Measure: its overlay
writes the four top-level bulb counts directly (`led = total`, the rest 0). It
is a free Optimiser candidate — an LED upgrade improves SAP at low cost, so the
Optimiser keeps or leaves it for least-cost-to-target (contrast ventilation's
forced dependency). Detection + pricing only; impact is produced later by
scoring (ADR-0016).
"""
from typing import Final, Optional
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.modelling.measure_type import MeasureType
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
from domain.modelling.simulation import EpcSimulation, LightingOverlay
from repositories.product.product_repository import ProductRepository
_LIGHTING_MEASURE_TYPE: Final[MeasureType] = MeasureType.LOW_ENERGY_LIGHTING
def recommend_lighting(
epc: EpcPropertyData, products: ProductRepository
) -> Optional[Recommendation]:
"""Return a lighting Recommendation upgrading every non-LED bulb to LED — its
single Option — else None when the dwelling has no non-LED bulbs (already
all-LED, or no bulb counts lodged)."""
led: int = epc.led_fixed_lighting_bulbs_count or 0
cfl: int = epc.cfl_fixed_lighting_bulbs_count or 0
incandescent: int = epc.incandescent_fixed_lighting_bulbs_count or 0
low_energy: int = epc.low_energy_fixed_lighting_bulbs_count or 0
non_led: int = cfl + incandescent + low_energy
if non_led == 0:
return None
product = products.get(_LIGHTING_MEASURE_TYPE)
overlay = EpcSimulation(
lighting=LightingOverlay(
led_fixed_lighting_bulbs_count=led + non_led,
cfl_fixed_lighting_bulbs_count=0,
incandescent_fixed_lighting_bulbs_count=0,
low_energy_fixed_lighting_bulbs_count=0,
)
)
cost = Cost(
total=non_led * product.unit_cost_per_m2,
contingency_rate=product.contingency_rate,
)
option = MeasureOption(
measure_type=_LIGHTING_MEASURE_TYPE,
description="Replace all non-LED bulbs with LED",
overlay=overlay,
cost=cost,
material_id=product.id,
)
return Recommendation(surface="Lighting", options=(option,))