mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
implementing funding optimiser
This commit is contained in:
parent
ce362f5262
commit
0b6bc95881
5 changed files with 76 additions and 21 deletions
|
|
@ -1056,17 +1056,20 @@ class Funding:
|
|||
|
||||
if self.tenure == "Private":
|
||||
# We return ECO4 rates
|
||||
return innovation_uplift * (
|
||||
rate = (
|
||||
self.eco4_private_cavity_abs_rate if is_cavity
|
||||
else self.eco4_private_solid_abs_rate
|
||||
)
|
||||
|
||||
return pps, pps * rate, innovation_uplift * rate
|
||||
|
||||
if self.tenure == "Social":
|
||||
# We return ECO4 rates
|
||||
return innovation_uplift * (
|
||||
rate = (
|
||||
self.eco4_social_cavity_abs_rate if is_cavity
|
||||
else self.eco4_social_solid_abs_rate
|
||||
)
|
||||
return pps, pps * rate, innovation_uplift * rate
|
||||
|
||||
raise ValueError("Invalid tenure type for innovation uplift calculation: {}".format(self.tenure))
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,6 @@ class PlanTriggerRequest(BaseModel):
|
|||
scenario_name: Optional[str] = ""
|
||||
scenario_id: Optional[str | int] = None # Used to utilise and existing scenario for a engine run
|
||||
multi_plan: Optional[bool] = False
|
||||
optimise: Optional[bool] = True
|
||||
default_u_values: Optional[bool] = True
|
||||
|
||||
ashp_cop: Optional[float] = 2.8
|
||||
|
|
|
|||
|
|
@ -870,13 +870,16 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
is_sandstone_or_limestone=p.walls["is_sandstone_or_limestone"],
|
||||
)
|
||||
|
||||
# TODO: Turn this into a function and store the innovaiton uplift
|
||||
for group in measures_to_optimise_with_uplift:
|
||||
for r in group:
|
||||
if r["type"] in ["mechanical_ventilation", "low_energy_lighting", "secondary_heating"]:
|
||||
r["innovation_uplift"] = 0
|
||||
continue
|
||||
|
||||
r["innovation_uplift"] = funding.get_innovation_uplift(
|
||||
(
|
||||
r["partial_project_score"], r["partial_project_funding"], r["innovation_uplift"]
|
||||
) = funding.get_innovation_uplift(
|
||||
measure=r,
|
||||
starting_sap=p.data["current-energy-efficiency"],
|
||||
floor_area=p.floor_area,
|
||||
|
|
@ -903,26 +906,26 @@ async def model_engine(body: PlanTriggerRequest):
|
|||
)
|
||||
|
||||
# Given the solutions we select the optimal one
|
||||
solutions["cost_less_full_project_funding"] = solutions["total_cost"] - solutions[
|
||||
"eco4_full_project_funding"]
|
||||
solutions["cost_less_full_project_funding"] = (
|
||||
solutions["total_cost"] - solutions["eco4_full_project_funding"] - solutions["total_uplift"]
|
||||
)
|
||||
solutions = solutions.sort_values("cost_less_full_project_funding", ascending=True)
|
||||
|
||||
if solutions["meets_upgrade_target"].any():
|
||||
# If we have a solution that meets the upgrade target, we select that one
|
||||
optimal_solution = solutions[solutions["meets_upgrade_target"]].iloc[0]
|
||||
else:
|
||||
optimal_solution = optimal_solution.iloc[0]
|
||||
optimal_solution = solutions.iloc[0]
|
||||
|
||||
# optimal_solution =
|
||||
solution = optimal_solution["items"] + optimal_solution["unfunded_items"]
|
||||
full_project_funding = optimal_solution["eco4_full_project_funding"]
|
||||
scheme = optimal_solution["scheme"]
|
||||
|
||||
if not body.optimise:
|
||||
if body.goal != "Increasing EPC":
|
||||
raise NotImplementedError("Only EPC optimisation is currently supported")
|
||||
solution = [max(sub_list, key=lambda x: (x['gain'], -x['cost'])) for sub_list in input_measures]
|
||||
else:
|
||||
# We optimise and then we determine eligibility for funding, based on the measures selected
|
||||
optimiser = (
|
||||
GainOptimiser(
|
||||
input_measures, max_cost=body.budget, max_gain=gain, allow_slack=body.goal == "Increasing EPC"
|
||||
input_measures, max_cost=body.budget, max_gain=gain, allow_slack=False
|
||||
) if body.budget else CostOptimiser(input_measures, min_gain=gain)
|
||||
)
|
||||
optimiser.setup()
|
||||
|
|
|
|||
|
|
@ -172,10 +172,8 @@ def _ensure_unfunded_costs(groups):
|
|||
for grp in groups:
|
||||
for opt in grp:
|
||||
base = opt.get("cost_minus_uplift")
|
||||
upl = opt.get("innovation_uplift", 0.0)
|
||||
if base is not None:
|
||||
opt["cost"] = float(base) + float(upl)
|
||||
# else: assume opt["cost"] already includes uplift
|
||||
opt["cost"] = opt["raw_cost"]
|
||||
return groups
|
||||
|
||||
|
||||
|
|
@ -250,7 +248,8 @@ def optimise_with_funding_paths(p, input_measures, housing_type, funding: Fundin
|
|||
"total_gain": sub_gain,
|
||||
"path": path_spec,
|
||||
"scheme": scheme,
|
||||
"is_eligible": _is_eligible_funding_package(scheme, p.data["current-energy-efficiency"], sub_gain)
|
||||
"is_eligible": _is_eligible_funding_package(scheme, p.data["current-energy-efficiency"], sub_gain),
|
||||
"unfunded_items": []
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -318,6 +317,43 @@ def optimise_with_funding_paths(p, input_measures, housing_type, funding: Fundin
|
|||
|
||||
scheme = _path_scheme(path_spec)
|
||||
|
||||
unfunded_picked = []
|
||||
if total_gain - target_gain < -0.1:
|
||||
# In this case, we have a funded package that does not meet the target gain, so we look at the remaining
|
||||
# measures and see if we can include them
|
||||
picked_types = {opt["type"] for opt in total_picks}
|
||||
|
||||
# We find the indexes of the picked types
|
||||
picked_group_index = {}
|
||||
for pt in picked_types:
|
||||
for gi, grp in enumerate(optimisation_input_measures):
|
||||
if any(pt in opt["type"] for opt in grp):
|
||||
picked_group_index[pt] = gi
|
||||
break
|
||||
# We get the remaining types
|
||||
# ECO4 case
|
||||
remaining = []
|
||||
for i, grp in enumerate(optimisation_input_measures):
|
||||
if i in picked_group_index.values():
|
||||
continue
|
||||
keep = [x for x in grp if x["type"] not in picked_types]
|
||||
if keep:
|
||||
for x in keep:
|
||||
# Adjust to raw cost (without funding)
|
||||
x["cost"] = x["raw_cost"]
|
||||
remaining.append(keep)
|
||||
|
||||
if remaining:
|
||||
# If we have remaining measures we can optimise, we run them down an unfunded route
|
||||
unfunded_picked, unfunded_cost, unfunded_gain = run_optimizer(
|
||||
remaining,
|
||||
budget - total_cost if budget is not None else None,
|
||||
sub_target_gain=target_gain - total_gain if target_gain is not None else None
|
||||
)
|
||||
|
||||
total_cost += unfunded_cost
|
||||
total_gain += unfunded_gain
|
||||
|
||||
solutions.append({
|
||||
"fixed_ids": fixed_ids,
|
||||
"items": total_picks,
|
||||
|
|
@ -325,14 +361,15 @@ def optimise_with_funding_paths(p, input_measures, housing_type, funding: Fundin
|
|||
"total_gain": total_gain,
|
||||
"path": path_spec,
|
||||
"scheme": scheme,
|
||||
"is_eligible": _is_eligible_funding_package(scheme, p.data["current-energy-efficiency"], total_gain)
|
||||
"is_eligible": _is_eligible_funding_package(scheme, p.data["current-energy-efficiency"], total_gain),
|
||||
"unfunded_items": unfunded_picked,
|
||||
})
|
||||
|
||||
solutions = pd.DataFrame(solutions)
|
||||
|
||||
# Given the scheme, we now check if the packages are eligible. If they *are* eligible, but they don't meet the
|
||||
# final upgrade target, we then look to perform a final optimisation pass to meet the target gain.
|
||||
solutions["meets_upgrade_target"] = solutions["total_gain"] >= target_gain
|
||||
solutions["meets_upgrade_target"] = solutions["total_gain"] >= target_gain - 0.1
|
||||
|
||||
# If we have packages that are fundable, but do not meet the upgrade target, we can run a final optimisation pass
|
||||
if not solutions[solutions["is_eligible"] & ~solutions["meets_upgrade_target"]].empty:
|
||||
|
|
@ -358,12 +395,19 @@ def optimise_with_funding_paths(p, input_measures, housing_type, funding: Fundin
|
|||
# if the scheme is not ECO4, we set the funding to 0 with iloc
|
||||
solutions.loc[solutions["scheme"] != "eco4", "eco4_full_project_funding"] = 0.0
|
||||
|
||||
# We pull out uplifts
|
||||
solutions["total_uplift"] = solutions.apply(lambda x: get_total_uplift(x), axis=1)
|
||||
|
||||
return solutions
|
||||
|
||||
|
||||
# ---- helpers -------------------------------------------------------------
|
||||
|
||||
|
||||
def get_total_uplift(x):
|
||||
return sum([y["innovation_uplift"] for y in x["items"]])
|
||||
|
||||
|
||||
def sum_cost_gain(items):
|
||||
c = sum(float(x['cost']) for x in items)
|
||||
g = sum(float(x['gain']) for x in items)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ def prepare_input_measures(property_recommendations, goal, needs_ventilation, fu
|
|||
# Build enriched measure data
|
||||
to_append = []
|
||||
for rec in recs:
|
||||
|
||||
raw_cost = rec["total"]
|
||||
|
||||
if funding:
|
||||
total = (
|
||||
rec["total"] - rec["innovation_uplift"] + ventilation_recommendation["total"]
|
||||
|
|
@ -108,9 +111,12 @@ def prepare_input_measures(property_recommendations, goal, needs_ventilation, fu
|
|||
# We also include the innovation uplift
|
||||
to_append.append(
|
||||
{
|
||||
"id": rec["recommendation_id"], "cost": non_negative_total, "gain": gain, "type": rec_type,
|
||||
"id": rec["recommendation_id"],
|
||||
"cost": non_negative_total,
|
||||
"gain": gain, "type": rec_type,
|
||||
"innovation_uplift": rec["innovation_uplift"] if funding else 0,
|
||||
"cost_minus_uplift": total
|
||||
"cost_minus_uplift": total,
|
||||
"raw_cost": raw_cost
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue