mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
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>
144 lines
5 KiB
Python
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
|
|
),
|
|
)
|