Model/tests/domain/modelling/test_optimiser_goal_objective.py
Khalim Conn-Kowlessar 7996eedf55 Goal-objective test uses the shared fixtures and a real boiler code 🟪
Review findings on PR #1527:

- The overlay constants, ScoredOption builder, ventilation dependency and
  selected_types helper come from the shared _optimiser_fixtures module
  (landed on the fabric-first base) instead of local copies; the boiler
  overlay is the shared BOILER_OVERLAY (SAP Table 4a code 104, a mains-gas
  combi) rather than code 201, which is neither a boiler nor a heat pump.
  _IWI_OVERLAY (solid-wall internal, type 3) stays local — no shared
  equivalent — and the carbon stubs stay bespoke (the shared StubScorer has
  no CO2 knob).
- The optimise_package_fabric_first import is lifted to module scope.

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

162 lines
6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Behaviour of the Optimiser under a goal-aligned objective (ADR-0062): a
Scenario whose goal is Reducing CO2 emissions / Energy Savings optimises its
own metric, not SAP. The caller supplies group signals already measured in the
objective's currency; the optimiser must price everything *it* computes — the
forced Measure Dependency signals — in the same currency, so a ventilation
that costs SAP but is carbon-neutral cannot sink a carbon-improving wall.
"""
from __future__ import annotations
from typing import Sequence
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
)
from domain.modelling.measure_type import MeasureType
from domain.modelling.optimisation.optimiser import (
OptimisedPackage,
ScoredOption,
optimise_package,
optimise_package_fabric_first,
)
from domain.modelling.scoring.package_scorer import Score
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
from tests.domain.modelling._optimiser_fixtures import (
ASHP_OVERLAY,
BOILER_OVERLAY,
WALL_OVERLAY,
scored_option,
selected_types,
ventilation_dependency,
)
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
build_epc,
)
class _CarbonScorer:
"""A stub where the wall is a small carbon win (20 kg/yr) and a large SAP
win (+6), while its forced ventilation is carbon-neutral but SAP-ruinous
(30): SAP-priced dependency signals sink the wall; carbon-priced ones
keep it."""
def score(
self, baseline: EpcPropertyData, simulations: Sequence[EpcSimulation]
) -> Score:
sap, co2 = 60.0, 500.0
for sim in simulations:
if sim.ventilation is not None:
sap -= 30.0
for part in sim.building_parts.values():
if part.wall_insulation_type is not None:
sap += 6.0
co2 -= 20.0
return Score(
sap_continuous=sap, co2_kg_per_yr=co2, primary_energy_kwh_per_yr=0.0
)
def _carbon_reduction(score: Score) -> float:
return -score.co2_kg_per_yr
def test_dependency_signals_are_priced_in_the_objective_currency() -> None:
# Arrange — the wall's signal (supplied by the caller, +20 kg CO2 saved)
# and the ventilation it forces in (carbon-neutral). Under legacy SAP
# pricing the ventilation's 30 SAP would outweigh the wall's +20 signal
# and the package would collapse to nothing.
groups: list[list[ScoredOption]] = [
[scored_option("cavity_wall_insulation", gain=20.0, cost=1000.0, overlay=WALL_OVERLAY)],
]
dependency = ventilation_dependency(
cost=300.0, triggers=frozenset({MeasureType.CAVITY_WALL_INSULATION})
)
# Act — a Reducing-CO2 brief: maximise carbon reduction within budget.
package: OptimisedPackage = optimise_package(
groups=groups,
scorer=_CarbonScorer(),
baseline_epc=build_epc(),
budget=5000.0,
target_sap=None,
dependencies=[dependency],
objective=_carbon_reduction,
)
# Assert — the wall survives with its ventilation: the dependency is worth
# 0 kg CO2, not 30 SAP, so the package is a net +20 kg saving.
assert selected_types(package.selected) == {
"cavity_wall_insulation",
"mechanical_ventilation",
}
assert abs(package.score.co2_kg_per_yr - 480.0) <= 1e-9
# Internal wall insulation — a distinct fabric overlay so the fabric-first
# phase-1 pick is unambiguous. No shared fixture (the shared WALL_OVERLAY is a
# cavity fill, type 2); this is a solid-wall internal treatment, type 3.
_IWI_OVERLAY = EpcSimulation(
building_parts={
BuildingPartIdentifier.MAIN: BuildingPartOverlay(wall_insulation_type=3)
}
)
class _CarbonHeatingScorer:
"""A stub where the boiler wins on SAP (+10 vs +2) but the heat pump wins
on carbon (50 vs 5 kg/yr): a fabric-first phase 2 that re-scores its
candidates in SAP picks the wrong heating for a Reducing-CO2 brief."""
def score(
self, baseline: EpcPropertyData, simulations: Sequence[EpcSimulation]
) -> Score:
sap, co2 = 60.0, 500.0
for sim in simulations:
for part in sim.building_parts.values():
if part.wall_insulation_type is not None:
sap += 5.0
co2 -= 10.0
if sim.heating is None:
continue
if sim.heating.sap_main_heating_code is not None:
sap += 10.0
co2 -= 5.0
if sim.heating.main_heating_index_number is not None:
sap += 2.0
co2 -= 50.0
return Score(
sap_continuous=sap, co2_kg_per_yr=co2, primary_energy_kwh_per_yr=0.0
)
def test_fabric_first_phase_two_rescores_in_the_objective_currency() -> None:
# Arrange — a fabric-first Reducing-CO2 brief. Phase 1 commits the wall;
# phase 2 must choose the heating on its post-fabric *carbon* worth, not
# its SAP worth. Signals are supplied in kg CO2 saved (the caller's job).
groups: list[list[ScoredOption]] = [
[scored_option("internal_wall_insulation", gain=10.0, cost=1000.0, overlay=_IWI_OVERLAY)],
[
scored_option("gas_boiler_upgrade", gain=5.0, cost=2000.0, overlay=BOILER_OVERLAY),
scored_option("air_source_heat_pump", gain=50.0, cost=6000.0, overlay=ASHP_OVERLAY),
],
]
# Act — no target (goal-aligned briefs have none), generous budget.
package: OptimisedPackage = optimise_package_fabric_first(
groups=groups,
scorer=_CarbonHeatingScorer(),
baseline_epc=build_epc(),
budget=10000.0,
target_sap=None,
objective=_carbon_reduction,
)
# Assert — the wall plus the heat pump (50 kg), not the SAP-favoured
# boiler; the truthful package carbon is 500 10 50 = 440.
assert selected_types(package.selected) == {
"internal_wall_insulation",
"air_source_heat_pump",
}
assert abs(package.score.co2_kg_per_yr - 440.0) <= 1e-9