Merge pull request #706 from Hestia-Homes/bug/ignored-cost-caps

bug fixed for 0 target gain where infinity was being selected
This commit is contained in:
KhalimCK 2026-02-13 12:56:31 +00:00 committed by GitHub
commit b5574467c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 172 additions and 49 deletions

View file

@ -865,7 +865,7 @@ async def model_engine(body: PlanTriggerRequest):
check_duplicate_property_ids(input_properties)
logger.info("Inserting property data")
# We now bulk upload all of the EPC data
# We now bulk upload all the EPC data
with db_session() as session:
db_funcs.epc_functions.EpcStoreService.bulk_upsert_epc_data(session, epc_upserts)

View file

@ -10,6 +10,7 @@ In the future, we will adapt this into a class-based structure to allow for more
from copy import deepcopy
import pandas as pd
import numpy as np
from typing import Mapping, Union
from itertools import product
from backend.app.plan.schemas import (
@ -823,21 +824,23 @@ def optimise_with_scenarios(
# No special path; just exclude ASHP from options and allow us to optimise.
measures_no_heat_pump = exclude_measure_types(optimisation_measures, ["air_source_heat_pump"])
picked, total_cost, total_gain = run_optimizer(
measures_no_heat_pump,
budget=budget,
sub_target_gain=target_gain,
)
if target_gain > 0:
# If we don't have any gain, we don't actually need to do this
picked, total_cost, total_gain = run_optimizer(
measures_no_heat_pump,
budget=budget,
sub_target_gain=target_gain,
)
if picked is not None:
solutions.append({
"scenario": "no_heat_pump",
"items": picked,
"fixed_items": [],
"total_cost": total_cost,
"total_gain": total_gain,
"already_installed_gain": sum([x["gain"] for x in picked if x["already_installed"]])
})
if picked is not None:
solutions.append({
"scenario": "no_heat_pump",
"items": picked,
"fixed_items": [],
"total_cost": total_cost,
"total_gain": total_gain,
"already_installed_gain": sum([x["gain"] for x in picked if x["already_installed"]])
})
solutions_df = append_solution_metrics(solutions, target_gain, p, already_installed_sap)
@ -1101,7 +1104,12 @@ def contributes_min_insulation(opt_types):
})
def run_optimizer(input_measures, budget=None, sub_target_gain=None, allow_slack=False):
def run_optimizer(
input_measures: list[list[Mapping[str, int | float | str]]],
budget: Union[float, None] = None,
sub_target_gain: Union[float, None] = None,
allow_slack: bool = False
):
"""
Thin wrapper over your optimisers.
Returns: list[dict] selected_options
@ -1112,7 +1120,7 @@ def run_optimizer(input_measures, budget=None, sub_target_gain=None, allow_slack
if budget is not None:
opt = GainOptimiser(
input_measures, max_cost=budget, max_gain=(sub_target_gain or float("inf")),
input_measures, max_cost=budget, max_gain=0 if sub_target_gain == 0 else (sub_target_gain or float("inf")),
allow_slack=allow_slack
)
else:
@ -1123,6 +1131,7 @@ def run_optimizer(input_measures, budget=None, sub_target_gain=None, allow_slack
opt.setup()
opt.solve()
cost = sum([x["cost"] for x in opt.solution])
return opt.solution, cost, opt.solution_gain

View file

@ -1,6 +1,7 @@
import pytest
from recommendations.optimiser.funding_optimiser import build_heat_pump_paths
from recommendations.optimiser.funding_optimiser import run_optimizer
class DummyProp:
@ -68,3 +69,143 @@ def test_build_heat_pump_paths():
assert eg2 == [{'AND': ['internal_wall_insulation', 'loft_insulation', 'air_source_heat_pump']},
{'AND': ['external_wall_insulation', 'loft_insulation', 'air_source_heat_pump']}]
def test_run_optimizer_empty_input():
solution, cost, gain = run_optimizer([])
assert solution is None
assert cost == 0.0
assert gain == 0.0
def test_uses_gain_optimiser_when_budget_provided(monkeypatch):
captured_args = {}
class FakeGainOptimiser:
def __init__(self, measures, max_cost, max_gain, allow_slack):
captured_args["measures"] = measures
captured_args["max_cost"] = max_cost
captured_args["max_gain"] = max_gain
captured_args["allow_slack"] = allow_slack
self.solution = [{"cost": 100}]
self.solution_gain = 5
def setup(self):
pass
def solve(self):
pass
monkeypatch.setattr(
"recommendations.optimiser.funding_optimiser.GainOptimiser",
FakeGainOptimiser
)
measures = [[{"cost": 100, "gain": 5}]]
solution, cost, gain = run_optimizer(
measures,
budget=500,
sub_target_gain=10,
allow_slack=True
)
assert captured_args["max_cost"] == 500
assert captured_args["max_gain"] == 10
assert captured_args["allow_slack"] is True
assert cost == 100
assert gain == 5
def test_sub_target_gain_zero_sets_max_gain_zero(monkeypatch):
captured_args = {}
class FakeGainOptimiser:
def __init__(self, measures, max_cost, max_gain, allow_slack):
captured_args["max_gain"] = max_gain
self.solution = []
self.solution_gain = 0
def setup(self):
pass
def solve(self):
pass
monkeypatch.setattr(
"recommendations.optimiser.funding_optimiser.GainOptimiser",
FakeGainOptimiser
)
measures = [[{"cost": 100, "gain": 5}]]
run_optimizer(
measures,
budget=500,
sub_target_gain=0
)
assert captured_args["max_gain"] == 0
def test_sub_target_gain_none_sets_max_gain_infinity(monkeypatch):
captured_args = {}
class FakeGainOptimiser:
def __init__(self, measures, max_cost, max_gain, allow_slack):
captured_args["max_gain"] = max_gain
self.solution = []
self.solution_gain = 0
def setup(self):
pass
def solve(self):
pass
monkeypatch.setattr(
"recommendations.optimiser.funding_optimiser.GainOptimiser",
FakeGainOptimiser
)
measures = [[{"cost": 100, "gain": 5}]]
run_optimizer(
measures,
budget=500,
sub_target_gain=None
)
assert captured_args["max_gain"] == float("inf")
def test_uses_cost_optimiser_when_no_budget(monkeypatch):
captured_args = {}
class FakeCostOptimiser:
def __init__(self, measures, min_gain):
captured_args["min_gain"] = min_gain
self.solution = [{"cost": 50}]
self.solution_gain = 10
def setup(self):
pass
def solve(self):
pass
monkeypatch.setattr(
"recommendations.optimiser.funding_optimiser.CostOptimiser",
FakeCostOptimiser
)
measures = [[{"cost": 50, "gain": 10}]]
solution, cost, gain = run_optimizer(
measures,
sub_target_gain=10
)
assert captured_args["min_gain"] == 10
assert cost == 50
assert gain == 10

View file

@ -28,12 +28,12 @@ from sqlalchemy import func
# PORTFOLIO_ID = 206
# SCENARIOS = [389]
PORTFOLIO_ID = 524
PORTFOLIO_ID = 568
SCENARIOS = [
1009,
1059,
]
scenario_names = {
1009: "EPC C; Most Economic",
1059: "EPC C - 10k budget",
}
@ -230,7 +230,7 @@ for scenario_id in SCENARIOS:
# Get recs for this scenario
recommended_measures_df = recommendations_df[
recommendations_df["scenario_id"] == scenario_id
][["property_id", "measure_type", "estimated_cost", "default"]]
][["property_id", "measure_type", "estimated_cost", "default"]]
recommended_measures_df = recommended_measures_df[
recommended_measures_df["default"]
]
@ -238,7 +238,7 @@ for scenario_id in SCENARIOS:
post_install_sap = recommendations_df[
recommendations_df["scenario_id"] == scenario_id
][["property_id", "default", "sap_points"]]
][["property_id", "default", "sap_points"]]
post_install_sap = post_install_sap[post_install_sap["default"]]
# Sum up the sap points by property id
post_install_sap = (
@ -301,33 +301,6 @@ for scenario_id in SCENARIOS:
)
df["uprn"] = df["uprn"].astype(str)
relevant_plans = plans_df[plans_df["scenario_id"] == scenario_id]
df2 = df.merge(
relevant_plans[["property_id", "post_sap_points", "post_epc_rating"]],
how="left",
on="property_id",
suffixes=("", "_plan"),
)
print(df2["predicted_post_works_epc"].value_counts())
print(df2["post_epc_rating"].value_counts())
z = df2[
(df2["predicted_post_works_epc"] != "D")
& (df2["post_epc_rating"].astype(str) == "Epc.D")
]
df2["predicted_post_works_epc"].value_counts()
df2["post_epc_rating"].astype(str).value_counts()
df2[df2["total_retrofit_cost"] > 0].shape
getting_works = df[df["total_retrofit_cost"] > 0]
getting_works["predicted_post_works_epc"].value_counts()
32565 / getting_works.shape[0]
df[df["predicted_post_works_sap"] == ""]
# Create excel to store to
filename = f"{scenario_names[scenario_id]} - 20250113 final.xlsx"
with pd.ExcelWriter(filename) as writer: