Added in portfolio aggregation method

This commit is contained in:
Khalim Conn-Kowlessar 2023-08-18 17:21:11 +01:00
parent f37f6ac029
commit 776f3a48e5
3 changed files with 88 additions and 39 deletions

View file

@ -0,0 +1,39 @@
from sqlalchemy.orm import sessionmaker
from sqlalchemy import func
from backend.app.db.connection import db_engine
from backend.app.db.models.recommendations import Plan, PlanRecommendations, Recommendation
from backend.app.db.models.portfolio import Portfolio
def aggregate_portfolio_recommendations(portfolio_id: int):
Session = sessionmaker(bind=db_engine)
with Session() as session:
# Aggregate multiple fields
aggregates = (
session.query(
func.sum(Recommendation.estimated_cost).label("cost"),
# For future usage we will aggregate multiple fields in this step
# func.sum(Recommendation.heat_demand).label("total_heat_demand"),
# func.sum(Recommendation.energy_savings).label("total_energy_savings")
)
.join(PlanRecommendations, PlanRecommendations.recommendation_id == Recommendation.id)
.join(Plan, Plan.id == PlanRecommendations.plan_id)
.filter(Plan.portfolio_id == portfolio_id, Plan.is_default == True, Recommendation.default == True)
.one()
)
aggregates_dict = {
"cost": aggregates.cost or 0,
# "total_heat_demand": aggregates.total_heat_demand or 0,
# "total_energy_savings": aggregates.total_energy_savings or 0
}
# Get the portfolio and update the fields
portfolio = session.query(Portfolio).filter_by(id=portfolio_id).one()
# Update the data
for key, value in aggregates_dict.items():
setattr(portfolio, key, value)
# Merge the updated portfolio back into the session
session.merge(portfolio)
session.commit()

View file

