mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
309 lines
13 KiB
Python
309 lines
13 KiB
Python
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:
|
|
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 windows_area is not None:
|
|
# TODO - we don't have a price for this so we can't recommend it
|
|
print("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"
|
|
)
|
|
|
|
# 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"
|
|
|
|
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,
|
|
}
|
|
|
|
measure_type = "double_glazing" if not is_secondary_glazing else "secondary_glazing"
|
|
|
|
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",
|
|
},
|
|
}
|
|
]
|