From a8e2d990185e27e13dd894206a33100a69ab65ec Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Jul 2026 12:09:06 +0000 Subject: [PATCH] =?UTF-8?q?Dependency=20signals=20are=20priced=20in=20the?= =?UTF-8?q?=20goal=20objective's=20currency=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../test_optimiser_goal_objective.py | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/domain/modelling/test_optimiser_goal_objective.py diff --git a/tests/domain/modelling/test_optimiser_goal_objective.py b/tests/domain/modelling/test_optimiser_goal_objective.py new file mode 100644 index 000000000..57e3c8240 --- /dev/null +++ b/tests/domain/modelling/test_optimiser_goal_objective.py @@ -0,0 +1,123 @@ +"""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 ( + MeasureDependency, + OptimisedPackage, + ScoredOption, + optimise_package, +) +from domain.modelling.recommendation import Cost, MeasureOption +from domain.modelling.scoring.package_scorer import Score +from domain.modelling.simulation import ( + BuildingPartOverlay, + EpcSimulation, + VentilationOverlay, +) +from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import ( + build_epc, +) + +_WALL_OVERLAY = EpcSimulation( + building_parts={ + BuildingPartIdentifier.MAIN: BuildingPartOverlay(wall_insulation_type=2) + } +) +_VENT_OVERLAY = EpcSimulation( + ventilation=VentilationOverlay(mechanical_ventilation_kind="EXTRACT_OR_PIV_OUTSIDE") +) + + +def _scored( + measure_type: str, *, gain: float, cost: float, overlay: EpcSimulation +) -> ScoredOption: + return ScoredOption( + option=MeasureOption( + measure_type=MeasureType(measure_type), + description=measure_type, + overlay=overlay, + cost=Cost(total=cost, contingency_rate=0.0), + ), + sap_gain=gain, + ) + + +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("cavity_wall_insulation", gain=20.0, cost=1000.0, overlay=_WALL_OVERLAY)], + ] + dependency = MeasureDependency( + triggers=frozenset({MeasureType.CAVITY_WALL_INSULATION}), + required=ScoredOption( + option=MeasureOption( + measure_type=MeasureType.MECHANICAL_VENTILATION, + description="mechanical_ventilation", + overlay=_VENT_OVERLAY, + cost=Cost(total=300.0, contingency_rate=0.0), + ), + sap_gain=0.0, + ), + ) + + # 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 {s.option.measure_type for s in package.selected} == { + "cavity_wall_insulation", + "mechanical_ventilation", + } + assert abs(package.score.co2_kg_per_yr - 480.0) <= 1e-9