from typing import List import numpy as np from backend.Property import Property from backend.app.plan.schemas import MEASURE_MAP from etl.epc_clean.epc_attributes.WindowAttributes import WindowAttributes from recommendations.Costs import Costs from recommendations.recommendation_utils import override_costs, check_simulation_difference class WindowsRecommendations: # If the property has existing glazing, we scale down the number of windows that need to be glazed COVERAGE_MAP = { # If most of the windows have already been glazed, we assume that 2/3 are glazed and 1/2 are remaining to be # glazed "most": 0.33, # If glazing is partial, we assume 50/50 split between glazed and unglazed "partial": 0.5, } def __init__(self, property_instance: Property, materials: List): self.property = property_instance self.costs = Costs(self.property) self.recommendation = [] self.glazing_material = [ material for material in materials if material["type"] == "windows_glazing" ] if len(self.glazing_material) != 1: raise ValueError("There should only be one window glazing material") self.glazing_material = self.glazing_material[0] def recommend(self, measures=None, phase=0): """ This method will recommend the best possible glazing options for a property. In order to do this, we need to estimate the number of windows that the home has. This information will be stored in the property object, under property.number_of_windows :return: """ measures = MEASURE_MAP["windows"] if measures is None else measures # If we have no windows recs, leave if not any(x in measures for x in MEASURE_MAP["windows"]): return if self.property.windows["glazing_type"] in ["triple", "high performance"]: # We don't make any recommendations in this case. The property already has outstanding glazing return # We handle the rare case of not having any windows data if self.property.windows["clean_description"] is None: return if self.property.windows["has_glazing"] & ( self.property.windows["glazing_coverage"] == "full" ): return # If the property is in a conservation area or is a listed building, it becomes more difficult to install # double glazing. Therefore, we don't recommend it. It is still possible but is not practical as it # requires planning permission and might require a more expensive window type, such as timber. number_of_windows = self.property.number_of_windows if "double_glazing" in measures and "secondary_glazing" not in measures: is_secondary_glazing = False elif "secondary_glazing" in measures and "double_glazing" not in measures: is_secondary_glazing = True else: # 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"] ) # We check if the windows are partially insulated but we're recommending double glazing as a complete # replacement double_glazing_replacement = ( not is_secondary_glazing and # As defined in coverage_map in windows attributes self.property.windows["glazing_coverage"] in ["partial", "most"] ) if not number_of_windows: 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.windows["clean_description"] == "Some double glazing") and ( self.property.data["windows-energy-eff"] == "Very Poor") and ( self.property.data["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 ) else: n_windows_scalar = self.COVERAGE_MAP.get( self.property.windows["glazing_coverage"], 1 ) number_of_windows *= n_windows_scalar number_of_windows = np.ceil(number_of_windows) # Handle edge case - prevent number of windows 0 number_of_windows = max(1, number_of_windows) # We then price the job based on the number of windows that there are cost_result = self.costs.window_glazing( number_of_windows=number_of_windows, material=self.glazing_material, is_secondary_glazing=is_secondary_glazing, ) measure_type = "double_glazing" if not is_secondary_glazing else "secondary_glazing" already_installed = measure_type in self.property.already_installed if already_installed: cost_result = override_costs(cost_result) description = "The property already has double glazing installed. No further action is required." else: glazing_type = ( "secondary glazing" if is_secondary_glazing else "double glazing" ) if self.property.windows["glazing_coverage"] in ["partial", "most"] and not double_glazing_replacement: description = f"Install {glazing_type} to the remaining windows" else: description = f"Install {glazing_type} to all windows" if self.property.is_listed: description += ( ". Secondary glazing recommended due to listed building status" ) elif self.property.is_heritage: description += ( ". Secondary glazing recommended due to herigate building status" ) elif self.property.in_conservation_area: description += ( ". Secondary glazing recommended due to conservation area status" ) # Set up the simulation config windows_energy_eff = "Good" if self.property.windows["glazing_type"] == "multiple": glazing_type_ending = "multiple" glazed_type_ending = ( "secondary glazing" if is_secondary_glazing else "double glazing installed during or after 2002" ) new_windows_description = "Multiple glazing throughout" elif self.property.windows["glazing_type"] == "single": # We will only recommend either secondary or double glazing glazing_type_ending = ( "secondary" if is_secondary_glazing else "double" ) glazed_type_ending = ( "secondary glazing" if is_secondary_glazing else "double glazing installed during or after 2002" ) if is_secondary_glazing: new_windows_description = "Full secondary glazing" else: new_windows_description = "Fully double glazed" elif self.property.windows["glazing_type"] == "double": glazing_type_ending = ( "multiple" if is_secondary_glazing else "double" ) # We set glazed type depending on which window type is more prevalent. Since there is already double # glazing in place, if we're recommending more double glazing, we set the glazed type to double glazing # otherwise, if we're recommending secondary glazing and the proportion of glazing in place already that # is double is less than 50% we set the glazed type to secondary glazing if not is_secondary_glazing: 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: glazed_type_ending = "secondary glazing" else: glazed_type_ending = "double glazing installed during or after 2002" new_windows_description = "Multiple glazing throughout" elif self.property.windows["glazing_type"] == "secondary": glazing_type_ending = ( "secondary" if is_secondary_glazing else "multiple" ) # This is the opposite. If there is secondary glazing in place, and we're recommending double # we set glazed_type_ending, depending on the proportion of glazing in place if is_secondary_glazing: glazed_type_ending = "secondary glazing" new_windows_description = "Full secondary glazing" else: if self.property.data["multi-glaze-proportion"] < 50: glazed_type_ending = "double glazing installed during or after 2002" else: glazed_type_ending = "secondary glazing" new_windows_description = "Multiple glazing throughout" # Windows only end up with an average efficiency windows_energy_eff = "Average" else: raise ValueError("Invalid glazing type - implement me") if self.property.data["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" windows_ending_config = WindowAttributes(new_windows_description).process() windows_simulation_config = check_simulation_difference( new_config=windows_ending_config, old_config=self.property.windows, prefix="windows_" ) simulation_config = { **windows_simulation_config, "multi_glaze_proportion_ending": 100, "windows_energy_eff_ending": windows_energy_eff, "glazing_type_ending": glazing_type_ending, "glazed_type_ending": glazed_type_ending, } description_simulation = { "multi-glaze-proportion": 100, "windows-energy-eff": windows_energy_eff, "windows-description": new_windows_description, "glazed-type": glazed_type_ending, } non_invasive_recommendation = next( (r for r in self.property.non_invasive_recommendations if r["type"] in ["windows_glazing", measure_type]), {} ) self.recommendation = [ { "phase": phase, "parts": [], "type": "windows_glazing", "measure_type": measure_type, "description": description, "starting_u_value": None, "new_u_value": None, "sap_points": non_invasive_recommendation.get("sap_points", None), "already_installed": already_installed, **cost_result, "is_secondary_glazing": is_secondary_glazing, "description_simulation": description_simulation, "simulation_config": simulation_config, "survey": non_invasive_recommendation.get("survey", None), "innovation_rate": self.glazing_material["innovation_rate"], } ] def recommend_mixed_glazing(self, phase): """ This function will recommend mixed glazing to the property. This is a more specific recommendation than the general windows recommendation, but is almost certain to arise from a survey :return: """ mixed_glazing_recommendation_config = next( (r for r in self.property.non_invasive_recommendations if r["type"] == "mixed_glazing"), {} ) if not mixed_glazing_recommendation_config: return description = ( "Install a combination of secondary and double glazing to single glazed windows" if not mixed_glazing_recommendation_config.get("description") else mixed_glazing_recommendation_config["description"] ) windows_ending_config = WindowAttributes("Full secondary glazing").process() windows_simulation_config = check_simulation_difference( new_config=windows_ending_config, old_config=self.property.windows, prefix="windows_" ) windows_simulation_config = { **windows_simulation_config, "windows_energy_eff_ending": "Average", "glazed_type_ending": "secondary glazing", "multi_glaze_proportion_ending": 100, } return [ { "phase": phase, "parts": [], "type": "mixed_glazing", "measure_type": "mixed_glazing", "description": description, "starting_u_value": None, "new_u_value": None, "already_installed": False, "sap_points": mixed_glazing_recommendation_config["sap_points"], "heat_demand": None, # We will predict this "kwh_savings": None, # We will predict this "co2_equivalent_savings": None, # We will predict this "energy_cost_savings": None, # We will predict this "total": mixed_glazing_recommendation_config["cost"], # We use a very simple and rough estimate of 4 hours per unit "labour_hours": mixed_glazing_recommendation_config.get("labour_hours", 8), "labour_days": mixed_glazing_recommendation_config.get("labour_days", 1), # Assume 8 hour day "survey": mixed_glazing_recommendation_config["survey"], "simulation_config": windows_simulation_config, "description_simulation": { "multi-glaze-proportion": 100, "windows-energy-eff": "Average", "windows-description": "Multiple glazing throughout", "glazed-type": "secondary glazing", }, } ]