diff --git a/orchestration/modelling_orchestrator.py b/orchestration/modelling_orchestrator.py index 5a50c12c9..111023694 100644 --- a/orchestration/modelling_orchestrator.py +++ b/orchestration/modelling_orchestrator.py @@ -182,7 +182,7 @@ class ModellingOrchestrator: # The Optimiser speaks the goal's currency (ADR-0062): group signals, # dependency pricing and repair marginals are all measured by this # objective — SAP by default, carbon reduction for a Reducing-CO2 goal. - objective: Callable[[Score], float] = _objective_for(scenario) + objective: Callable[[Score], float] = _objective_for(scenario, bill_derivation) groups: list[list[ScoredOption]] = _scored_candidate_groups( scorer, effective_epc, @@ -443,12 +443,28 @@ def _carbon_reduction(score: Score) -> float: return -score.co2_kg_per_yr -def _objective_for(scenario: Scenario) -> Callable[[Score], float]: +def _bill_saving(bill_derivation: BillDerivation) -> Callable[[Score], float]: + """The Energy-Savings objective: the annual Bill at the current Fuel Rates + snapshot, negated so higher is better (a saved £ scores +1). Priced at the + live snapshot, not SAP's internal tariff book — that difference is the + point of the goal (ADR-0062).""" + + def objective(score: Score) -> float: + return -_bill_for(bill_derivation, score).total_gbp + + return objective + + +def _objective_for( + scenario: Scenario, bill_derivation: BillDerivation +) -> Callable[[Score], float]: """The metric the Scenario's goal maximises (ADR-0062), as an Optimiser objective (higher is better). Goals without an aligned metric optimise SAP, as every goal did before.""" if scenario.goal == _REDUCING_CO2_GOAL: return _carbon_reduction + if scenario.goal == _ENERGY_SAVINGS_GOAL: + return _bill_saving(bill_derivation) return sap_rating