import pandas as pd from recommendations.Costs import Costs from recommendations.recommendation_utils import check_simulation_difference from backend.Property import Property from etl.epc_clean.epc_attributes.MainheatAttributes import MainHeatAttributes from etl.epc_clean.epc_attributes.HotWaterAttributes import HotWaterAttributes from etl.epc_clean.epc_attributes.MainFuelAttributes import MainFuelAttributes from recommendations.HeatingControlRecommender import HeatingControlRecommender class HeatingRecommender: def __init__(self, property_instance: Property): self.property = property_instance self.costs = Costs(self.property) self.recommendations = [] def recommend(self, phase=0): self.recommendations = [] # 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: # Recommend high heat retention storage heaters self.recommend_electric_storage_heaters(phase=phase, system_change=True, heating_controls_only=False) # if the property has mains heating with boiler and radiators, we recommend optimal heating controls has_boiler = self.property.main_heating["clean_description"] in ["Boiler and radiators, mains gas"] # We also check that the property doesn't have a heating system, but it has access to the mains gas no_heating_has_mains = self.property.main_heating["clean_description"] in [ 'No system present, electric heaters assumed' ] and self.property.data["mains-gas-flag"] # 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"] if has_boiler or no_heating_has_mains or electic_heating_has_mains: # This indicates that the home previously did not have a boiler in place and so would require # an overhaul to the system system_change = not has_boiler self.recommend_boiler_upgrades(phase=phase, system_change=system_change) return @staticmethod def check_simulation_difference(old_config, new_config): """ Given two dictionaries, that describe the heating control configurations, this method will compare the two and pick out the differences. These differences will be things that have been added and things that have been removed. This will be used to determine how we should be updating the configuration in the simulation :return: """ differences = {key + "_ending": new_config[key] for key in new_config if old_config[key] != new_config[key]} return differences @staticmethod def combine_heating_and_controls( controls_recommendations, heating_simulation_config, costs, description, phase, heating_controls_only, system_change ): """ Given a recommendation for heating controls, and a recommendation for the heating system, we combine the two into a single recommendation :param controls_recommendations: The heating controls recommendations :param heating_simulation_config: The simulation configuration for the heating system :param costs: The costs of the heating system :param description: The description of the recommendation :param phase: The phase of the recommendation :param heating_controls_only: If True, we will also add a recommendation for heating controls only :param system_change: Indicates if we are recommending a different type of heating system, compared to the current system. If we have a system change and we have a heat control recommendation, we only recommend both heating and controls together :return: """ # We produce recommendations with & without heating controls # We will also produce a recommendation for heating controls only heating_controls_switch = [True, False] if controls_recommendations else [False] if not heating_simulation_config: heating_controls_switch = [] if system_change and len(controls_recommendations): heating_controls_switch = [True] output = [] for controls_switch in heating_controls_switch: total_costs = costs.copy() recommendation_simulation_config = heating_simulation_config.copy() recommendation_description = description if controls_switch: # We add the costs of the heating controls, onto each key in the costs dictionary for key in total_costs: total_costs[key] += controls_recommendations[0][key] recommendation_simulation_config = { **recommendation_simulation_config, **controls_recommendations[0]["simulation_config"] } controls_description = controls_recommendations[0]['description'] # Make the first letter of the description lowercase controls_description = ( controls_description[0].lower() + controls_description[1:] ) recommendation_description = f"{description} and {controls_description}" recommendation = { "phase": phase, "parts": [ # TODO ], "type": "heating", "description": recommendation_description, "starting_u_value": None, "new_u_value": None, "sap_points": None, **total_costs, "simulation_config": recommendation_simulation_config } output.append(recommendation) if heating_controls_only and len(controls_recommendations): # Also add on a recommendation for heating controls only heating_control_recommendation = controls_recommendations[0].copy() # Capitalize the first letter of the description heating_control_recommendation["description"] = ( heating_control_recommendation["description"][0].upper() + heating_control_recommendation["description"][1:] ) output.append( { "phase": phase, "parts": [ # TODO ], "type": "heating", "starting_u_value": None, "new_u_value": None, "sap_points": None, **heating_control_recommendation } ) return output def recommend_electric_storage_heaters(self, phase, system_change, heating_controls_only): """ We recommend electric storage heaters as an upgrade to the heating system. We will recommend upgrading to a high heat retention storage system, if the current system is not already high heat retention storage :param phase: The phase of the recommendation :param system_change: Indicates if we are recommending a different type of heating system, compared to the current system :param heating_controls_only: Indicates if we should include a recommendation for just heating controls :return: """ controls_recommender = HeatingControlRecommender(self.property) # The heating controls we're recommending for are based on the recommended heating system high_heat_retention_contols_desc = "Controls for high heat retention storage heaters" # We only recommend Celect-type controls if the current heating system is not Celect-type controls if self.property.main_heating_controls["clean_description"] != high_heat_retention_contols_desc: controls_recommender.recommend(heating_description="Electric storage heaters, radiators") # Conditions for not needing this recommendation already_installed_hh_retention = ( "Electric storage heaters" in self.property.main_heating["clean_description"] and self.property.main_heating_controls["clean_description"].lower() == high_heat_retention_contols_desc.lower() ) # Conditions for not recommending electric storage heaters if already_installed_hh_retention: # No recommendation needed return # Set up artefacts, suitable for the simulation and regardless of controls heating_ending_config = MainHeatAttributes("Electric storage heaters, radiators").process() heating_simulation_config = check_simulation_difference( new_config=heating_ending_config, old_config=self.property.main_heating ) # This upgrade will only take the heating system to average energy efficiency heating_simulation_config["mainheat_energy_eff_ending"] = "Average" # If the property is off-gas and has no heating system in place, the number of heated rooms will actually # be 0, so we use the number of rooms as the figure number_heated_rooms = ( self.property.data["number-heated-rooms"] if self.property.data["number-heated-rooms"] > 0 else ( self.property.number_of_rooms - 1 if self.property.number_of_rooms > 1 else self.property.number_of_rooms ) ) # Upgrade to electric storage heaters costs = self.costs.high_heat_electric_storage_heaters( number_heated_rooms=number_heated_rooms ) description = "Install high heat retention electric storage heaters" recommendations = self.combine_heating_and_controls( controls_recommendations=controls_recommender.recommendation, heating_simulation_config=heating_simulation_config, costs=costs, description=description, phase=phase, heating_controls_only=heating_controls_only, system_change=system_change ) self.recommendations.extend(recommendations) @staticmethod def estimate_boiler_size(property_type, built_form, floor_area, floor_height, num_heated_rooms): # Step 1: Base size estimation based on property type (as a starting point) base_size = { 'Flat': 25, 'House': 30, 'Maisonette': 28, 'Bungalow': 27 } # Step 2: Calculate the volume of the property volume = floor_area * floor_height # Step 3: Adjust base size for built form (to account for heat retention) form_adjustment = { 'Mid-Terrace': 0, 'End-Terrace': 2, 'Semi-Detached': 4, 'Detached': 6 } # Step 4: Further adjust for the total volume and number of heated rooms volume_adjustment = (volume / 100) # Simplified adjustment factor for volume rooms_adjustment = (num_heated_rooms - 5) * 0.5 # Assuming base case of 5 rooms # Calculate the estimated boiler size estimated_size = base_size[property_type] + form_adjustment[built_form] + volume_adjustment + rooms_adjustment # Step 5: Align with available boiler sizes and ensure it does not exceed 35kW, as it's rare to need more available_sizes = [30, 35, 40, 45, 50] estimated_size = min(max(estimated_size, 30), 40) # Ensure within 30kW to 35kW range # Find the closest available size (in this case, either rounding up or down to align with 30 or 35) closest_size = min(available_sizes, key=lambda x: abs(x - estimated_size)) return closest_size def recommend_boiler_upgrades(self, phase, system_change): """ This boiler recommendation will only recommend a like-for-like upgrade, since changing the system is generally more expensive :param phase: :param system_change: Indicates if the property would be undergoing a heating system change. This could be true if the home didn't have a heating system in place, or if the home had electric heating previously :return: """ recommendation_phase = phase # We now recommend boiler upgrades, if applicable simulation_config = {} boiler_costs = {} if self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor", "Average"]: boiler_size = self.estimate_boiler_size( property_type=self.property.data["property-type"], built_form=self.property.data["built-form"], floor_area=self.property.floor_area, floor_height=self.property.floor_height, num_heated_rooms=self.property.data["number-heated-rooms"], ) # We recommend a combi boiler under the following conditions # 1) If there are 4 or fewer rooms (we don't use heqted rooms because none of the rooms could be # heated if there is no existing heating system). # 2) There is more than 1 bathroom is_combi = ( (self.property.data["number-heated-rooms"] <= 4) or (self.property.n_bathrooms not in [None, 0, 1]) ) if is_combi: description = "Upgrade to a new combi boiler" else: description = "Upgrade to a new gas condensing boiler" simulation_config = {"mainheat_energy_eff_ending": "Good"} if system_change: # Installation of a boiler improves the hot water system so we need to reflect this in # the outcome of the recommendation heating_ending_config = MainHeatAttributes("Boiler and radiators, mains gas").process() hotwater_ending_config = HotWaterAttributes("From main system").process() fuel_ending_config = MainFuelAttributes("mains gas (not community)").process() heating_simulation_config = check_simulation_difference( new_config=heating_ending_config, old_config=self.property.main_heating ) hotwater_simulation_config = check_simulation_difference( new_config=hotwater_ending_config, old_config=self.property.hotwater ) fuel_simulation_config = check_simulation_difference( new_config=fuel_ending_config, old_config=self.property.main_fuel ) simulation_config = { **simulation_config, **heating_simulation_config, **hotwater_simulation_config, **fuel_simulation_config, "hot_water_energy_eff_ending": "Good" } boiler_costs = self.costs.low_carbon_boiler(is_combi=is_combi, size=f"{boiler_size}kw") self.recommendations.append( { "phase": recommendation_phase, "parts": [ # TODO ], "type": "heating", "description": description, "starting_u_value": None, "new_u_value": None, "sap_points": None, "simulation_config": simulation_config, **boiler_costs } ) # We recommend the heating controls # If the property did not previously have a boiler, we combine controls_recommender = HeatingControlRecommender(self.property) controls_recommender.recommend(heating_description="Boiler and radiators, mains gas") # We may have 2 recommendations from the heating controls if not controls_recommender.recommendation: return if no_heating_has_mains: # We combine the heating and controls recommendations boiler_recommendation = self.recommendations[0].copy() combined_recommendations = [] for controls_recommendation in controls_recommender.recommendation: combined_recommendation = self.combine_heating_and_controls( controls_recommendations=[controls_recommendation], heating_simulation_config=simulation_config, costs=boiler_costs, description=boiler_recommendation["description"], phase=recommendation_phase, heating_controls_only=False, system_change=True ) combined_recommendations.extend(combined_recommendation) # Overwrite the existing boiler recommendation self.recommendations = combined_recommendations else: # We increment the recommendation phase, since the heating controls are separate from the boiler upgrade # but we'll only upgrade if we have a heating recommendation has_heating_recommendation = any( recommendation["type"] == "heating" for recommendation in self.recommendations ) if has_heating_recommendation: recommendation_phase += 1 # The heating controls recommendation is distrinct from the boiler upgrade recommendation # We insert phase into the recommendations for heating controls for recommendation in controls_recommender.recommendation: recommendation["phase"] = recommendation_phase self.recommendations.extend(controls_recommender.recommendation) return