Model/tests/domain/modelling/_optimiser_fixtures.py
Khalim Conn-Kowlessar 832a30a985 Optimiser test fixtures are shared and domain-plausible 🟪
Review findings on PR #1526:

- tests/domain/modelling/_optimiser_fixtures.py is the one home for the
  overlay constants, the ScoredOption builder, the additive per-kind
  StubScorer and the forced ventilation dependency; test_optimiser.py and
  test_optimiser_fabric_first.py had byte-identical copies of each
  (and _StubScorer / _VentStubScorer fold into one parameterised stub).
- Fixture worlds are domain-plausible per team convention: the fabric-vs-
  heating contrast is a £12,000 EWI against a £3,200 gas boiler rather
  than a £500 heat pump undercutting a £1,000 cavity wall; heating
  overlays carry real identities (SAP Table 4a code 104 for the boiler,
  a PCDF index for the heat pump) instead of code 201 doubling as both;
  whole-dwelling double glazing is £3,500, not £500.
- Dead knobs removed: the unused _ROOF_OVERLAY, the always-zero roof
  gain, the duplicate _BOILER_OVERLAY, and the nested conditional
  expressions in the interaction stubs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:29:45 +00:00

144 lines
5 KiB
Python

"""Shared fixtures for the Optimiser test files: distinguishable Simulation
Overlays (so a stub scorer can attribute a true gain per measure kind), the
ScoredOption builder, the additive per-kind stub scorer, and the forced
ventilation Measure Dependency edge.
Fixture values stay domain-plausible: overlay heating codes are real SAP
Table 4a codes (104 = mains-gas combi boiler, heat pumps carry a PCDF index),
and tests price measures at realistic magnitudes (a CWI around £1,000, an
ASHP around £8,000)."""
from __future__ import annotations
from typing import Optional, Sequence
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
)
from domain.modelling.measure_type import MeasureType
from domain.modelling.optimisation.optimiser import MeasureDependency, ScoredOption
from domain.modelling.recommendation import Cost, MeasureOption
from domain.modelling.scoring.package_scorer import Score
from domain.modelling.simulation import (
BuildingPartOverlay,
EpcSimulation,
GlazingOverlay,
HeatingOverlay,
VentilationOverlay,
)
WALL_OVERLAY = EpcSimulation(
building_parts={
BuildingPartIdentifier.MAIN: BuildingPartOverlay(wall_insulation_type=2)
}
)
ROOF_OVERLAY = EpcSimulation(
building_parts={
BuildingPartIdentifier.MAIN: BuildingPartOverlay(roof_insulation_thickness=300)
}
)
FLOOR_OVERLAY = EpcSimulation(
building_parts={
BuildingPartIdentifier.MAIN: BuildingPartOverlay(floor_insulation_thickness=100)
}
)
GLAZING_OVERLAY = EpcSimulation(glazing=GlazingOverlay(glazing_type=2))
VENT_OVERLAY = EpcSimulation(
ventilation=VentilationOverlay(mechanical_ventilation_kind="EXTRACT_OR_PIV_OUTSIDE")
)
# SAP Table 4a code 104: a mains-gas combi boiler.
BOILER_OVERLAY = EpcSimulation(heating=HeatingOverlay(sap_main_heating_code=104))
# Heat pumps are expressed as a PCDF product index, as the generator emits them.
ASHP_OVERLAY = EpcSimulation(heating=HeatingOverlay(main_heating_index_number=13000))
_WALL_TRIGGERS: frozenset[MeasureType] = frozenset(
{MeasureType.CAVITY_WALL_INSULATION, MeasureType.EXTERNAL_WALL_INSULATION}
)
def scored_option(
measure_type: str,
*,
gain: float,
cost: float,
overlay: Optional[EpcSimulation] = None,
) -> ScoredOption:
"""A one-Option fixture: ``gain`` is the role-1 warm-start signal, ``cost``
the total install cost. Omit ``overlay`` where the test never re-scores."""
return ScoredOption(
option=MeasureOption(
measure_type=MeasureType(measure_type),
description=measure_type,
overlay=overlay if overlay is not None else EpcSimulation(),
cost=Cost(total=cost, contingency_rate=0.0),
),
sap_gain=gain,
)
class StubScorer:
"""A deterministic stand-in for PackageScorer: the package SAP is a base
plus a fixed *true* gain per measure kind present (detected by overlay
field), decoupled from the role-1 signal — so selection, repair and the
two-phase split are exercised without the calculator. Kinds a test does
not use default to 0."""
def __init__(
self,
*,
base: float,
wall: float = 0.0,
roof: float = 0.0,
floor: float = 0.0,
heating: float = 0.0,
vent: float = 0.0,
) -> None:
self._base = base
self._wall = wall
self._roof = roof
self._floor = floor
self._heating = heating
self._vent = vent
def score(
self, baseline: EpcPropertyData, simulations: Sequence[EpcSimulation]
) -> Score:
sap = self._base
for sim in simulations:
if sim.heating is not None:
sap += self._heating
if sim.ventilation is not None:
sap += self._vent
for part in sim.building_parts.values():
if part.wall_insulation_type is not None:
sap += self._wall
if part.roof_insulation_thickness is not None:
sap += self._roof
if part.floor_insulation_thickness is not None:
sap += self._floor
return Score(
sap_continuous=sap, co2_kg_per_yr=0.0, primary_energy_kwh_per_yr=0.0
)
def selected_types(selection: Sequence[ScoredOption]) -> set[str]:
return {scored.option.measure_type for scored in selection}
def ventilation_dependency(
*, cost: float, triggers: frozenset[MeasureType] = _WALL_TRIGGERS
) -> MeasureDependency:
"""A forced 'airtightness requires ventilation' edge for the tests."""
return MeasureDependency(
triggers=triggers,
required=ScoredOption(
option=MeasureOption(
measure_type=MeasureType.MECHANICAL_VENTILATION,
description="mechanical_ventilation",
overlay=VENT_OVERLAY,
cost=Cost(total=cost, contingency_rate=0.0),
),
sap_gain=0.0, # placeholder; optimise_package scores the real signal
),
)