mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(modelling): wire glazing generator into the candidate pool
Slice 4 of the glazing generator (ADR-0022): run recommend_glazing in _candidate_recommendations, threading the Property's PlanningRestrictions so a protected dwelling is offered secondary glazing instead of double (mirrors recommend_solid_wall). Price both Measure Types in the offline catalogue (double £600/window, secondary £510 -- the legacy 0.85x scaling) and the contingency table (0.15, the legacy windows_glazing rate); the _GENERATOR_MEASURE_TYPES forcing test enforces both entries exist. run_modelling tests pin the wiring end-to-end on an all-single-glazed dwelling: double when unrestricted, secondary when listed. The first-run integration test seeds a double_glazing Product because its lodged EPC has a single-glazed window. _single_glazed_epc() deep-copies build_epc() (which shares its window objects) so the mutation can't leak into other tests' baselines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
276dd1a500
commit
456a81df0a
5 changed files with 66 additions and 4 deletions
|
|
@ -15,6 +15,8 @@ _CONTINGENCY_RATES: dict[str, float] = {
|
|||
"mechanical_ventilation": 0.26,
|
||||
"external_wall_insulation": 0.26,
|
||||
"internal_wall_insulation": 0.26,
|
||||
"double_glazing": 0.15,
|
||||
"secondary_glazing": 0.15,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,5 +7,7 @@
|
|||
"solid_floor_insulation": { "unit_cost_per_m2": 45.0 },
|
||||
"mechanical_ventilation": { "unit_cost_per_m2": 450.0 },
|
||||
"external_wall_insulation": { "unit_cost_per_m2": 100.0 },
|
||||
"internal_wall_insulation": { "unit_cost_per_m2": 90.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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,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.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.sap10_calculator.calculator import SapCalculator
|
||||
from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository
|
||||
|
|
@ -198,13 +199,14 @@ def _candidate_recommendations(
|
|||
planning_restrictions: PlanningRestrictions,
|
||||
) -> list[Recommendation]:
|
||||
"""Run every fabric Recommendation Generator; keep the ones that apply.
|
||||
Solid-wall insulation is additionally gated by the Property's planning
|
||||
protections (ADR-0019)."""
|
||||
Solid-wall insulation and glazing are additionally gated by the Property's
|
||||
planning protections (ADR-0019 / ADR-0022)."""
|
||||
found = (
|
||||
recommend_cavity_wall(effective_epc, products),
|
||||
recommend_solid_wall(effective_epc, products, planning_restrictions),
|
||||
recommend_roof_insulation(effective_epc, products),
|
||||
recommend_floor_insulation(effective_epc, products),
|
||||
recommend_glazing(effective_epc, products, planning_restrictions),
|
||||
)
|
||||
return [recommendation for recommendation in found if recommendation is not None]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
|
||||
import pytest
|
||||
|
|
@ -31,6 +32,8 @@ _GENERATOR_MEASURE_TYPES = (
|
|||
"suspended_floor_insulation",
|
||||
"solid_floor_insulation",
|
||||
"mechanical_ventilation",
|
||||
"double_glazing",
|
||||
"secondary_glazing",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -112,6 +115,49 @@ def test_run_modelling_listed_building_yields_no_wall_insulation() -> None:
|
|||
)
|
||||
|
||||
|
||||
def _single_glazed_epc() -> EpcPropertyData:
|
||||
"""The cavity/floor dwelling with all windows single-glazed — the glazing
|
||||
generator's trigger, sized so the upgrade reaches the optimised package.
|
||||
|
||||
`build_epc()` shares its window objects across calls, so deep-copy before
|
||||
mutating to avoid leaking single-glazing into other tests' baselines."""
|
||||
epc: EpcPropertyData = copy.deepcopy(_build_uninsulated_cavity_and_floor_epc())
|
||||
for window in epc.sap_windows:
|
||||
window.glazing_type = 1 # SAP10.2 Table U2 code 1 = single.
|
||||
return epc
|
||||
|
||||
|
||||
def test_run_modelling_recommends_double_glazing_for_single_glazed_windows() -> None:
|
||||
# Arrange — a single-glazed dwelling; the glazing generator is wired into
|
||||
# the candidate pool.
|
||||
epc: EpcPropertyData = _single_glazed_epc()
|
||||
|
||||
# Act — Modelling only, no database, unrestricted.
|
||||
plan = run_modelling(epc, goal_band="C", print_table=False)
|
||||
|
||||
# Assert — double glazing reaches the optimised package.
|
||||
measure_types = {measure.measure_type for measure in plan.measures}
|
||||
assert "double_glazing" in measure_types
|
||||
|
||||
|
||||
def test_run_modelling_protected_dwelling_yields_secondary_glazing() -> None:
|
||||
# Arrange — the same single-glazed dwelling, listed (blocks external work).
|
||||
epc: EpcPropertyData = _single_glazed_epc()
|
||||
|
||||
# Act — thread a listed-building restriction through to the generator.
|
||||
plan = run_modelling(
|
||||
epc,
|
||||
goal_band="C",
|
||||
print_table=False,
|
||||
planning_restrictions=PlanningRestrictions(is_listed=True),
|
||||
)
|
||||
|
||||
# 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
|
||||
assert "double_glazing" not in measure_types
|
||||
|
||||
|
||||
def test_sample_catalogue_prices_every_generator_measure_type() -> None:
|
||||
# Arrange — the default offline catalogue.
|
||||
products: ProductJsonRepository = ProductJsonRepository(DEFAULT_CATALOGUE)
|
||||
|
|
|
|||
|
|
@ -123,7 +123,9 @@ def test_first_run_baselines_through_repos_and_is_idempotent_on_rerun(
|
|||
# The sample EPC's solid floor is uninsulated, so the floor generator
|
||||
# fires during candidate generation and prices against this Product. The
|
||||
# ventilation Measure Dependency is built for every not-yet-ventilated
|
||||
# dwelling, so its Product must exist too (ADR-0016).
|
||||
# dwelling, so its Product must exist too (ADR-0016). The EPC also lodges
|
||||
# a single-glazed window, so the glazing generator fires and reaches for
|
||||
# the double-glazing Product (ADR-0022).
|
||||
session.add_all(
|
||||
[
|
||||
MaterialRow(
|
||||
|
|
@ -142,6 +144,14 @@ def test_first_run_baselines_through_repos_and_is_idempotent_on_rerun(
|
|||
is_active=True,
|
||||
description="Mechanical extract ventilation unit",
|
||||
),
|
||||
MaterialRow(
|
||||
id=3,
|
||||
type="double_glazing",
|
||||
total_cost=600.0,
|
||||
cost_unit="gbp_per_unit",
|
||||
is_active=True,
|
||||
description="Double glazing",
|
||||
),
|
||||
]
|
||||
)
|
||||
session.commit()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue