mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(modelling): wire the lighting generator into the candidate pool
Slice 4 of the lighting generator (ADR-0023): run recommend_lighting in _candidate_recommendations (no planning gate). Price low_energy_lighting in the offline catalogue + contingency table (0.26, the legacy rate); the _GENERATOR_MEASURE_TYPES forcing test enforces both. A run_modelling test pins the wiring end-to-end (an incandescent-lit dwelling gets the LED upgrade in the optimised package). Downstream updates, all because lighting now fires on any cert with non-LED bulbs: report.py gains the low_energy_lighting trigger (the non-LED counts); the two golden-cert report tests and the multi-measure integration test now expect low_energy_lighting alongside the fabric measures (the sample/golden EPCs lodge low-energy-unknown bulbs); first-run integration seeds a low_energy_lighting MaterialRow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ca397e1e3f
commit
f7863f986d
7 changed files with 85 additions and 4 deletions
|
|
@ -17,6 +17,7 @@ _CONTINGENCY_RATES: dict[str, float] = {
|
|||
"internal_wall_insulation": 0.26,
|
||||
"double_glazing": 0.15,
|
||||
"secondary_glazing": 0.15,
|
||||
"low_energy_lighting": 0.26,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -123,6 +123,17 @@ def _triggers_for(epc: EpcPropertyData, measure_type: str) -> dict[str, Any]:
|
|||
else epc.sap_ventilation.mechanical_ventilation_kind
|
||||
)
|
||||
return {"mechanical_ventilation_kind": kind}
|
||||
if measure_type == "low_energy_lighting":
|
||||
# lighting_recommendation.py fires on any non-LED bulb.
|
||||
return {
|
||||
"incandescent_fixed_lighting_bulbs_count": (
|
||||
epc.incandescent_fixed_lighting_bulbs_count
|
||||
),
|
||||
"cfl_fixed_lighting_bulbs_count": epc.cfl_fixed_lighting_bulbs_count,
|
||||
"low_energy_fixed_lighting_bulbs_count": (
|
||||
epc.low_energy_fixed_lighting_bulbs_count
|
||||
),
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,5 +9,6 @@
|
|||
"external_wall_insulation": { "unit_cost_per_m2": 100.0 },
|
||||
"internal_wall_insulation": { "unit_cost_per_m2": 90.0 },
|
||||
"double_glazing": { "unit_cost_per_m2": 600.0 },
|
||||
"secondary_glazing": { "unit_cost_per_m2": 510.0 }
|
||||
"secondary_glazing": { "unit_cost_per_m2": 510.0 },
|
||||
"low_energy_lighting": { "unit_cost_per_m2": 8.0 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from domain.modelling.scoring.scoring import (
|
|||
from domain.modelling.generators.wall_recommendation import recommend_cavity_wall
|
||||
from domain.modelling.generators.solid_wall_recommendation import recommend_solid_wall
|
||||
from domain.modelling.generators.glazing_recommendation import recommend_glazing
|
||||
from domain.modelling.generators.lighting_recommendation import recommend_lighting
|
||||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.sap10_calculator.calculator import SapCalculator
|
||||
from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository
|
||||
|
|
@ -207,6 +208,7 @@ def _candidate_recommendations(
|
|||
recommend_roof_insulation(effective_epc, products),
|
||||
recommend_floor_insulation(effective_epc, products),
|
||||
recommend_glazing(effective_epc, products, planning_restrictions),
|
||||
recommend_lighting(effective_epc, products),
|
||||
)
|
||||
return [recommendation for recommendation in found if recommendation is not None]
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ _GENERATOR_MEASURE_TYPES = (
|
|||
"mechanical_ventilation",
|
||||
"double_glazing",
|
||||
"secondary_glazing",
|
||||
"low_energy_lighting",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -151,6 +152,30 @@ def test_run_modelling_protected_dwelling_yields_secondary_glazing() -> None:
|
|||
# Assert — the picked glazing Measure is secondary, never double.
|
||||
measure_types = {measure.measure_type for measure in plan.measures}
|
||||
assert "secondary_glazing" in measure_types
|
||||
|
||||
|
||||
def _incandescent_lit_epc() -> EpcPropertyData:
|
||||
"""The cavity/floor dwelling lit entirely by incandescent bulbs — the
|
||||
lighting generator's trigger, sized so the LED upgrade reaches the package."""
|
||||
epc: EpcPropertyData = _build_uninsulated_cavity_and_floor_epc()
|
||||
epc.led_fixed_lighting_bulbs_count = 0
|
||||
epc.cfl_fixed_lighting_bulbs_count = 0
|
||||
epc.incandescent_fixed_lighting_bulbs_count = 10
|
||||
epc.low_energy_fixed_lighting_bulbs_count = 0
|
||||
return epc
|
||||
|
||||
|
||||
def test_run_modelling_recommends_low_energy_lighting_for_non_led_bulbs() -> None:
|
||||
# Arrange — a dwelling lit by incandescent bulbs; the lighting generator is
|
||||
# wired into the candidate pool.
|
||||
epc: EpcPropertyData = _incandescent_lit_epc()
|
||||
|
||||
# Act — Modelling only, no database.
|
||||
plan = run_modelling(epc, goal_band="C", print_table=False)
|
||||
|
||||
# Assert — the LED upgrade reaches the optimised package.
|
||||
measure_types = {measure.measure_type for measure in plan.measures}
|
||||
assert "low_energy_lighting" in measure_types
|
||||
assert "double_glazing" not in measure_types
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ def test_each_fired_measure_carries_the_attributes_that_triggered_it() -> None:
|
|||
"cavity_wall_insulation",
|
||||
"mechanical_ventilation",
|
||||
"solid_floor_insulation",
|
||||
"low_energy_lighting",
|
||||
}
|
||||
# Cavity fill fired because the main wall is an uninsulated cavity.
|
||||
assert triggers["cavity_wall_insulation"].triggers == {
|
||||
|
|
@ -97,22 +98,34 @@ def test_each_fired_measure_carries_the_attributes_that_triggered_it() -> None:
|
|||
"floor_insulation_thickness": None,
|
||||
"floor_construction_type": "Solid",
|
||||
}
|
||||
# The LED upgrade fired because the dwelling lodges 10 low-energy-unknown bulbs.
|
||||
assert triggers["low_energy_lighting"].triggers == {
|
||||
"incandescent_fixed_lighting_bulbs_count": 0,
|
||||
"cfl_fixed_lighting_bulbs_count": None,
|
||||
"low_energy_fixed_lighting_bulbs_count": 10,
|
||||
}
|
||||
|
||||
|
||||
def test_single_measure_cert_surfaces_only_that_measures_trigger() -> None:
|
||||
def test_few_measure_cert_surfaces_only_its_fired_measures_triggers() -> None:
|
||||
# Arrange
|
||||
path: Path = _GOLDEN / f"{_WITHIN_TOLERANCE}.json"
|
||||
|
||||
# Act
|
||||
report: PropertyReport = build_property_report(path)
|
||||
|
||||
# Assert — 0036 fires the solid-floor measure alone.
|
||||
# Assert — 0036 fires solid-floor insulation and the LED upgrade (it lodges
|
||||
# 7 low-energy-unknown bulbs), and nothing else.
|
||||
triggers: dict[str, MeasureTrigger] = _triggers_by_measure(report)
|
||||
assert set(triggers) == {"solid_floor_insulation"}
|
||||
assert set(triggers) == {"solid_floor_insulation", "low_energy_lighting"}
|
||||
assert triggers["solid_floor_insulation"].triggers == {
|
||||
"floor_insulation_thickness": None,
|
||||
"floor_construction_type": "Solid",
|
||||
}
|
||||
assert triggers["low_energy_lighting"].triggers == {
|
||||
"incandescent_fixed_lighting_bulbs_count": 0,
|
||||
"cfl_fixed_lighting_bulbs_count": None,
|
||||
"low_energy_fixed_lighting_bulbs_count": 7,
|
||||
}
|
||||
|
||||
|
||||
def test_cohort_builder_models_each_path_capturing_errors(tmp_path: Path) -> None:
|
||||
|
|
|
|||
|
|
@ -152,6 +152,14 @@ def test_first_run_baselines_through_repos_and_is_idempotent_on_rerun(
|
|||
is_active=True,
|
||||
description="Double glazing",
|
||||
),
|
||||
MaterialRow(
|
||||
id=4,
|
||||
type="low_energy_lighting",
|
||||
total_cost=8.0,
|
||||
cost_unit="gbp_per_unit",
|
||||
is_active=True,
|
||||
description="LED bulb",
|
||||
),
|
||||
]
|
||||
)
|
||||
session.commit()
|
||||
|
|
@ -260,6 +268,14 @@ def test_modelling_optimises_and_persists_a_multi_measure_plan(
|
|||
is_active=True,
|
||||
description="Mechanical extract ventilation unit",
|
||||
),
|
||||
MaterialRow(
|
||||
id=4,
|
||||
type="low_energy_lighting",
|
||||
total_cost=8.0,
|
||||
cost_unit="gbp_per_unit",
|
||||
is_active=True,
|
||||
description="LED bulb",
|
||||
),
|
||||
]
|
||||
)
|
||||
session.commit()
|
||||
|
|
@ -316,6 +332,9 @@ def test_modelling_optimises_and_persists_a_multi_measure_plan(
|
|||
"cavity_wall_insulation",
|
||||
"suspended_floor_insulation",
|
||||
"mechanical_ventilation",
|
||||
# The sample EPC lodges 8 low-energy-unknown bulbs, so the LED upgrade is
|
||||
# a cheap positive-SAP candidate the Optimiser also keeps (ADR-0023).
|
||||
"low_energy_lighting",
|
||||
}
|
||||
# Each persisted measure carries the catalogue id of the Product it installs
|
||||
# (the MaterialRow ids seeded above), replacing the retired
|
||||
|
|
@ -323,6 +342,7 @@ def test_modelling_optimises_and_persists_a_multi_measure_plan(
|
|||
assert by_type["cavity_wall_insulation"].material_id == 1
|
||||
assert by_type["suspended_floor_insulation"].material_id == 2
|
||||
assert by_type["mechanical_ventilation"].material_id == 3
|
||||
assert by_type["low_energy_lighting"].material_id == 4
|
||||
for rec in rec_rows:
|
||||
assert rec.default is True
|
||||
assert rec.already_installed is False
|
||||
|
|
@ -411,6 +431,14 @@ def test_modelling_recommends_nothing_when_already_at_the_target_band(
|
|||
is_active=True,
|
||||
description="Mechanical extract ventilation unit",
|
||||
),
|
||||
MaterialRow(
|
||||
id=13,
|
||||
type="low_energy_lighting",
|
||||
total_cost=8.0,
|
||||
cost_unit="gbp_per_unit",
|
||||
is_active=True,
|
||||
description="LED bulb",
|
||||
),
|
||||
]
|
||||
)
|
||||
session.commit()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue