import itertools from utils.logger import setup_logger from backend.Property import Property 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.SolarPvRecommendations import SolarPvRecommendations from recommendations.WindowsRecommendations import WindowsRecommendations from recommendations.HeatingRecommender import HeatingRecommender from recommendations.HotwaterRecommendations import HotwaterRecommendations from recommendations.SecondaryHeating import SecondaryHeating from recommendations.Recommendations import Recommendations logger = setup_logger() class Mds: """ Handles the contruction of the MDS report """ format_map = { "external_wall_insulation": "EWI (Trad Const)", "internal_wall_insualtion": "IWI", "cavity_wall_insulation": "CWI", "loft_insulation": "LI", "air_source_heat_pump": "ASHP Htg", "high_heat_retention_storage_heaters": "High Heat Retention Storage Heaters", "solar_pv": "Solar PV", } def __init__(self, property_instance: Property, materials, optimise_measures: bool = False): self.property_instance = property_instance self.floor_recommender = FloorRecommendations(property_instance=property_instance, materials=materials) self.wall_recommender = 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) self.solar_recommender = SolarPvRecommendations(property_instance=property_instance) self.heating_recommender = HeatingRecommender(property_instance=property_instance) self.hotwater_recommender = HotwaterRecommendations(property_instance=property_instance) self.secondary_heating_recommender = SecondaryHeating(property_instance=property_instance) # This flag indicates that we wish to optimise the measures, to the property, depending on the set of measures # we have been provided self.optimise_measures = optimise_measures def select_optimal_measure_set(self, measures): # This is the set all_considered_measures = [ 'external_wall_insulation', 'cavity_wall_insulation', 'loft_insulation', 'air_source_heat_pump', 'high_heat_retention_storage_heaters', 'solar_pv' ] # Check if our measures are within the ones we've handled new = [m for m in measures if m not in all_considered_measures] if new: raise NotImplementedError("New measures - handle me") def prune_options(options, measures): options_pruned = [] for _group in options: group_pruned = [m for m in _group if m in measures] if not group_pruned: continue options_pruned.append(group_pruned) return options_pruned # For options in here, a property could only possibly have one of these one_choice_options = [ ["external_wall_insulation", "cavity_wall_insulation", "internal_wall_insulation"], ["loft_insulation", "flat_roof_insulation", "room_in_roof_insulation"], ["solid_floor_insulation", "suspended_floor_insulation"], ] # prune one_choice_options based on the measure set considered for this property one_choice_options_pruned = prune_options(one_choice_options, measures) # For options in here, a property could have one or the other so all should be considered multi_path_options = [ ["air_source_heat_pump", "high_heat_retention_storage_heaters", "gas_boiler"] ] multi_path_options_pruned = prune_options(multi_path_options, measures) one_choice_combinations = [list(itertools.product(*one_choice_options_pruned))] one_choice_combinations = [list(x) for sublist in one_choice_combinations for x in sublist] multi_path_combinations = [list(itertools.product(*multi_path_options_pruned))] multi_path_combinations = [list(x) for sublist in multi_path_combinations for x in sublist] one_choice_flat = [item for sublist in one_choice_options_pruned for item in sublist] multi_path_flat = [item for sublist in multi_path_options_pruned for item in sublist] remaining_measures = [ measure for measure in measures if measure not in one_choice_flat and measure not in multi_path_flat ] # Combine one_choice and multi_path combinations with remaining measures final_combinations = [] for one_choice in one_choice_combinations: for multi_path in multi_path_combinations: final_combinations.append([m for m in one_choice + multi_path + remaining_measures]) pruned_combinations = [] # TODO: We can do these checks once, outside of the loop and prune the combinations for combination in final_combinations: pruned_measures = [] for measure in combination: if measure not in measures: continue # There are certain measures where we need to if measure == "external_wall_insulation": # Check if the wall is not cavity since the other wall types can take external wall insulation if ( self.wall_recommender.ewi_valid() and not self.property_instance.walls["insulation_thickness"] in ["average", "above average"] ): pruned_measures.append(measure) continue if measure == "cavity_wall_insulation": # Check if the wall is cavity if ( self.property_instance.walls['is_cavity_wall'] and not self.property_instance.walls['is_filled_cavity'] ): pruned_measures.append(measure) continue if measure == "loft_insulation": # Check if the roof is suitable for loft insulation and the loft isn't already done # Or, if the home had a u-value for the roof, we don't recommend loft insulation if ( self.property_instance.roof["is_pitched"] and not self.roof_recommender.is_loft_already_insulated() and self.property_instance.roof["thermal_transmittance_unit"] is None ): pruned_measures.append(measure) continue if measure == "solid_floor_insulation": # Check if the floor is solid if ( self.property_instance.floor["is_solid"] and self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] and self.property_instance.floor["thermal_transmittance_unit"] is not None ): pruned_measures.append(measure) continue if measure == "suspended_floor_insulation": # Check if the floor is suspended if ( self.property_instance.floor["is_suspended"] and self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] and self.property_instance.floor["thermal_transmittance_unit"] is not None ): pruned_measures.append(measure) continue if measure == "high_heat_retention_storage_heaters": # For the moment, we recommend storage heaters if the property doesn't already # and don't make it contngent on controls already_has_hhr = self.heating_recommender.is_hhr_already_installed() if ( self.heating_recommender.is_high_heat_retention_valid() and not already_has_hhr ): pruned_measures.append(measure) continue if measure == "air_source_heat_pump": if self.heating_recommender.is_ashp_valid(): pruned_measures.append(measure) continue if measure == "solar_pv": if self.solar_recommender.is_solar_pv_valid(): pruned_measures.append(measure) continue raise NotImplementedError("Implement me") if not pruned_measures: continue pruned_measures_formatted = [] for pm in pruned_measures: pruned_measures_formatted.append({pm: self.format_map[pm]}) pruned_combinations.append(pruned_measures_formatted) # We're left with the subset of measures that are possible for this property # These are the possible groups of measures that could be applied to this home return pruned_combinations def _build(self, measure_config_list, measures): not_implemented_measures = [ "party_wall_insulation", "ground_source_heat_pump", "shared_ground_loops", "communal_heat_networks", "district_heating_networks", "solar_thermal", "draught_proofing", "ev_charging", "battery", ] # Check if we have a not implemented measure if any([m in not_implemented_measures for m in measure_config_list]): raise NotImplementedError("Not implemented measure in the property - implement me") mds_recommendations = [] errors = [] phase = 0 # TODO: Could use a decarator to reduce the boilerplate code - insert_recommendation_id and then the append if "external_wall_insulation" in measure_config_list: recs = self.wall_recommender.mds_recommend_ewi(phase=phase) if not recs: raise Exception("No recommendations for external wall insulation") recs = self.insert_recommendation_id(recs, measures, "external_wall_insulation") mds_recommendations.append(recs) if self.optimise_measures and len(recs): phase += 1 if "cavity_wall_insulation" in measure_config_list: recs = self.wall_recommender.mds_recommend_cavity_wall_insulation(phase=phase) recs = self.insert_recommendation_id(recs, measures, "cavity_wall_insulation") mds_recommendations.append(recs) if self.optimise_measures and len(recs): phase += 1 if "loft_insulation" in measure_config_list: # Check if the roof is suitable for loft insulation if self.property_instance.roof['is_roof_room']: errors.append("Roof is a room") else: recs = self.roof_recommender.mds_loft_insulation(phase=phase) if not recs: raise Exception("No recommendations for loft insulation") recs = self.insert_recommendation_id(recs, measures, "loft_insulation") mds_recommendations.append(recs) if self.optimise_measures and len(recs): phase += 1 if "internal_wall_insulation" in measure_config_list: raise Exception("check me out 4") self.wall_recommender.recommend(phase=phase) if "suspended_floor_insulation" in measure_config_list: raise Exception("check me out 5") self.floor_recommender.recommend(phase=phase) if "solid_floor_insulation" in measure_config_list: raise Exception("check me out 6") self.floor_recommender.recommend(phase=phase) if "air_source_heat_pump" in measure_config_list: recs = self.heating_recommender.recommend_air_source_heat_pump( phase=phase, has_cavity_or_loft_recommendations=False, _return=True ) recs = self.insert_recommendation_id(recs, measures, "air_source_heat_pump") mds_recommendations.append(recs) if self.optimise_measures and len(recs): phase += 1 if "high_heat_retention_storage_heaters" in measure_config_list: recs = self.heating_recommender.recommend_hhr_storage_heaters( phase=phase, system_change=True, heating_controls_only=False, _return=True ) if recs is None: logger.info( f"No recommendations for high heat retention storage heaters, current heating " f"{self.property_instance.main_heating['clean_description']}" ) else: recs = self.insert_recommendation_id(recs, measures, "high_heat_retention_storage_heaters") mds_recommendations.append(recs) if self.optimise_measures and len(recs): phase += 1 if "low_energy_lighting" in measure_config_list: raise Exception("check me out 9") self.lighting_recommender.recommend(phase=phase) if "cylinder_insulation" in measure_config_list: raise Exception("check me out 10") self.hotwater_recommender.recommend(phase=phase) if "smart_controls" in measure_config_list: raise Exception("check me out 11") self.heating_recommender.recommend(phase=phase) if "zone_controls" in measure_config_list: raise Exception("check me out 12") self.heating_recommender.recommend(phase=phase) if "trvs" in measure_config_list: raise Exception("check me out 13") self.heating_recommender.recommend(phase=phase) if "solar_pv" in measure_config_list: recs = self.solar_recommender.mds_recommend(phase=phase, solar_pv_percentage=0.5) recs = self.insert_recommendation_id(recs, measures, "solar_pv") mds_recommendations.append(recs) if self.optimise_measures and len(recs): phase += 1 if "double_glazing" in measure_config_list: raise Exception("check me out 15") self.windows_recommender.recommend(phase=phase) if "mechanical_ventilation" in measure_config_list: raise Exception("check me out 16") self.ventilation_recomender.recommend(phase=phase) if "gas_boiler" in measure_config_list: raise Exception("check me out 17") self.heating_recommender.recommend(phase=phase) if "flat_roof_insulation" in measure_config_list: raise Exception("check me out 18") self.roof_recommender.recommend(phase=phase) if "room_in_roof_insulation" in measure_config_list: raise Exception("check me out 19") self.roof_recommender.recommend(phase=phase) property_representative_recommendations = Recommendations.create_representative_recommendations( mds_recommendations, non_invasive_recommendations=[] ) return mds_recommendations, property_representative_recommendations, errors def build(self): if self.property_instance.measures is None: raise NotImplementedError("No measures in the property - implement me") if self.optimise_measures: measures_set = self.select_optimal_measure_set(self.property_instance.measures) mds_recommendations_map = {} representative_recommendations_map = {} errors_map = {} for measures in measures_set: measure_config_list = [list(x.keys())[0] for x in measures] mds_recommendations, rep_recommendations, errors = self._build( measure_config_list=measure_config_list, measures=measures ) if errors: logger.info(f"Errors: {errors}") mds_recommendations_map[str(measure_config_list)] = mds_recommendations representative_recommendations_map[str(measure_config_list)] = rep_recommendations errors_map[str(measure_config_list)] = errors return mds_recommendations_map, representative_recommendations_map, errors_map else: measure_config_list = [list(m.keys())[0] for m in self.property_instance.measures] return self._build(measure_config_list=measure_config_list, measures=self.property_instance.measures) @staticmethod def insert_recommendation_id(recommendations, measures, measure_name): # Insert the recommendation identifier into this recommendation measure_config = [m for m in measures if measure_name in m][0] idx = 0 for r in recommendations: r["recommendation_id"] = list(measure_config.values())[0] + "-" + str(idx) idx += 1 return recommendations