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 (
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.CostOptimiser import CostOptimiser
@ -105,23 +106,23 @@ def filter_materials(materials):
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
filtering recommendations between default and no, after the optimiser has been
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"
integer inserted
"""
idx = 0
for recs in recommendations_to_upload:
for recs in property_recommendations:
for rec in recs:
rec["recommendation_id"] = idx
idx += 1
return recommendations_to_upload
return property_recommendations
@router.post("/trigger")
@ -197,6 +198,8 @@ async def trigger_plan(body: PlanTriggerRequest):
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 = {}
for p in input_properties:
property_recommendations = []
@ -264,37 +267,14 @@ async def trigger_plan(body: PlanTriggerRequest):
if 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:
# 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:
if not property_recommendations:
continue
recommendations_to_upload = insert_temp_recommendation_id(recommendations_to_upload)
# Optimise the recommendations
# We need to format the recommendations for the optimiser
input_measures = prepare_input_measures(recommendations_to_upload, body.goal)
input_measures = prepare_input_measures(property_recommendations, body.goal)
if 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}
# 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}
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
recommendations_to_upload = [
rec for recommendations_by_type in recommendations_to_upload for rec in recommendations_by_type
property_recommendations = [
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
new_plan_id = create_plan(
{
@ -371,4 +373,12 @@ async def trigger_plan(body: PlanTriggerRequest):
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)

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
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,
the goal should reflect that desired gain
: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")
input_measures = []
for recs in recommendations_to_upload:
for recs in property_recommendations:
input_measures.append(
[
{