diff --git a/recommendations/HeatingRecommender.py b/recommendations/HeatingRecommender.py index 2041f783..1b8c5035 100644 --- a/recommendations/HeatingRecommender.py +++ b/recommendations/HeatingRecommender.py @@ -8,6 +8,11 @@ from recommendations.HeatingControlRecommender import HeatingControlRecommender class HeatingRecommender: + ELECTRIC_HEATING_DESCRIPTIONS = [ + "Room heaters, electric", + "Electric storage heaters", + "Electric storage heaters, radiators" + ] def __init__(self, property_instance: Property): self.property = property_instance @@ -16,6 +21,23 @@ class HeatingRecommender: self.heating_recommendations = [] self.heating_control_recommendations = [] + self.has_electric_heating_description = ( + self.property.main_heating["clean_description"] in self.ELECTRIC_HEATING_DESCRIPTIONS + ) + + def is_high_heat_retention_valid(self): + """ + Check conditions if high heat retention storage is valid + :return: + """ + + no_heating_no_mains = ( + self.property.main_heating["clean_description"] in ["No system present, electric heaters assumed"] and + not self.property.data["mains-gas-flag"] + ) + + return self.has_electric_heating_description or no_heating_no_mains + def recommend(self, has_cavity_or_loft_recommendations, phase=0): """ Produces heating recommendations @@ -34,16 +56,7 @@ class HeatingRecommender: # This first iteration of the recommender will provide very basic recommendation # We recommend heating controls based on the main heating system - has_electric_heating_description = self.property.main_heating["clean_description"] in [ - "Room heaters, electric", "Electric storage heaters", "Electric storage heaters, radiators" - ] - - no_heating_no_mains = ( - self.property.main_heating["clean_description"] in ["No system present, electric heaters assumed"] and - not self.property.data["mains-gas-flag"] - ) - - if has_electric_heating_description or no_heating_no_mains: + if self.is_high_heat_retention_valid(): # Recommend high heat retention storage heaters self.recommend_hhr_storage_heaters(phase=phase, system_change=True, heating_controls_only=False) @@ -61,7 +74,7 @@ class HeatingRecommender: ) # We also check if the property has electric heating, but it has access to the mains gas - electic_heating_has_mains = has_electric_heating_description and self.property.data["mains-gas-flag"] + electic_heating_has_mains = self.has_electric_heating_description and self.property.data["mains-gas-flag"] portable_heaters_has_mains = ( self.property.main_heating["clean_description"] in ["Portable electric heaters assumed for most rooms"] and @@ -93,16 +106,19 @@ class HeatingRecommender: # In the future, we'll allow overrides, so that non-intrusive surveys can contradict these conditions # and either allow or prevent the recommendation of an air source heat pump - suitable_property_type = self.property.data["property-type"] in ["House", "Bungalow"] - has_air_source_heat_pump = self.property.main_heating["has_air_source_heat_pump"] - - if suitable_property_type and not has_air_source_heat_pump: + if self.is_ashp_valid(): self.recommend_air_source_heat_pump( phase=phase, has_cavity_or_loft_recommendations=has_cavity_or_loft_recommendations ) return + def is_ashp_valid(self): + suitable_property_type = self.property.data["property-type"] in ["House", "Bungalow"] + has_air_source_heat_pump = self.property.main_heating["has_air_source_heat_pump"] + + return suitable_property_type and not has_air_source_heat_pump + def recommend_air_source_heat_pump(self, phase, has_cavity_or_loft_recommendations, _return=False): """ This method will implement the recommendation for an air source heat pump diff --git a/recommendations/Mds.py b/recommendations/Mds.py index af0a0be8..ad3c4d2e 100644 --- a/recommendations/Mds.py +++ b/recommendations/Mds.py @@ -123,36 +123,58 @@ class Mds: continue # There are certain measures where we need to if measure == "external_wall_insulation": - # Check if the wall is solid - if self.property_instance.walls['is_solid_brick']: + # Check if the wall is not cavity since the other wall types can take external wall insulation + if self.wall_recommender.ewi_valid(): pruned_measures.append(measure) continue if measure == "cavity_wall_insulation": # Check if the wall is cavity - if self.property_instance.walls['is_cavity_wall']: + 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 - if self.property_instance.roof["is_pitched"]: + # Check if the roof is suitable for loft insulation and the loft isn't already done + if ( + self.property_instance.roof["is_pitched"] and + not self.roof_recommender.is_loft_already_insulated() + ): pruned_measures.append(measure) continue if measure == "solid_floor_insulation": # Check if the floor is solid - if self.property_instance.floor["is_solid"]: + if ( + self.property_instance.floor["is_solid"] and + self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] + ): pruned_measures.append(measure) continue if measure == "suspended_floor_insulation": # Check if the floor is suspended - if self.property_instance.floor["is_suspended"]: + if ( + self.property_instance.floor["is_suspended"] and + self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] + ): pruned_measures.append(measure) continue - pruned_measures.append(measure) + if measure == "high_heat_retention_storage_heaters": + if self.heating_recommender.is_high_heat_retention_valid(): + pruned_measures.append(measure) + continue + + if measure == "air_source_heat_pump": + if self.heating_recommender.is_ashp_valid(): + pruned_measures.append(measure) + continue + + raise NotImplementedError("Implement me") pruned_measures_formatted = [] for pm in pruned_measures: @@ -311,7 +333,6 @@ class Mds: if self.optimise_measures: measures_set = self.select_optimal_measure_set(self.property_instance.measures) - logger.info(f"Building recommendations for {len(measures_set)} combinations of measures") mds_recommendations_map = {} representative_recommendations_map = {} errors_map = {} diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 538d90e4..81f514b1 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -54,6 +54,13 @@ class RoofRecommendations: ] ] + # Extract the insulation thickness from the roof, which is used throughout this method + self.insulation_thickness = convert_thickness_to_numeric( + self.property.roof["insulation_thickness"], + self.property.roof["is_pitched"], + self.property.roof["is_flat"] + ) + def mds_loft_insulation(self, phase): """ For usages within the mds report @@ -62,18 +69,18 @@ class RoofRecommendations: """ self.recommendations = [] - insulation_thickness = convert_thickness_to_numeric( - self.property.roof["insulation_thickness"], - self.property.roof["is_pitched"], - self.property.roof["is_flat"] - ) - u_value = get_roof_u_value(**{**self.property.roof, "age_band": self.property.age_band}) - self.recommend_roof_insulation(u_value, insulation_thickness, self.property.roof, phase) + self.recommend_roof_insulation(u_value, self.insulation_thickness, self.property.roof, phase) return self.recommendations + def is_loft_already_insulated(self): + """ + Check if the loft is already insulated + """ + return (self.insulation_thickness > self.MINIMUM_LOFT_ISULATION_MM) and self.property.roof["is_pitched"] + def recommend(self, phase): if self.property.roof["has_dwelling_above"]: @@ -81,21 +88,15 @@ class RoofRecommendations: u_value = self.property.roof["thermal_transmittance"] - insulation_thickness = convert_thickness_to_numeric( - self.property.roof["insulation_thickness"], - self.property.roof["is_pitched"], - self.property.roof["is_flat"] - ) - # We check if the roof is already insulated and if so, we exit # Building regulations part L recommend installing at least 270mm of insulation, however generally we # experience diminishing returns in terms of SAP once we go beyond around 150mm of insulation # This only holds true for pitched roofs. - if (insulation_thickness > self.MINIMUM_LOFT_ISULATION_MM) and self.property.roof["is_pitched"]: + if self.is_loft_already_insulated(): return - if (insulation_thickness >= self.MINIMUM_FLAT_ROOF_ISULATION_MM) and self.property.roof["is_flat"]: + if (self.insulation_thickness >= self.MINIMUM_FLAT_ROOF_ISULATION_MM) and self.property.roof["is_flat"]: return if self.property.roof["is_roof_room"]: @@ -119,7 +120,7 @@ class RoofRecommendations: return if self.property.roof["is_pitched"] or self.property.roof["is_flat"]: - self.recommend_roof_insulation(u_value, insulation_thickness, self.property.roof, phase) + self.recommend_roof_insulation(u_value, self.insulation_thickness, self.property.roof, phase) return if self.property.roof["is_roof_room"]: diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index fcd8e2bd..868c08c0 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -112,11 +112,9 @@ class WallRecommendations(Definitions): self.external_wall_non_insulation_materials = [ part for part in materials - if part["type"] - in ["ewi_wall_demolition", "ewi_wall_preparation", "ewi_wall_redecoration"] + if part["type"] in ["ewi_wall_demolition", "ewi_wall_preparation", "ewi_wall_redecoration"] ] - @property def ewi_valid(self): """ This method check available data, to determine if a property is suitable for external wall insulation @@ -126,11 +124,24 @@ class WallRecommendations(Definitions): # it is not suitable for EWI if self.property.restricted_measures or ( self.property.data["property-type"].lower() == "flat" + ) or ( + self.property.walls['is_cob'] or + self.property.walls['is_sandstone_or_limestone'] or + self.property.walls["is_cavity_wall"] ): return False return True + def is_suitable_for_solid_insulation(self): + """ + Checks if the wall is of a suitable type for internal/external wall insulation + """ + if self.property.walls["is_cavity_wall"] or self.property.walls["is_cob"]: + return False + + return True + def mds_recommend_cavity_wall_insulation(self, phase=None): # Function specifically for cavity wall insulation, for usage in the mds report self.recommendations = [] @@ -249,7 +260,7 @@ class WallRecommendations(Definitions): return # Remaining wall types are treated with IWI or EWI - if u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: + if (u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) and self.is_suitable_for_solid_insulation(): self.find_insulation(u_value, phase) return @@ -528,7 +539,7 @@ class WallRecommendations(Definitions): # consider diminishing returns between the two as they are considered to be separate measures ewi_recommendations = [] - if self.ewi_valid: + if self.ewi_valid(): ewi_recommendations = self._find_insulation( u_value=u_value, insulation_materials=pd.DataFrame(