mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Merge branch 'main' of github.com:Hestia-Homes/Model into michael-initial
This commit is contained in:
commit
a3234b87e9
7 changed files with 50 additions and 24 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
from sqlalchemy import text
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from backend.app.db.connection import db_engine
|
from backend.app.db.connection import db_engine
|
||||||
from backend.app.db.models.recommendations import Plan, Recommendation, RecommendationMaterials
|
from backend.app.db.models.recommendations import Plan, Recommendation, RecommendationMaterials
|
||||||
|
|
@ -60,12 +61,13 @@ def create_plan_recommendations(plan_id, recommendation_ids):
|
||||||
:param plan_id: ID of the plan
|
:param plan_id: ID of the plan
|
||||||
:param recommendation_ids: list of recommendation IDs
|
:param recommendation_ids: list of recommendation IDs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Session = sessionmaker(bind=db_engine)
|
Session = sessionmaker(bind=db_engine)
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
for recommendation_id in recommendation_ids:
|
for recommendation_id in recommendation_ids:
|
||||||
session.execute(
|
session.execute(
|
||||||
'INSERT INTO plan_recommendations (plan_id, recommendation_id) VALUES (:plan_id, :recommendation_id)',
|
text(
|
||||||
|
'INSERT INTO plan_recommendations (plan_id, recommendation_id) VALUES (:plan_id, '
|
||||||
|
':recommendation_id)'),
|
||||||
{'plan_id': plan_id, 'recommendation_id': recommendation_id}
|
{'plan_id': plan_id, 'recommendation_id': recommendation_id}
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import enum
|
import enum
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, String, Float, Enum, TIMESTAMP
|
from sqlalchemy import Column, Integer, String, Float, Enum, TIMESTAMP, Boolean
|
||||||
from sqlalchemy.orm import declarative_base
|
from sqlalchemy.orm import declarative_base
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ class Material(Base):
|
||||||
description = Column(String, nullable=False)
|
description = Column(String, nullable=False)
|
||||||
depths = Column(String) # You may want to use a specific JSON type depending on the database
|
depths = Column(String) # You may want to use a specific JSON type depending on the database
|
||||||
depth_unit = Column(Enum(DepthUnit, values_callable=lambda x: [e.value for e in x]), nullable=False)
|
depth_unit = Column(Enum(DepthUnit, values_callable=lambda x: [e.value for e in x]), nullable=False)
|
||||||
cost = Column(Float)
|
cost = Column(String)
|
||||||
cost_unit = Column(Enum(CostUnit, values_callable=lambda x: [e.value for e in x]), nullable=False)
|
cost_unit = Column(Enum(CostUnit, values_callable=lambda x: [e.value for e in x]), nullable=False)
|
||||||
r_value_per_mm = Column(Float)
|
r_value_per_mm = Column(Float)
|
||||||
r_value_unit = Column(Enum(RValueUnit, values_callable=lambda x: [e.value for e in x]), nullable=False)
|
r_value_unit = Column(Enum(RValueUnit, values_callable=lambda x: [e.value for e in x]), nullable=False)
|
||||||
|
|
@ -49,3 +49,4 @@ class Material(Base):
|
||||||
)
|
)
|
||||||
link = Column(String)
|
link = Column(String)
|
||||||
created_at = Column(TIMESTAMP, nullable=False, server_default=func.now())
|
created_at = Column(TIMESTAMP, nullable=False, server_default=func.now())
|
||||||
|
is_active = Column(Boolean, nullable=False, default=True)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ class RecommendationMaterials(Base):
|
||||||
recommendation_id = Column(BigInteger, ForeignKey('recommendation.id'), nullable=False)
|
recommendation_id = Column(BigInteger, ForeignKey('recommendation.id'), nullable=False)
|
||||||
material_id = Column(BigInteger, ForeignKey(Material.id), nullable=False)
|
material_id = Column(BigInteger, ForeignKey(Material.id), nullable=False)
|
||||||
created_at = Column(TIMESTAMP, nullable=False, server_default=func.now())
|
created_at = Column(TIMESTAMP, nullable=False, server_default=func.now())
|
||||||
|
depth = Column(Float, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
class Plan(Base):
|
class Plan(Base):
|
||||||
|
|
|
||||||
|
|
@ -200,8 +200,10 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
|
|
||||||
# Floor recommendations
|
# Floor recommendations
|
||||||
floor_recommender = FloorRecommendations(
|
floor_recommender = FloorRecommendations(
|
||||||
property_instance=p, uvalue_estimates=floors_u_value_estimate,
|
property_instance=p,
|
||||||
total_floor_area_group_decile=total_floor_area_group_decile
|
uvalue_estimates=floors_u_value_estimate,
|
||||||
|
total_floor_area_group_decile=total_floor_area_group_decile,
|
||||||
|
materials=materials_by_type["suspended_floor_insulation"] + materials_by_type["solid_floor_insulation"],
|
||||||
)
|
)
|
||||||
floor_recommender.recommend()
|
floor_recommender.recommend()
|
||||||
|
|
||||||
|
|
@ -256,8 +258,9 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
# TODO: We start off by optimising the recommendations
|
# TODO: We start off by optimising the recommendations
|
||||||
|
|
||||||
recommendations_to_upload = recommendations[p.id]
|
recommendations_to_upload = recommendations[p.id]
|
||||||
if not recommendations:
|
if not recommendations_to_upload:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Create a plan
|
# Create a plan
|
||||||
new_plan_id = create_plan(
|
new_plan_id = create_plan(
|
||||||
{
|
{
|
||||||
|
|
@ -269,14 +272,13 @@ async def trigger_plan(body: PlanTriggerRequest):
|
||||||
# upload recommendations
|
# upload recommendations
|
||||||
uploaded_recommendation_ids = []
|
uploaded_recommendation_ids = []
|
||||||
for rec in recommendations_to_upload:
|
for rec in recommendations_to_upload:
|
||||||
# TODO: implement costs (at least a placeholder)
|
|
||||||
estimated_cost = sum([x["cost"] if x["cost"] else 0 for x in rec["parts"]])
|
|
||||||
|
|
||||||
recommendation_id = create_recommendation(
|
recommendation_id = create_recommendation(
|
||||||
{
|
{
|
||||||
|
"property_id": p.id,
|
||||||
"type": rec["type"],
|
"type": rec["type"],
|
||||||
"description": rec["description"],
|
"description": rec["description"],
|
||||||
"estimated_cost": estimated_cost,
|
"estimated_cost": rec["cost"],
|
||||||
"default": True,
|
"default": True,
|
||||||
"starting_u_value": rec.get("starting_u_value"),
|
"starting_u_value": rec.get("starting_u_value"),
|
||||||
"new_u_value": rec.get("new_u_value"),
|
"new_u_value": rec.get("new_u_value"),
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,13 @@ class FloorRecommendations(BaseUtility):
|
||||||
else:
|
else:
|
||||||
self.materials = parts
|
self.materials = parts
|
||||||
|
|
||||||
|
self.suspended_floor_insulation_parts = [
|
||||||
|
part for part in self.materials if part["type"] == "suspended_floor_insulation"
|
||||||
|
]
|
||||||
|
self.solid_floor_insulation_parts = [
|
||||||
|
part for part in self.materials if part["type"] == "solid_floor_insulation"
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _estimate_perimeter(floor_area, num_rooms):
|
def _estimate_perimeter(floor_area, num_rooms):
|
||||||
# Compute average room size based on total floor area and number of rooms
|
# Compute average room size based on total floor area and number of rooms
|
||||||
|
|
@ -266,11 +273,11 @@ class FloorRecommendations(BaseUtility):
|
||||||
|
|
||||||
if is_suspended:
|
if is_suspended:
|
||||||
# Given the U-value, we recommend underfloor insulation
|
# Given the U-value, we recommend underfloor insulation
|
||||||
self.recommend_floor_insulation(u_value=u_value, parts=suspended_floor_insulation_parts)
|
self.recommend_floor_insulation(u_value=u_value, parts=self.suspended_floor_insulation_parts)
|
||||||
|
|
||||||
if is_solid:
|
if is_solid:
|
||||||
# Given the U-value, we recommend solid floor insulation options which are usually solid foam
|
# Given the U-value, we recommend solid floor insulation options which are usually solid foam
|
||||||
self.recommend_floor_insulation(u_value=u_value, parts=solid_floor_insulation_parts)
|
self.recommend_floor_insulation(u_value=u_value, parts=self.solid_floor_insulation_parts)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _make_floor_description(part, depth):
|
def _make_floor_description(part, depth):
|
||||||
|
|
@ -284,7 +291,8 @@ class FloorRecommendations(BaseUtility):
|
||||||
|
|
||||||
lowest_selected_u_value = None
|
lowest_selected_u_value = None
|
||||||
for part in parts:
|
for part in parts:
|
||||||
for depth in part["depths"]:
|
for depth, cost_per_unit in zip(part["depths"], part["cost"]):
|
||||||
|
|
||||||
part_u_value = r_value_per_mm_to_u_value(depth, part["r_value_per_mm"])
|
part_u_value = r_value_per_mm_to_u_value(depth, part["r_value_per_mm"])
|
||||||
_, new_u_value = calculate_u_value_uplift(u_value, part_u_value)
|
_, new_u_value = calculate_u_value_uplift(u_value, part_u_value)
|
||||||
new_u_value = math.ceil(new_u_value * 100.0) / 100.0
|
new_u_value = math.ceil(new_u_value * 100.0) / 100.0
|
||||||
|
|
@ -300,13 +308,14 @@ class FloorRecommendations(BaseUtility):
|
||||||
self.recommendations.append(
|
self.recommendations.append(
|
||||||
{
|
{
|
||||||
"parts": [
|
"parts": [
|
||||||
get_recommended_part(part, depth),
|
get_recommended_part(part, depth, cost_per_unit),
|
||||||
],
|
],
|
||||||
"type": "floor_insulation",
|
"type": "floor_insulation",
|
||||||
"description": self._make_floor_description(part, depth),
|
"description": self._make_floor_description(part, depth),
|
||||||
"starting_u_value": u_value,
|
"starting_u_value": u_value,
|
||||||
"new_u_value": new_u_value,
|
"new_u_value": new_u_value,
|
||||||
"sap_points": estimate_sap_points()
|
"sap_points": estimate_sap_points(),
|
||||||
|
"cost": cost_per_unit * self.property.floor_area,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -310,7 +310,8 @@ class WallRecommendations(BaseUtility):
|
||||||
recommendations = []
|
recommendations = []
|
||||||
for part in parts:
|
for part in parts:
|
||||||
|
|
||||||
for depth in part["depths"]:
|
for depth, cost_per_unit in zip(part["depths"], part["cost"]):
|
||||||
|
|
||||||
part_u_value = r_value_per_mm_to_u_value(depth, part["r_value_per_mm"])
|
part_u_value = r_value_per_mm_to_u_value(depth, part["r_value_per_mm"])
|
||||||
|
|
||||||
_, new_u_value = calculate_u_value_uplift(u_value, part_u_value)
|
_, new_u_value = calculate_u_value_uplift(u_value, part_u_value)
|
||||||
|
|
@ -333,12 +334,13 @@ class WallRecommendations(BaseUtility):
|
||||||
|
|
||||||
recommendations.append(
|
recommendations.append(
|
||||||
{
|
{
|
||||||
"parts": [get_recommended_part(part, depth)],
|
"parts": [get_recommended_part(part, depth, cost_per_unit)],
|
||||||
"type": "wall_insulation",
|
"type": "wall_insulation",
|
||||||
"description": "Install " + self._make_description(part, depth),
|
"description": "Install " + self._make_description(part, depth),
|
||||||
"starting_u_value": u_value,
|
"starting_u_value": u_value,
|
||||||
"new_u_value": new_u_value,
|
"new_u_value": new_u_value,
|
||||||
"sap_points": estimate_sap_points(),
|
"sap_points": estimate_sap_points(),
|
||||||
|
"cost": cost_per_unit * self.property.insulation_wall_area,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -371,7 +373,10 @@ class WallRecommendations(BaseUtility):
|
||||||
# By looping through ewi first, if there is nothing there, that ensures not combinations are tested
|
# By looping through ewi first, if there is nothing there, that ensures not combinations are tested
|
||||||
for ewi_part in ewi_parts:
|
for ewi_part in ewi_parts:
|
||||||
for iwi_part in iwi_parts:
|
for iwi_part in iwi_parts:
|
||||||
for ewi_depth, iwi_depth in itertools.product(ewi_part["depths"], iwi_part["depths"]):
|
for (ewi_depth, ewi_cost_per_unit), (iwi_depth, iwi_cost_per_unit) in itertools.product(
|
||||||
|
zip(ewi_part["depths"], ewi_part["cost"]),
|
||||||
|
zip(iwi_part["depths"], iwi_part["cost"])
|
||||||
|
):
|
||||||
ewi_part_u_value = r_value_per_mm_to_u_value(ewi_depth, ewi_part["r_value_per_mm"])
|
ewi_part_u_value = r_value_per_mm_to_u_value(ewi_depth, ewi_part["r_value_per_mm"])
|
||||||
iwi_part_u_value = r_value_per_mm_to_u_value(iwi_depth, iwi_part["r_value_per_mm"])
|
iwi_part_u_value = r_value_per_mm_to_u_value(iwi_depth, iwi_part["r_value_per_mm"])
|
||||||
|
|
||||||
|
|
@ -391,8 +396,8 @@ class WallRecommendations(BaseUtility):
|
||||||
# For now, I'm adding them as separate items in the list
|
# For now, I'm adding them as separate items in the list
|
||||||
recommendation = {
|
recommendation = {
|
||||||
"parts": [
|
"parts": [
|
||||||
get_recommended_part(ewi_part, ewi_depth),
|
get_recommended_part(ewi_part, ewi_depth, ewi_cost_per_unit),
|
||||||
get_recommended_part(iwi_part, iwi_depth)
|
get_recommended_part(iwi_part, iwi_depth, iwi_cost_per_unit)
|
||||||
],
|
],
|
||||||
"type": "wall_insulation",
|
"type": "wall_insulation",
|
||||||
"description": (
|
"description": (
|
||||||
|
|
@ -401,7 +406,11 @@ class WallRecommendations(BaseUtility):
|
||||||
),
|
),
|
||||||
"starting_u_value": u_value,
|
"starting_u_value": u_value,
|
||||||
"new_u_value": combined_new_u_value,
|
"new_u_value": combined_new_u_value,
|
||||||
"sap_points": estimate_sap_points()
|
"sap_points": estimate_sap_points(),
|
||||||
|
"cost": (
|
||||||
|
ewi_cost_per_unit * self.property.insulation_wall_area + iwi_cost_per_unit *
|
||||||
|
self.property.insulation_wall_area
|
||||||
|
),
|
||||||
}
|
}
|
||||||
self.recommendations.append(recommendation)
|
self.recommendations.append(recommendation)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,15 +110,17 @@ def update_lowest_selected_u_value(lowest_selected_u_value, new_u_value):
|
||||||
return lowest_selected_u_value
|
return lowest_selected_u_value
|
||||||
|
|
||||||
|
|
||||||
def get_recommended_part(part, selected_depth):
|
def get_recommended_part(part, selected_depth, selected_cost):
|
||||||
"""
|
"""
|
||||||
Utility function to return a recommended part with the selected depth.
|
Utility function to return a recommended part with the selected depth.
|
||||||
:param part:
|
:param part: part to be recommended
|
||||||
:param selected_depth:
|
:param selected_depth: depth of the selected part
|
||||||
|
:param selected_cost: cost of the selected depth
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
recommended_part = deepcopy(part)
|
recommended_part = deepcopy(part)
|
||||||
recommended_part["depths"] = [selected_depth]
|
recommended_part["depths"] = [selected_depth]
|
||||||
|
recommended_part["cost"] = [selected_cost]
|
||||||
|
|
||||||
return recommended_part
|
return recommended_part
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue