mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Added in portfolio aggregation method
This commit is contained in:
parent
f37f6ac029
commit
776f3a48e5
3 changed files with 88 additions and 39 deletions
39
backend/app/db/functions/portfolio_functions.py
Normal file
39
backend/app/db/functions/portfolio_functions.py
Normal 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()
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue