mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
With the mapper now in main, cert 001431 parses: it lodges four single-glazed
windows — codes 1 ("Single") and 15 ("single glazing, known data", a single
pane with manufacturer U/g). The generator only detected code 1, so it missed
two panes. Detect {1, 15}; set the secondary target to code 11 ("Secondary
glazing - Normal emissivity", what the cert re-lodges; score-neutral vs 7 but
exact).
A deterministic green pin proves the overlay reproduces the after's 14 windows
exactly. The full-SAP before->after pins are xfail(strict) tripwires: the
overlay nails the windows, but the measure also re-lodges percent_draughtproofed
84->100 (sealed units draught-proof the replaced openings) plus a ~0.4 SAP
fabric residual the overlay doesn't model yet — a glazing-measure coupling to
close later.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
126 lines
5 KiB
Python
126 lines
5 KiB
Python
"""Behaviour of the glazing Recommendation Generator: detecting single-glazed
|
|
windows and emitting one planning-picked "Windows" Recommendation (ADR-0022).
|
|
|
|
Unrestricted dwellings get `double_glazing`; a planning-protected dwelling gets
|
|
`secondary_glazing` instead — the measure is hard-picked by the Property's
|
|
`PlanningRestrictions`, not offered as competing Options. All single-glazed
|
|
windows are upgraded together in one overlay; the overlay writes each window's
|
|
lodged U-value and g-value (not just `glazing_type`) because the calculator
|
|
consumes those per-window values directly. Detection + pricing only, no scores
|
|
(ADR-0016). The before/after cascade pins live in `test_elmhurst_cascade_pins`.
|
|
"""
|
|
|
|
import copy
|
|
|
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
|
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
|
from domain.modelling.generators.glazing_recommendation import recommend_glazing
|
|
from domain.modelling.product import Product
|
|
from domain.modelling.recommendation import Recommendation
|
|
from domain.modelling.simulation import WindowOverlay
|
|
from repositories.product.product_repository import ProductRepository
|
|
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
|
|
build_epc,
|
|
)
|
|
|
|
# SAP10.2 Table U2 code 1 = single glazing (the generator's trigger).
|
|
_SINGLE_GLAZED = 1
|
|
|
|
|
|
class _StubProducts(ProductRepository):
|
|
"""In-memory ProductRepository returning a fixed per-window glazing price.
|
|
|
|
`unit_cost_per_m2` carries the catalogue row's fully-loaded total, reused as
|
|
a flat average price per window (ADR-0022)."""
|
|
|
|
def get(self, measure_type: str) -> Product:
|
|
return Product(
|
|
measure_type=measure_type,
|
|
unit_cost_per_m2=600.0,
|
|
contingency_rate=0.2,
|
|
id=42,
|
|
)
|
|
|
|
|
|
def _with_single_glazed(epc: EpcPropertyData, *indices: int) -> EpcPropertyData:
|
|
"""Return a copy of `epc` with the windows at `indices` set single-glazed."""
|
|
clone: EpcPropertyData = copy.deepcopy(epc)
|
|
for index in indices:
|
|
clone.sap_windows[index].glazing_type = _SINGLE_GLAZED
|
|
return clone
|
|
|
|
|
|
def test_single_glazed_dwelling_yields_a_double_glazing_recommendation() -> None:
|
|
# Arrange — 000490 is all double-glazed; make windows 0 and 2 single-glazed.
|
|
baseline: EpcPropertyData = _with_single_glazed(build_epc(), 0, 2)
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_glazing(
|
|
baseline, _StubProducts()
|
|
)
|
|
|
|
# Assert — one double-glazing Option whose overlay rewrites each single-
|
|
# glazed window to the pinned double target (gt=5, U=1.40, g=0.72).
|
|
assert recommendation is not None
|
|
assert recommendation.surface == "Windows"
|
|
assert len(recommendation.options) == 1
|
|
option = recommendation.options[0]
|
|
assert option.measure_type == "double_glazing"
|
|
assert dict(option.overlay.windows) == {
|
|
0: WindowOverlay(glazing_type=5, u_value=1.40, solar_transmittance=0.72),
|
|
2: WindowOverlay(glazing_type=5, u_value=1.40, solar_transmittance=0.72),
|
|
}
|
|
|
|
|
|
def test_fully_glazed_dwelling_yields_no_recommendation() -> None:
|
|
# Arrange — 000490 is entirely double-glazed (gt=2); nothing to upgrade.
|
|
baseline: EpcPropertyData = build_epc()
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_glazing(
|
|
baseline, _StubProducts()
|
|
)
|
|
|
|
# Assert
|
|
assert recommendation is None
|
|
|
|
|
|
def test_recommendation_prices_a_flat_average_per_single_glazed_window() -> None:
|
|
# Arrange — three single-glazed windows at £600 each (a flat per-window
|
|
# average reused from `unit_cost_per_m2`, ADR-0022).
|
|
baseline: EpcPropertyData = _with_single_glazed(build_epc(), 0, 2, 4)
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_glazing(
|
|
baseline, _StubProducts()
|
|
)
|
|
|
|
# Assert
|
|
assert recommendation is not None
|
|
cost = recommendation.options[0].cost
|
|
assert cost is not None
|
|
assert cost.total == 3 * 600.0
|
|
assert cost.contingency_rate == 0.2
|
|
assert recommendation.options[0].material_id == 42
|
|
|
|
|
|
def test_planning_protection_picks_secondary_glazing_over_double() -> None:
|
|
# Arrange — a conservation area blocks external work, so the external units
|
|
# can't be replaced; an internal secondary pane is the picked Measure.
|
|
baseline: EpcPropertyData = _with_single_glazed(build_epc(), 0, 2)
|
|
restrictions = PlanningRestrictions(in_conservation_area=True)
|
|
|
|
# Act
|
|
recommendation: Recommendation | None = recommend_glazing(
|
|
baseline, _StubProducts(), restrictions
|
|
)
|
|
|
|
# Assert — one secondary-glazing Option; each single window upgraded to the
|
|
# pinned secondary target (gt=11, U=2.90, g unchanged at 0.85).
|
|
assert recommendation is not None
|
|
option = recommendation.options[0]
|
|
assert option.measure_type == "secondary_glazing"
|
|
assert dict(option.overlay.windows) == {
|
|
0: WindowOverlay(glazing_type=11, u_value=2.90, solar_transmittance=0.85),
|
|
2: WindowOverlay(glazing_type=11, u_value=2.90, solar_transmittance=0.85),
|
|
}
|