From c2062507cab66b0d9ec37d7b3021b3353a052409 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 15 Mar 2025 17:34:55 +0000 Subject: [PATCH] implementing mv --- backend/Property.py | 9 +-- etl/epc/Dataset.py | 16 ++--- etl/epc/Record.py | 26 +++++--- recommendations/Costs.py | 7 +++ recommendations/HeatingControlRecommender.py | 43 ++++++++----- recommendations/HeatingRecommender.py | 21 +++---- recommendations/Recommendations.py | 62 ++++++++++++------- recommendations/VentilationRecommendations.py | 12 +++- recommendations/county_to_region.py | 5 +- 9 files changed, 120 insertions(+), 81 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index 498fe0e0..b9c88bc2 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -380,7 +380,7 @@ class Property: for rec in property_recommendations_by_phase: # We simulate the impact of the recommendation at this current phase, and all of the prior phases - if rec["type"] in ["mechanical_ventilation", "trickle_vents", "draught_proofing"]: + if rec["type"] in ["trickle_vents", "draught_proofing"]: continue scoring_dict = self.create_recommendation_scoring_data( @@ -388,7 +388,6 @@ class Property: recommendation_record=recommendation_record, recommendations=previous_phase_representatives + [rec], primary_recommendation_id=rec["recommendation_id"], - non_invasive_recommendations=self.non_invasive_recommendations, ) self.recommendations_scoring_data.append(scoring_dict) @@ -494,7 +493,6 @@ class Property: recommendation_record, recommendations: list, primary_recommendation_id: int, - non_invasive_recommendations: list = None, ): """ This function will iterate through a list of recommendations and apply a simulation for each recommendation @@ -503,7 +501,6 @@ class Property: :param recommendation_record: The record of the property, which will be updated :param recommendations: The list of recommendations to apply :param primary_recommendation_id: The id of the primary recommendation, which is used to identify the record - :param non_invasive_recommendations: The list of non-invasive recommendations :return: The updated recommendation record """ @@ -532,7 +529,7 @@ class Property: "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation", "cylinder_thermostat", "loft_insulation", "room_roof_insulation", "flat_roof_insulation", "solid_floor_insulation", "suspended_floor_insulation", "mixed_glazing", - "windows_glazing" + "windows_glazing", "mechanical_ventilation" ]: # We update the data, as defined in the recommendaton for prefix in ["walls", "roof", "floor"]: @@ -558,7 +555,7 @@ class Property: "solid_floor_insulation", "suspended_floor_insulation", "windows_glazing", "solar_pv", "heating", "hot_water_tank_insulation", "heating_control", "secondary_heating", "cylinder_thermostat", "mixed_glazing", - "extension_cavity_wall_insulation", + "extension_cavity_wall_insulation", "mechanical_ventilation", ]: raise NotImplementedError( "Implement me, given type %s" % recommendation["type"] diff --git a/etl/epc/Dataset.py b/etl/epc/Dataset.py index 3f2e810e..83a85b78 100644 --- a/etl/epc/Dataset.py +++ b/etl/epc/Dataset.py @@ -203,11 +203,11 @@ class TrainingDataset(BaseDataset): common_cols = [[col + "_starting", col + "_ending"] for col in common_cols] self.df = self.df.loc[ - :, - no_suffix_cols - + only_ending_cols - + [col for cols in common_cols for col in cols], - ] + :, + no_suffix_cols + + only_ending_cols + + [col for cols in common_cols for col in cols], + ] def _remove_abnormal_change_in_floor_area(self): """ @@ -511,7 +511,7 @@ class TrainingDataset(BaseDataset): expanded_df["is_sandstone_or_limestone"] == expanded_df["is_sandstone_or_limestone_ending"] ) - ] + ] elif component == "floor": expanded_df = expanded_df[ (expanded_df["is_suspended"] == expanded_df["is_suspended_ending"]) @@ -528,7 +528,7 @@ class TrainingDataset(BaseDataset): expanded_df["is_to_external_air"] == expanded_df["is_to_external_air_ending"] ) - ] + ] elif component == "roof": expanded_df = expanded_df[ (expanded_df["is_pitched"] == expanded_df["is_pitched_ending"]) @@ -541,7 +541,7 @@ class TrainingDataset(BaseDataset): expanded_df["has_dwelling_above"] == expanded_df["has_dwelling_above_ending"] ) - ] + ] return expanded_df diff --git a/etl/epc/Record.py b/etl/epc/Record.py index 558dbacb..9ff1de0a 100644 --- a/etl/epc/Record.py +++ b/etl/epc/Record.py @@ -139,28 +139,22 @@ class EPCRecord: self._clean_records_using_epc_records() self._clean_with_data_processor() - self._expand_prepared_epc_to_attributes() - self._identify_delta_between_prepared_and_original_records() # Process to create uvalues for the single epc record - - # selff.df = self.epc_record_as_dataframe('prepared_epc') - + # self.df = self.epc_record_as_dataframe('prepared_epc') # self._feature_generation() # self._drop_features() return - self._expand_description_to_features() - self._expand_description_to_uvalues() - + # self._expand_description_to_features() + # self._expand_description_to_uvalues() + # # self._generate_uvalues() # self._validate_expanded_description() # self._validate_u_values() - # etc - pass def _drop_features(self): """ @@ -360,6 +354,7 @@ class EPCRecord: self._clean_number_lighting_outlets() self._clean_floor_level() self._clean_floor_height() + self._clean_constituency() # self._clean_potential_energy_efficiency() # self._clean_environment_impact_potential() @@ -402,6 +397,17 @@ class EPCRecord: if self.prepared_epc["floor-height"] <= 1.665: self.prepared_epc["floor-height"] = average + def _clean_constituency(self): + """ + We handle the single case of finding a missing constituency by using the local authority + """ + if pd.isnull(self.prepared_epc["constituency"]) or (self.prepared_epc["constituency"] == ""): + if self.prepared_epc["local-authority"] != "E06000044": + raise NotImplementedError( + "This function is only implemented for Portsmouth, in the single edgecase seen" + ) + self.prepared_epc["constituency"] = "E14000883" + def _clean_floor_level(self): """ This method will clean the floor level, if empty or invalid diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 2312dff2..4d25ec18 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -234,6 +234,13 @@ class Costs: if self.region is None: # Try and grab using the local-authority-label self.region = county_to_region_map.get(self.property.data["local-authority-label"], None) + + if self.region is None: + # Try and get the region after converting the keys to lower + self.region = { + k.lower(): v for k, v in county_to_region_map.items() + }.get(self.property.data["local-authority-label"].lower(), None) + if self.region is None: raise ValueError("Region not found in county map") diff --git a/recommendations/HeatingControlRecommender.py b/recommendations/HeatingControlRecommender.py index c613aa42..bd015a79 100644 --- a/recommendations/HeatingControlRecommender.py +++ b/recommendations/HeatingControlRecommender.py @@ -12,7 +12,7 @@ class HeatingControlRecommender: self.recommendation = [] - def recommend(self, heating_description, description_prefix="", description_suffix=""): + def recommend(self, heating_description, phase, description_prefix="", description_suffix=""): # TODO: Many of these functions are quite similar. We can possibly create a single wrapper function that # takes in the heating description and the description prefix/suffix, and then creates the appropriate @@ -23,32 +23,32 @@ class HeatingControlRecommender: # This first iteration of the recommender will provide very basic recommendation # We recommend heating controls based on the main heating system if heating_description in ["Room heaters, electric"]: - self.recommend_room_heaters_electric_controls() + self.recommend_room_heaters_electric_controls(phase=phase) return if heating_description in ["Electric storage heaters", "Electric storage heaters, radiators"]: - self.recommend_high_heat_retention_controls(description_prefix=description_prefix) + self.recommend_high_heat_retention_controls(description_prefix=description_prefix, phase=phase) return if heating_description in ["Boiler and radiators, mains gas"]: # We can recommend roomstat programmer trvs - self.recommend_roomstat_programmer_trvs(description_suffix=description_suffix) + self.recommend_roomstat_programmer_trvs(description_suffix=description_suffix, phase=phase) # We can also recommend time and temperature zone controls - self.recommend_time_temperature_zone_controls(description_suffix=description_suffix) + self.recommend_time_temperature_zone_controls(description_suffix=description_suffix, phase=phase) return if heating_description in ["Boiler and radiators, electric"]: - self.recommend_roomstat_programmer_trvs() + self.recommend_roomstat_programmer_trvs(phase=phase) return if heating_description in ["Air source heat pump, radiators, electric"]: # For an ASHP, we can recommend time and temperature zone controls, as well as programmer, trvs and a bypass # which are common configurations for ASHPs - self.recommend_time_temperature_zone_controls() + self.recommend_time_temperature_zone_controls(phase=phase) # self.recommend_programmer_trvs_bypass() - def recommend_room_heaters_electric_controls(self): + def recommend_room_heaters_electric_controls(self, phase): """ If the home has Room heaters, electric, we start by identifying potential heating controls that could be upgraded, that would provide a practical impact. This will be the least invasive improvement. @@ -88,6 +88,9 @@ class HeatingControlRecommender: self.recommendation.append( { + "phase": phase, + "type": "heating", + "measure_type": "programmer_appliance_thermostat", "description": "upgrade heating controls to Programmer and Appliance or Smart Thermostats", **self.costs.programmer_and_appliance_thermostat(has_programmer=has_programmer), "simulation_config": simulation_config @@ -97,7 +100,7 @@ class HeatingControlRecommender: # We don't implement any other recommendations right now return - def recommend_high_heat_retention_controls(self, description_prefix=""): + def recommend_high_heat_retention_controls(self, phase, description_prefix=""): """ When applicable, we recommend upgrading the heating controls to high heat retention controls. This is a specific type of control system that is designed to work with electric storage heaters. It is a more @@ -133,6 +136,9 @@ class HeatingControlRecommender: self.recommendation.append( { + "phase": phase, + "type": "heating", + "measure_type": "celect_type_controls", "description": "Upgrade heating controls to High Heat Retention Storage Heater Controls", **self.costs.celect_type_controls(), "simulation_config": simulation_config, @@ -143,7 +149,7 @@ class HeatingControlRecommender: # We don't implement any other recommendations right now return - def recommend_roomstat_programmer_trvs(self, description_suffix=""): + def recommend_roomstat_programmer_trvs(self, phase, description_suffix=""): """ If the home has a boiler and radiators, mains gas, we start by identifying potential heating controls that could be upgraded, that would provide a practical impact. @@ -208,15 +214,16 @@ class HeatingControlRecommender: description = "Upgrade heating controls to Room thermostat, programmer and TRVs" - already_installed = "heating_control" in self.property.already_installed + already_installed = "roomstat_programmer_trvs" in self.property.already_installed if already_installed: cost_result = override_costs(cost_result) description = "Heating controls have already been upgraded, no further action needed." self.recommendation.append( { - "type": "heating_control", + "type": "heating", "measure_type": "roomstat_programmer_trvs", + "phase": phase, "parts": [], "description": description, **cost_result, @@ -231,7 +238,7 @@ class HeatingControlRecommender: return - def recommend_time_temperature_zone_controls(self, description_suffix=""): + def recommend_time_temperature_zone_controls(self, phase, description_suffix=""): """ If the home has a boiler, we can recommend time and temperature zone controls. This is a more advanced and more efficient control system than the standard controls that come with a boiler. However, it may come @@ -282,14 +289,15 @@ class HeatingControlRecommender: "temperature zone control)" ) - already_installed = "heating_control" in self.property.already_installed + already_installed = "time_temperature_zone_control" in self.property.already_installed if already_installed: cost_result = override_costs(cost_result) description = "Heating controls have already been upgraded, no further action needed." self.recommendation.append( { - "type": "heating_control", + "type": "heating", + "phase": phase, "measure_type": "time_temperature_zone_control", "parts": [], "description": description, @@ -335,14 +343,15 @@ class HeatingControlRecommender: description = "Install a Bypass valve, TRVs and a Programmer" - already_installed = "heating_control" in self.property.already_installed + already_installed = "programmer_trvs_bypass" in self.property.already_installed if already_installed: cost_result = override_costs(cost_result) description = "Heating controls have already been upgraded, no further action needed." self.recommendation.append( { - "type": "heating_control", + "type": "heating", + "measure_type": "programmer_trvs_bypass", "parts": [], "description": description, **cost_result, diff --git a/recommendations/HeatingRecommender.py b/recommendations/HeatingRecommender.py index e4dd3a78..20f5e7ad 100644 --- a/recommendations/HeatingRecommender.py +++ b/recommendations/HeatingRecommender.py @@ -65,7 +65,6 @@ class HeatingRecommender: self.costs = Costs(self.property) self.heating_recommendations = [] - self.heating_control_recommendations = [] self.has_electric_heating_description = ( self.property.main_heating["has_electric"] or self.property.main_heating["has_electricaire"] @@ -259,7 +258,6 @@ class HeatingRecommender: "ashp_only_heating_recommendation", False ) self.heating_recommendations = [] - self.heating_control_recommendations = [] # This first iteration of the recommender will provide very basic recommendation # We recommend heating controls based on the main heating system @@ -302,7 +300,6 @@ class HeatingRecommender: self.recommend_air_source_heat_pump( phase=phase, has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations, - ) return @@ -360,7 +357,7 @@ class HeatingRecommender: } controls_recommender = HeatingControlRecommender(self.property) - controls_recommender.recommend(heating_description="Boiler and radiators, electric") + controls_recommender.recommend(heating_description="Boiler and radiators, electric", phase=phase) self.heating_recommendations.extend([boiler_recommendation] + controls_recommender.recommendation) return @@ -453,7 +450,7 @@ class HeatingRecommender: ), {}) controls_recommender = HeatingControlRecommender(self.property) - controls_recommender.recommend(heating_description="Air source heat pump, radiators, electric") + controls_recommender.recommend(heating_description="Air source heat pump, radiators, electric", phase=phase) ashp_size = self.size_heat_pump() ashp_costs = self.costs.air_source_heat_pump(ashp_size) @@ -805,7 +802,9 @@ class HeatingRecommender: description_prefix = "" controls_recommender.recommend( - heating_description="Electric storage heaters", description_prefix=description_prefix + heating_description="Electric storage heaters", + description_prefix=description_prefix, + phase=phase ) has_hhr = self.is_hhr_already_installed() @@ -1120,10 +1119,10 @@ class HeatingRecommender: description_suffix = "" controls_recommender.recommend( heating_description="Boiler and radiators, mains gas", - description_suffix=description_suffix + description_suffix=description_suffix, + phase=recommendation_phase ) # We may have 2 recommendations from the heating controls - if not controls_recommender.recommendation and not boiler_recommendation: return @@ -1161,10 +1160,6 @@ class HeatingRecommender: # 3) Heating controls only # But they are options that are not mutually exclusive # So, we actually set heating controls as a heating recommendation - for recommendation in controls_recommender.recommendation: - recommendation["phase"] = recommendation_phase - # recommendation["type"] = "heating" - - self.heating_control_recommendations.extend(controls_recommender.recommendation) + self.heating_recommendations.extend(controls_recommender.recommendation) return diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index 715332a5..edaa611a 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -149,9 +149,10 @@ class Recommendations: (self.wall_recomender.recommendations or self.roof_recommender.recommendations) and ("ventilation" in measures) ): - self.ventilation_recomender.recommend() + self.ventilation_recomender.recommend(phase=phase) if self.ventilation_recomender.recommendation: property_recommendations.append(self.ventilation_recomender.recommendation) + phase += 1 if "trickle_vents" in measures: # This is a recommendatin that typically comes from an energy assessment @@ -208,27 +209,25 @@ class Recommendations: measures=measures, has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations, ) - if ( - self.heating_recommender.heating_recommendations or - self.heating_recommender.heating_control_recommendations - ): + if self.heating_recommender.heating_recommendations: # We split into first and second phase recommendations first_phase_recommendations = [ r for r in ( - self.heating_recommender.heating_recommendations + - self.heating_recommender.heating_control_recommendations + self.heating_recommender.heating_recommendations ) if r["phase"] == phase ] second_phase_recommendations = [ r for r in ( - self.heating_recommender.heating_recommendations + - self.heating_recommender.heating_control_recommendations + self.heating_recommender.heating_recommendations ) if r["phase"] == phase + 1 ] + if first_phase_recommendations and second_phase_recommendations: + raise Exception("Imeplement me") + if first_phase_recommendations: property_recommendations.append(first_phase_recommendations) @@ -240,8 +239,7 @@ class Recommendations: # otherwise we incremenet by 1 max_used_phase = max( [rec["phase"] for rec in - self.heating_recommender.heating_recommendations + - self.heating_recommender.heating_control_recommendations] + self.heating_recommender.heating_recommendations] ) amount_to_increment = max_used_phase - phase + 1 phase += amount_to_increment @@ -306,7 +304,7 @@ class Recommendations: # want to include the cavity wall insulation recommendation in the defaults if recommendations_by_type[0].get("type") in [ - "mechanical_ventilation", "trickle_vents", "draught_proofing" + "trickle_vents", "draught_proofing" ]: continue @@ -480,12 +478,14 @@ class Recommendations: increasing_variables = ["sap"] decreasing_variables = ["carbon", "heat_demand"] + # If the recommendation is mechanical ventilation, we don't apply the rule that the new value should be higher + mv_increasing_variables = ["carbon", "heat_demand"] + mv_decreasing_variables = ["sap"] + impact_summary = [] for recommendations_by_type in property_recommendations: for rec in recommendations_by_type: - if rec["type"] in [ - "mechanical_ventilation", "trickle_vents", "draught_proofing", "extension_cavity_wall_insulation" - ]: + if rec["type"] in ["trickle_vents", "draught_proofing", "extension_cavity_wall_insulation"]: # We don't have a percieved sap impact of mechanical ventilation or trickle vents, and we don't # have the capacity to score draught proofing if rec["type"] == "extension_cavity_wall_insulation": @@ -571,13 +571,23 @@ class Recommendations: # For decreasing variables, the new value should be lower than the previous, otherwise we set it to # the previous # In either case, we adjudge the recommendation to have had no/negligible impact - for v in increasing_variables: + # However, if the recommendation is mechanical ventilation, this can have a negative SAP impact so + # we don't apply this rule + + if rec["type"] == "mechanical_ventilation": + phase_increasing_variables = mv_increasing_variables + phase_decreasing_variables = mv_decreasing_variables + else: + phase_increasing_variables = increasing_variables + phase_decreasing_variables = decreasing_variables + + for v in phase_increasing_variables: current_phase_values[v] = ( current_phase_values[v] if current_phase_values[v] > previous_phase_values[v] else previous_phase_values[v] ) for v in previous_phase_values: - if v in decreasing_variables: + if v in phase_decreasing_variables: current_phase_values[v] = ( current_phase_values[v] if current_phase_values[v] < previous_phase_values[v] else previous_phase_values[v] @@ -592,13 +602,19 @@ class Recommendations: "heat_demand": previous_phase_values["heat_demand"] - current_phase_values["heat_demand"], } - # Prevent from being negative + # Prevent from being negative - apart from ventilation for metric in ["sap", "carbon", "heat_demand"]: - property_phase_impact[metric] = ( - 0 if property_phase_impact[metric] < 0 else property_phase_impact[metric] - ) - if metric == "sap": - property_phase_impact[metric] = round(property_phase_impact[metric], 2) + if rec["type"] != "mechanical_ventilation": + property_phase_impact[metric] = ( + 0 if property_phase_impact[metric] < 0 else property_phase_impact[metric] + ) + if metric == "sap": + property_phase_impact[metric] = round(property_phase_impact[metric], 2) + else: + # We prevent these from being positive + property_phase_impact[metric] = ( + 0 if property_phase_impact[metric] > 0 else property_phase_impact[metric] + ) # For the moment, we cap the number of SAP points that can be achieved by LEDs at 2 if rec["type"] == "low_energy_lighting": diff --git a/recommendations/VentilationRecommendations.py b/recommendations/VentilationRecommendations.py index 9738b898..a82e4df5 100644 --- a/recommendations/VentilationRecommendations.py +++ b/recommendations/VentilationRecommendations.py @@ -29,7 +29,7 @@ class VentilationRecommendations(Definitions): def identify_ventilation(self): self.has_ventilaion = self.property.data["mechanical-ventilation"] in self.VENTILATION_DESCRIPTIONS - def recommend(self): + def recommend(self, phase): """ If there is no ventilation, we recommend installing ventilation @@ -63,7 +63,7 @@ class VentilationRecommendations(Definitions): # We recommend installing two mechanical ventilation systems self.recommendation = [ { - "phase": None, + "phase": phase, "parts": part, "type": part[0]["type"], "measure_type": "mechanical_ventilation", @@ -79,7 +79,13 @@ class VentilationRecommendations(Definitions): "total": estimated_cost, # We use a very simple and rough estimate of 4 hours per unit "labour_hours": labour_hours, - "labour_days": labour_days # Assume 8 hour day + "labour_days": labour_days, # Assume 8 hour day + "simulation_config": { + "mechanical_ventilation_ending": "mechanical, extract only", + }, + "description_simulation": { + "mechanical-ventilation": "mechanical, extract only" + } } ] diff --git a/recommendations/county_to_region.py b/recommendations/county_to_region.py index e84b5698..13c1cdaa 100644 --- a/recommendations/county_to_region.py +++ b/recommendations/county_to_region.py @@ -135,7 +135,10 @@ county_to_region_map = { 'Merthyr Tydfil': 'Wales', 'Monmouthshire': 'Wales', 'Mountain Ash': 'Wales', 'Neath Port Talbot': 'Wales', 'Newport': 'Wales', 'Pembrokeshire': 'Wales', 'Penarth': 'Wales', 'Pentre': 'Wales', 'Pontyclun': 'Wales', 'Pontypridd': 'Wales', 'Porth': 'Wales', 'Porthcawl': 'Wales', 'Powys': 'Wales', 'Rhondda Cynon Taff': 'Wales', - 'Rhoose': 'Wales', 'Sully': 'Wales', 'Swansea': 'Wales', 'The Vale of Glamorgan': 'Wales', 'Tonypandy': 'Wales', + 'Rhoose': 'Wales', 'Sully': 'Wales', 'Swansea': 'Wales', + 'The Vale of Glamorgan': 'Wales', + 'Vale of Glamorgan': 'Wales', + 'Tonypandy': 'Wales', 'Torfaen': 'Wales', 'Treharris': 'Wales', 'Treorchy': 'Wales', 'Wrexham': 'Wales', 'Birmingham': 'West Midlands', 'Bromsgrove': 'West Midlands', 'Cannock Chase': 'West Midlands', 'Coventry': 'West Midlands', 'Dudley': 'West Midlands', 'East Staffordshire': 'West Midlands', 'Herefordshire': 'West Midlands',