mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Dependency signals are priced in the goal objective's currency 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
832a30a985
commit
a8e2d99018
1 changed files with 123 additions and 0 deletions
123
tests/domain/modelling/test_optimiser_goal_objective.py
Normal file
123
tests/domain/modelling/test_optimiser_goal_objective.py
Normal 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
|
||||
Loading…
Add table
Reference in a new issue