@ -22,6 +22,7 @@ from backend.app.db.functions.materials_functions import get_materials
from backend.app.db.functions.recommendations_functions import ( from backend.app.db.functions.recommendations_functions import (
create_plan, create_recommendation, create_recommendation_material, create_plan_recommendations create_plan, create_recommendation, create_recommendation_material, create_plan_recommendations
) )
from backend.app.db.functions.portfolio_functions import aggregate_portfolio_recommendations
from model_data.optimiser.GainOptimiser import GainOptimiser from model_data.optimiser.GainOptimiser import GainOptimiser
from model_data.optimiser.CostOptimiser import CostOptimiser from model_data.optimiser.CostOptimiser import CostOptimiser
@ -105,23 +106,23 @@ def filter_materials(materials):
return materials_by_type return materials_by_type
def insert_temp_recommendation_id(recommendations_to_upload): def insert_temp_recommendation_id(property_recommendations):
""" """
Creates a temporary recommendation id which is needed for Creates a temporary recommendation id which is needed for
filtering recommendations between default and no, after the optimiser has been filtering recommendations between default and no, after the optimiser has been
run run
:param recommendations_to_upload: nested list of recommendations, grouped by types :param property_recommendations: nested list of recommendations, grouped by types
:return: Updated recommendations_to_upload, where where recommendation has a "recommendation_id" :return: Updated recommendations_to_upload, where where recommendation has a "recommendation_id"
integer inserted integer inserted
""" """
idx = 0 idx = 0
for recs in recommendations_to_upload: for recs in property_recommendations:
for rec in recs: for rec in recs:
rec["recommendation_id"] = idx rec["recommendation_id"] = idx
idx += 1 idx += 1
return recommendations_to_upload return property_recommendations
@router.post("/trigger") @router.post("/trigger")
@ -197,6 +198,8 @@ async def trigger_plan(body: PlanTriggerRequest):
logger.info("Getting components and properties recommendations") logger.info("Getting components and properties recommendations")
# TODO: Move this to a class. We probably was a Recommender class which takes the injects the optimisers
# in as a dependency and then the optimisers can take the input measures in as part of the setup() method
recommendations = {} recommendations = {}
for p in input_properties: for p in input_properties:
property_recommendations = [] property_recommendations = []
@ -264,37 +267,14 @@ async def trigger_plan(body: PlanTriggerRequest):
if wall_recomender.recommendations: if wall_recomender.recommendations:
property_recommendations.append(wall_recomender.recommendations) property_recommendations.append(wall_recomender.recommendations)
recommendations[p.id] = property_recommendations # Use the optimiser to pick the default recommendations and decide if we need certain
# recommendations to get to the goal
property_recommendations = insert_temp_recommendation_id(property_recommendations)
# Once we're done, we'll store: if not property_recommendations:
# 1) the property data
# 2) the property details (epc)
# 3) the recommendations
logger.info("Uploading recommendations to the database")
# Upload property data
for p in input_properties:
property_details_epc = p.get_property_details_epc(portfolio_id=body.portfolio_id, rating_lookup=rating_lookup)
create_property_details_epc(property_details_epc)
property_data = p.get_full_property_data()
update_property_data(property_id=p.id, portfolio_id=body.portfolio_id, property_data=property_data)
# Upload recommendations
# TODO: We start off by optimising the recommendations
recommendations_to_upload = recommendations[p.id]
if not recommendations_to_upload:
continue continue
recommendations_to_upload = insert_temp_recommendation_id(recommendations_to_upload) input_measures = prepare_input_measures(property_recommendations, body.goal)
# Optimise the recommendations
# We need to format the recommendations for the optimiser
input_measures = prepare_input_measures(recommendations_to_upload, body.goal)
if body.budget: if body.budget:
optimiser = GainOptimiser(input_measures, max_cost=body.budget) optimiser = GainOptimiser(input_measures, max_cost=body.budget)
@ -315,19 +295,41 @@ async def trigger_plan(body: PlanTriggerRequest):
selected_recommendations = {r["id"] for r in solution} selected_recommendations = {r["id"] for r in solution}
# We'll use the set of selected recommendations to filter the recommendations to upload # We'll use the set of selected recommendations to filter the recommendations to upload
recommendations_to_upload = [ property_recommendations = [
[ [
{**rec, "default": True if rec["recommendation_id"] in selected_recommendations else False} {**rec, "default": True if rec["recommendation_id"] in selected_recommendations else False}
for rec in recommendations_by_type for rec in recommendations_by_type
] ]
for recommendations_by_type in recommendations_to_upload for recommendations_by_type in property_recommendations
] ]
# We'll also unlist the recommendations so they're a bit easier to handle from here onwards # We'll also unlist the recommendations so they're a bit easier to handle from here onwards
recommendations_to_upload = [ property_recommendations = [
rec for recommendations_by_type in recommendations_to_upload for rec in recommendations_by_type rec for recommendations_by_type in property_recommendations for rec in recommendations_by_type
] ]
recommendations[p.id] = property_recommendations
# Once we're done, we'll store:
# 1) the property data
# 2) the property details (epc)
# 3) the recommendations
logger.info("Uploading recommendations to the database")
# Upload property data
for p in input_properties:
property_details_epc = p.get_property_details_epc(portfolio_id=body.portfolio_id, rating_lookup=rating_lookup)
create_property_details_epc(property_details_epc)
property_data = p.get_full_property_data()
update_property_data(property_id=p.id, portfolio_id=body.portfolio_id, property_data=property_data)
# Upload recommendations
recommendations_to_upload = recommendations.get(p.id, [])
if not recommendations_to_upload:
continue
# Create a plan # Create a plan
new_plan_id = create_plan( new_plan_id = create_plan(
{ {
@ -371,4 +373,12 @@ async def trigger_plan(body: PlanTriggerRequest):
recommendation_ids=uploaded_recommendation_ids recommendation_ids=uploaded_recommendation_ids
) )
logger.info("Creating portfolio aggregations")
# We implement this in the simplest way possible which will be just to query the database for all
# recommendations associated to the portfolio and then aggregate them. This is not the most efficient
# way to do this, but it's the simplest and will be a process that we can re-use since when we change a
# recommendation from being default to not default, we'll need to re-run this process to re-calculate the
# the portfolion level impact
aggregate_portfolio_recommendations(portfolio_id=body.portfolio_id)
return Response(status_code=200) return Response(status_code=200)

View file

@ -1,8 +1,8 @@
def prepare_input_measures(recommendations_to_upload, goal): def prepare_input_measures(property_recommendations, goal):
""" """
Basic function to convert recommendations_to_upload to a format that is Basic function to convert recommendations_to_upload to a format that is
suitable for the optimiser - large suitable for the optimiser - large
:param recommendations_to_upload: object containing the recommendations, created in the plan trigger api :param property_recommendations: object containing the recommendations, created in the plan trigger api
:param goal: goal to be optimised for, should be one of the keys in gain_map. E.g. if the gain is SAP points, :param goal: goal to be optimised for, should be one of the keys in gain_map. E.g. if the gain is SAP points,
the goal should reflect that desired gain the goal should reflect that desired gain
:return: Nested list of input measures :return: Nested list of input measures
@ -17,7 +17,7 @@ def prepare_input_measures(recommendations_to_upload, goal):
raise NotImplementedError("Not implemented this gain type - investigate me") raise NotImplementedError("Not implemented this gain type - investigate me")
input_measures = [] input_measures = []
for recs in recommendations_to_upload: for recs in property_recommendations:
input_measures.append( input_measures.append(
[ [
{ {