Model/recommendations/WindowsRecommendations.py

192 lines
8 KiB
Python

from typing import List
import numpy as np
from backend.Property import Property
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, 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:
"""
# 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
is_secondary_glazing = self.property.restricted_measures or (
self.property.windows["glazing_type"] == "secondary"
)
windows_area = self.property.windows_area
if not number_of_windows:
raise ValueError("Number of windows not specified")
if self.property.windows["has_glazing"] & (
self.property.windows["glazing_coverage"] == "full"
):
return
if windows_area is not None:
raise Exception("We have windows area, we should use this data for our recommendations!!!")
# We scale the number of windows based on the proportion of existing glazing
if self.property.data["multi-glaze-proportion"] != "":
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)
# 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,
)
already_installed = "windows_glazing" 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"]:
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"
)
self.recommendation = [
{
"phase": phase,
"parts": [],
"type": "windows_glazing",
"description": description,
"starting_u_value": None,
"new_u_value": None,
"sap_points": None,
"already_installed": already_installed,
**cost_result,
"is_secondary_glazing": is_secondary_glazing,
# TODO: Make this condition on is_secondary_glazing
"description_simulation": {
"multi-glaze-proportion": 100,
"windows-energy-eff": "Average",
"windows-description": "Fully double glazed",
"glazed-type": "double glazing installed during or after 2002",
}
}
]
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",
"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",
},
}
]