diff --git a/recommendations/Mds.py b/recommendations/Mds.py deleted file mode 100644 index 4c417447..00000000 --- a/recommendations/Mds.py +++ /dev/null @@ -1,392 +0,0 @@ -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 diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index 80cc06b4..2d56eda9 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -581,10 +581,10 @@ class Recommendations: ) -> dict: if rec_phase == starting_phase: return { - "sap": float(property_instance.data["current-energy-efficiency"]), - "sap_prediction": float(property_instance.data["current-energy-efficiency"]), - "carbon": float(property_instance.data["co2-emissions-current"]), - "heat_demand": float(property_instance.data["energy-consumption-current"]), + "sap": float(property_instance.epc_record.current_energy_efficiency), + "sap_prediction": float(property_instance.epc_record.current_energy_efficiency), + "carbon": float(property_instance.epc_record.co2_emissions_current), + "heat_demand": float(property_instance.epc_record.energy_consumption_current), } previous_phase_reps = [ @@ -599,10 +599,10 @@ class Recommendations: # run the next step and run a median of nothing, which will return None if not previous_phase_reps: return { - "sap": float(property_instance.data["current-energy-efficiency"]), - "sap_prediction": float(property_instance.data["current-energy-efficiency"]), - "carbon": float(property_instance.data["co2-emissions-current"]), - "heat_demand": float(property_instance.data["energy-consumption-current"]), + "sap": property_instance.epc_record.current_energy_efficiency, + "sap_prediction": property_instance.epc_record.current_energy_efficiency, + "carbon": property_instance.epc_record.co2_emissions_current, + "heat_demand": property_instance.epc_record.energy_consumption_current, } # Median fallback (including zero-length case) @@ -707,7 +707,7 @@ class Recommendations: # For the moment, we cap the number of SAP points that can be achieved by LEDs at 2 if rec["type"] == "low_energy_lighting": lighting_sap_limit = LightingRecommendations.get_sap_limit( - property_instance.data["lighting-energy-eff"], + property_instance.epc_record.lighting_energy_eff, property_instance.lighting["low_energy_proportion"] ) @@ -802,7 +802,7 @@ class Recommendations: # By limiting here, we don't change the value in current_phase_values. This means that the # future recommendations won't have an impact that is too large li_sap_limit = RoofRecommendations.get_loft_insulation_sap_limit( - property_instance.data["roof-energy-eff"], property_instance.roof["insulation_thickness"] + property_instance.epc_record.roof_energy_eff, property_instance.roof["insulation_thickness"] ) if li_sap_limit is not None: new_value = min(property_phase_impact["sap"], li_sap_limit) @@ -1246,9 +1246,9 @@ class Recommendations: { "id": STARTING_DUMMY_ID_VALUE, **cls.map_descriptions_to_fuel( - property_instance.data["mainheat-description"], - property_instance.data["hotwater-description"], - property_instance.data["main-fuel"], + property_instance.epc_record.mainheat_description, + property_instance.epc_record.hotwater_description, + property_instance.epc_record.main_fuel, descriptions_to_fuel_types ) } @@ -1271,7 +1271,7 @@ class Recommendations: # 2) Have an average efficiency boiler, we adjust the COP of the existing boiler down to 75% heating_upgrades = [x for x in property_recommendations if x[0]["type"] == "heating"] boiler_upgrade = [r for recs in heating_upgrades for r in recs if r["measure_type"] == "boiler_upgrade"] - existing_heating_efficiency = property_instance.data["mainheat-energy-eff"] + existing_heating_efficiency = property_instance.epc_record.mainheat_energy_eff if len(boiler_upgrade) and existing_heating_efficiency in ["Very Poor", "Poor", "Average"]: efficiency_map = {"Very Poor": 0.6, "Poor": 0.65, "Average": 0.7} diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 0021edcc..3f434976 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -800,7 +800,7 @@ class RoofRecommendations: if proposed_depth >= 300: new_efficiency = "Very Good" else: - if self.property.data["roof-energy-eff"] not in ["Good", "Very Good"]: + if self.property.epc_record.roof_energy_eff not in ["Good", "Very Good"]: new_efficiency = "Good" else: new_efficiency = "Very Good" @@ -959,10 +959,10 @@ class RoofRecommendations: roof_simulation_config = check_simulation_difference( new_config=roof_ending_config, old_config=self.property.roof, prefix="roof_" ) - if self.property.data["roof-energy-eff"] in ["Very Poor", "Poor"]: + if self.property.epc_record.roof_energy_eff in ["Very Poor", "Poor"]: new_efficiency = "Average" else: - new_efficiency = self.property.data["roof-energy-eff"] + new_efficiency = self.property.epc_record.roof_energy_eff if default_u_values: new_u_value = get_roof_u_value( diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index 38b206da..2a96da28 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -129,7 +129,7 @@ class WallRecommendations(Definitions): # Current logic: If the property is in a conservation area/heritage building/listed building or a flat, # it is not suitable for EWI if self.property.restricted_measures or ( - self.property.data["property-type"].lower() == "flat" + self.property.epc_record.property_type.lower() == "flat" ) or ( self.property.walls['is_cob'] or self.property.walls['is_sandstone_or_limestone'] or @@ -181,7 +181,7 @@ class WallRecommendations(Definitions): # If the property is a new build and the U-value is below 0.75, we don't recommend insulation because it's # not practical - if (self.property.data["transaction-type"] == "new dwelling") and ( + if (self.property.epc_record.transaction_type == "new dwelling") and ( u_value <= self.NEW_BUILD_INSULATED ): # Recommend nothing @@ -480,13 +480,13 @@ class WallRecommendations(Definitions): x["construction-age-band"] == self.property.construction_age_band ][0] - if self.property.data["walls-energy-eff"] == "Good" and efficiency_data["walls-energy-eff"] not in [ + if self.property.epc_record.walls_energy_eff == "Good" and efficiency_data["walls-energy-eff"] not in [ "Good", "Very Good" ]: simulation_config = { - "walls_energy_eff_ending": self.property.data["walls-energy-eff"] + "walls_energy_eff_ending": self.property.epc_record.walls_energy_eff } - elif self.property.data["walls-energy-eff"] == "Very Good": + elif self.property.epc_record.walls_energy_eff == "Very Good": simulation_config = { "walls_energy_eff_ending": "Very Good" } diff --git a/recommendations/WindowsRecommendations.py b/recommendations/WindowsRecommendations.py index 917a1667..8940148d 100644 --- a/recommendations/WindowsRecommendations.py +++ b/recommendations/WindowsRecommendations.py @@ -75,7 +75,7 @@ class WindowsRecommendations: # If the property currently has some secondary glazing but isn't in a conservation area # is_secondary_glazing = self.property.restricted_measures and ( - self.property.data["windows-energy-eff"] in ["Poor", "Very Poor"] + self.property.epc_record.windows_energy_eff in ["Poor", "Very Poor"] ) # We check if the windows are partially insulated but we're recommending double glazing as a complete @@ -90,17 +90,17 @@ class WindowsRecommendations: raise ValueError("Number of windows not specified") # We scale the number of windows based on the proportion of existing glazing - if self.property.data["multi-glaze-proportion"] != "": + if self.property.epc_record.multi_glaze_proportion != "": if (self.property.windows["clean_description"] == "Some double glazing") and ( - self.property.data["windows-energy-eff"] == "Very Poor") and ( - self.property.data["multi-glaze-proportion"] == 100 + self.property.epc_record.windows_energy_eff == "Very Poor") and ( + self.property.epc_record.multi_glaze_proportion == 100 ): # In this case, we assume all of the dinwos need replacing n_windows_scalar = 1 else: n_windows_scalar = 1 - ( - int(self.property.data["multi-glaze-proportion"]) / 100 + int(self.property.epc_record.multi_glaze_proportion) / 100 ) else: n_windows_scalar = self.COVERAGE_MAP.get( @@ -186,7 +186,7 @@ class WindowsRecommendations: glazed_type_ending = "double glazing installed during or after 2002" new_windows_description = "Fully double glazed" else: - if self.property.data["multi-glaze-proportion"] < 50: + if self.property.epc_record.multi_glaze_proportion < 50: glazed_type_ending = "secondary glazing" else: glazed_type_ending = "double glazing installed during or after 2002" @@ -203,7 +203,7 @@ class WindowsRecommendations: glazed_type_ending = "secondary glazing" new_windows_description = "Full secondary glazing" else: - if self.property.data["multi-glaze-proportion"] < 50: + if self.property.epc_record.multi_glaze_proportion < 50: glazed_type_ending = "double glazing installed during or after 2002" else: glazed_type_ending = "secondary glazing" @@ -214,7 +214,7 @@ class WindowsRecommendations: else: raise ValueError("Invalid glazing type - implement me") - if self.property.data["windows-energy-eff"] == "Very Good": + if self.property.epc_record.windows_energy_eff == "Very Good": windows_energy_eff = "Very Good" # For post 2002 windows, the energy efficiency is "Good" and so for the simulation, we simulate with "Good"