from backend.Property import Property from typing import List from recommendations.FloorRecommendations import FloorRecommendations from recommendations.WallRecommendations import WallRecommendations from recommendations.RoofRecommendations import RoofRecommendations from recommendations.VentilationRecommendations import VentilationRecommendations from recommendations.FireplaceRecommendations import FireplaceRecommendations from recommendations.LightingRecommendations import LightingRecommendations from recommendations.WindowsRecommendations import WindowsRecommendations from backend.ml_models.AnnualBillSavings import AnnualBillSavings class Recommendations: """ High level recommendations class, which sits above the measure specific recommendation classes """ def __init__( self, property_instance: Property, materials: List ): """ :param property_instance: Instance of the Property class, for the home associated to property_id :param materials: List of materials to be used in the recommendations """ self.property_instance = property_instance self.materials = materials self.floor_recommender = FloorRecommendations(property_instance=property_instance, materials=materials) self.wall_recomender = WallRecommendations(property_instance=property_instance, materials=materials) self.roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials) self.ventilation_recomender = VentilationRecommendations( property_instance=property_instance, materials=materials ) self.fireplace_recommender = FireplaceRecommendations(property_instance=property_instance) self.lighting_recommender = LightingRecommendations(property_instance=property_instance, materials=materials) self.windows_recommender = WindowsRecommendations(property_instance=property_instance, materials=materials) def recommend(self): """ This method runs the recommendations for the individual measures and then appends them to a list for output :return: """ property_recommendations = [] # Floor recommendations self.floor_recommender.recommend() if self.floor_recommender.recommendations: property_recommendations.append(self.floor_recommender.recommendations) # Wall recommendations self.wall_recomender.recommend() if self.wall_recomender.recommendations: property_recommendations.append(self.wall_recomender.recommendations) # Roof recommendations self.roof_recommender.recommend() if self.roof_recommender.recommendations: property_recommendations.append(self.roof_recommender.recommendations) # Ventilation recommendations # We only produce a ventilation recommendation if the property is recommended to have wall or roof insulation if self.wall_recomender.recommendations or self.roof_recommender.recommendations: self.ventilation_recomender.recommend() if self.ventilation_recomender.recommendation: property_recommendations.append(self.ventilation_recomender.recommendation) # Fireplace sealing recommendations self.fireplace_recommender.recommend() if self.fireplace_recommender.recommendation: property_recommendations.append(self.fireplace_recommender.recommendation) # Lighting recommendations self.lighting_recommender.recommend() if self.lighting_recommender.recommendation: property_recommendations.append(self.lighting_recommender.recommendation) # Windows recommendations self.windows_recommender.recommend() if self.windows_recommender.recommendation: property_recommendations.append(self.windows_recommender.recommendation) # We insert temporary ids into the recommendations which is important for the optimiser later property_recommendations = self.insert_temp_recommendation_id(property_recommendations) return property_recommendations @staticmethod 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 property_recommendations: nested list of recommendations, grouped by data_types :return: Updated recommendations_to_upload, where where recommendation has a "recommendation_id" integer inserted """ idx = 0 for recs in property_recommendations: for rec in recs: rec["recommendation_id"] = idx idx += 1 return property_recommendations @classmethod def calculate_recommendation_impact(cls, property_instance, all_predictions, recommendations): """ Given predictions from the model apis, with method will update the recommendations with the predicted impact of the recommendation on the property :param property_instance: Instance of the Property class, for the home associated to property_id :param all_predictions: dictionary of predictions from the model apis :param recommendations: dictionary of recommendations for the property :return: """ property_sap_predictions = all_predictions["sap_change_predictions"][ all_predictions["sap_change_predictions"]["property_id"] == str(property_instance.id) ] property_heat_predictions = all_predictions["heat_demand_predictions"][ all_predictions["heat_demand_predictions"]["property_id"] == str(property_instance.id) ] property_carbon_predictions = all_predictions["carbon_change_predictions"][ all_predictions["carbon_change_predictions"]["property_id"] == str(property_instance.id) ] property_recommendations = recommendations[property_instance.id].copy() for recommendations_by_type in property_recommendations: for rec in recommendations_by_type: new_heat_demand = property_heat_predictions[property_heat_predictions["recommendation_id"] == str( rec["recommendation_id"] )]["predictions"].values[0] new_carbon = property_carbon_predictions[property_carbon_predictions["recommendation_id"] == str( rec["recommendation_id"] )]["predictions"].values[0] # We don't use the model for low energy lighting at the moment if rec["type"] != "low_energy_lighting": new_sap = property_sap_predictions[property_sap_predictions["recommendation_id"] == str( rec["recommendation_id"] )]["predictions"].values[0] rec["sap_points"] = new_sap - float(property_instance.data["current-energy-efficiency"]) if rec["type"] == "mechanical_ventilation": # For the moment, we cap the number of SAP points that can be achieved by ventilation at 2 rec["sap_points"] = min(rec["sap_points"], VentilationRecommendations.SAP_LIMIT) # Round to 2 decimal places rec["sap_points"] = round(rec["sap_points"], 2) rec["co2_equivalent_savings"] = float(property_instance.data["co2-emissions-current"]) - new_carbon # Energy consumption current is per meter squared, so we need to multiply by the floor area to get # an absolute figure for the home rec["heat_demand"] = ( (float(property_instance.data["energy-consumption-current"]) - new_heat_demand ) * property_instance.floor_area) rec["energy_cost_savings"] = AnnualBillSavings.estimate(rec["heat_demand"]) if (rec["sap_points"] is None) and (rec["co2_equivalent_savings"] is None) or ( rec["heat_demand"] is None) or (rec["energy_cost_savings"] is None): raise ValueError("sap points, co2 or heat demand is missing") return property_recommendations