mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(modelling): recommend_lighting upgrades all non-LED bulbs to LED
Slice 2 of the lighting generator (ADR-0023): detect non-LED bulbs (incandescent + CFL + low-energy-unknown > 0) and emit one "Lighting" Recommendation whose single low_energy_lighting Option converts every bulb to LED — the overlay sets led = total, the other three counts 0. Priced as a flat per-bulb average x the non-LED count, contingency 0.26; the description names "LED" while the measure_type stays MEASURE_MAP-aligned. None when already all-LED or no bulb counts are lodged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
139c90c885
commit
dbbfb8ea28
2 changed files with 184 additions and 0 deletions
63
domain/modelling/generators/lighting_recommendation.py
Normal file
63
domain/modelling/generators/lighting_recommendation.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""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.recommendation import Cost, MeasureOption, Recommendation
|
||||
from domain.modelling.simulation import EpcSimulation, LightingOverlay
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
|
||||
_LIGHTING_MEASURE_TYPE: Final[str] = "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,))
|
||||
121
tests/domain/modelling/test_lighting_recommendation.py
Normal file
121
tests/domain/modelling/test_lighting_recommendation.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
"""Behaviour of the lighting Recommendation Generator: detecting non-LED bulbs
|
||||
and emitting one "Lighting" Recommendation whose single Option upgrades every
|
||||
bulb to LED (ADR-0023). A free Optimiser candidate (it improves SAP), unlike
|
||||
ventilation's forced dependency. Detection + pricing only, no scores (ADR-0016).
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.modelling.generators.lighting_recommendation import recommend_lighting
|
||||
from domain.modelling.product import Product
|
||||
from domain.modelling.recommendation import Recommendation
|
||||
from domain.modelling.simulation import LightingOverlay
|
||||
from repositories.product.product_repository import ProductRepository
|
||||
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
|
||||
build_epc,
|
||||
)
|
||||
|
||||
|
||||
class _StubProducts(ProductRepository):
|
||||
"""In-memory ProductRepository returning a fixed per-bulb lighting price.
|
||||
|
||||
`unit_cost_per_m2` carries the catalogue row's fully-loaded total, reused as
|
||||
a flat average price per bulb (ADR-0023)."""
|
||||
|
||||
def get(self, measure_type: str) -> Product:
|
||||
return Product(
|
||||
measure_type=measure_type,
|
||||
unit_cost_per_m2=5.0,
|
||||
contingency_rate=0.26,
|
||||
id=7,
|
||||
)
|
||||
|
||||
|
||||
def _with_bulbs(
|
||||
epc: EpcPropertyData, *, led: int, cfl: int, incandescent: int, low_energy: int
|
||||
) -> EpcPropertyData:
|
||||
"""Return a copy of `epc` with the four fixed-lighting bulb counts set."""
|
||||
clone: EpcPropertyData = copy.deepcopy(epc)
|
||||
clone.led_fixed_lighting_bulbs_count = led
|
||||
clone.cfl_fixed_lighting_bulbs_count = cfl
|
||||
clone.incandescent_fixed_lighting_bulbs_count = incandescent
|
||||
clone.low_energy_fixed_lighting_bulbs_count = low_energy
|
||||
return clone
|
||||
|
||||
|
||||
def test_dwelling_with_non_led_bulbs_yields_a_lighting_recommendation() -> None:
|
||||
# Arrange — a mixed inventory: 2 LED + 3 CFL + 4 incandescent + 1 LEL.
|
||||
baseline: EpcPropertyData = _with_bulbs(
|
||||
build_epc(), led=2, cfl=3, incandescent=4, low_energy=1
|
||||
)
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_lighting(
|
||||
baseline, _StubProducts()
|
||||
)
|
||||
|
||||
# Assert — one all-LED Option: led = total (10), every other count zeroed.
|
||||
assert recommendation is not None
|
||||
assert recommendation.surface == "Lighting"
|
||||
assert len(recommendation.options) == 1
|
||||
option = recommendation.options[0]
|
||||
assert option.measure_type == "low_energy_lighting"
|
||||
assert option.overlay.lighting == LightingOverlay(
|
||||
led_fixed_lighting_bulbs_count=10,
|
||||
cfl_fixed_lighting_bulbs_count=0,
|
||||
incandescent_fixed_lighting_bulbs_count=0,
|
||||
low_energy_fixed_lighting_bulbs_count=0,
|
||||
)
|
||||
|
||||
|
||||
def test_already_all_led_dwelling_yields_no_recommendation() -> None:
|
||||
# Arrange — every bulb is already LED; nothing to upgrade.
|
||||
baseline: EpcPropertyData = _with_bulbs(
|
||||
build_epc(), led=9, cfl=0, incandescent=0, low_energy=0
|
||||
)
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_lighting(
|
||||
baseline, _StubProducts()
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert recommendation is None
|
||||
|
||||
|
||||
def test_dwelling_with_no_lodged_bulbs_yields_no_recommendation() -> None:
|
||||
# Arrange — no bulb counts lodged (the calculator's L5b fallback case): with
|
||||
# no inventory to size against, no Recommendation is offered (ADR-0023).
|
||||
baseline: EpcPropertyData = _with_bulbs(
|
||||
build_epc(), led=0, cfl=0, incandescent=0, low_energy=0
|
||||
)
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_lighting(
|
||||
baseline, _StubProducts()
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert recommendation is None
|
||||
|
||||
|
||||
def test_recommendation_prices_a_flat_average_per_non_led_bulb() -> None:
|
||||
# Arrange — 8 non-LED bulbs (3 CFL + 4 incandescent + 1 LEL) at £5 each; the
|
||||
# 2 existing LEDs are not priced (not replaced).
|
||||
baseline: EpcPropertyData = _with_bulbs(
|
||||
build_epc(), led=2, cfl=3, incandescent=4, low_energy=1
|
||||
)
|
||||
|
||||
# Act
|
||||
recommendation: Recommendation | None = recommend_lighting(
|
||||
baseline, _StubProducts()
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert recommendation is not None
|
||||
cost = recommendation.options[0].cost
|
||||
assert cost is not None
|
||||
assert cost.total == 8 * 5.0
|
||||
assert cost.contingency_rate == 0.26
|
||||
assert recommendation.options[0].material_id == 7
|
||||
Loading…
Add table
Reference in a new issue