Dependency signals are priced in the goal objective's currency 🟥

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-09 12:09:06 +00:00
parent 832a30a985
commit a8e2d99018

View file

@ -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