mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
392 lines
18 KiB
Python
392 lines
18 KiB
Python
import itertools
|
|
from utils.logger import setup_logger
|
|
from backend.Property import Property
|
|
from recommendations.FloorRecommendations import FloorRecommendations
|
|
from recommendations.WallRecommendations import WallRecommendations
|
|
from recommendations.RoofRecommendations import RoofRecommendations
|
|
from recommendations.VentilationRecommendations import VentilationRecommendations
|
|
from recommendations.FireplaceRecommendations import FireplaceRecommendations
|
|
from recommendations.LightingRecommendations import LightingRecommendations
|
|
from recommendations.SolarPvRecommendations import SolarPvRecommendations
|
|
from recommendations.WindowsRecommendations import WindowsRecommendations
|
|
from recommendations.HeatingRecommender import HeatingRecommender
|
|
from recommendations.HotwaterRecommendations import HotwaterRecommendations
|
|
from recommendations.SecondaryHeating import SecondaryHeating
|
|
from recommendations.Recommendations import Recommendations
|
|
|
|
logger = setup_logger()
|
|
|
|
|
|
class Mds:
|
|
"""
|
|
Handles the contruction of the MDS report
|
|
"""
|
|
|
|
format_map = {
|
|
"external_wall_insulation": "EWI (Trad Const)",
|
|
"internal_wall_insualtion": "IWI",
|
|
"cavity_wall_insulation": "CWI",
|
|
"loft_insulation": "LI",
|
|
"air_source_heat_pump": "ASHP Htg",
|
|
"high_heat_retention_storage_heaters": "High Heat Retention Storage Heaters",
|
|
"solar_pv": "Solar PV",
|
|
}
|
|
|
|
def __init__(self, property_instance: Property, materials, optimise_measures: bool = False):
|
|
self.property_instance = property_instance
|
|
|
|
self.floor_recommender = FloorRecommendations(property_instance=property_instance, materials=materials)
|
|
self.wall_recommender = WallRecommendations(property_instance=property_instance, materials=materials)
|
|
self.roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials)
|
|
self.ventilation_recomender = VentilationRecommendations(
|
|
property_instance=property_instance, materials=materials
|
|
)
|
|
self.fireplace_recommender = FireplaceRecommendations(property_instance=property_instance)
|
|
self.lighting_recommender = LightingRecommendations(property_instance=property_instance, materials=materials)
|
|
self.windows_recommender = WindowsRecommendations(property_instance=property_instance, materials=materials)
|
|
self.solar_recommender = SolarPvRecommendations(property_instance=property_instance)
|
|
self.heating_recommender = HeatingRecommender(property_instance=property_instance)
|
|
self.hotwater_recommender = HotwaterRecommendations(property_instance=property_instance)
|
|
self.secondary_heating_recommender = SecondaryHeating(property_instance=property_instance)
|
|
|
|
# This flag indicates that we wish to optimise the measures, to the property, depending on the set of measures
|
|
# we have been provided
|
|
self.optimise_measures = optimise_measures
|
|
|
|
def select_optimal_measure_set(self, measures):
|
|
|
|
# This is the set
|
|
all_considered_measures = [
|
|
'external_wall_insulation',
|
|
'cavity_wall_insulation',
|
|
'loft_insulation',
|
|
'air_source_heat_pump',
|
|
'high_heat_retention_storage_heaters',
|
|
'solar_pv'
|
|
]
|
|
|
|
# Check if our measures are within the ones we've handled
|
|
new = [m for m in measures if m not in all_considered_measures]
|
|
if new:
|
|
raise NotImplementedError("New measures - handle me")
|
|
|
|
def prune_options(options, measures):
|
|
options_pruned = []
|
|
for _group in options:
|
|
group_pruned = [m for m in _group if m in measures]
|
|
if not group_pruned:
|
|
continue
|
|
options_pruned.append(group_pruned)
|
|
|
|
return options_pruned
|
|
|
|
# For options in here, a property could only possibly have one of these
|
|
one_choice_options = [
|
|
["external_wall_insulation", "cavity_wall_insulation", "internal_wall_insulation"],
|
|
["loft_insulation", "flat_roof_insulation", "room_in_roof_insulation"],
|
|
["solid_floor_insulation", "suspended_floor_insulation"],
|
|
]
|
|
# prune one_choice_options based on the measure set considered for this property
|
|
one_choice_options_pruned = prune_options(one_choice_options, measures)
|
|
|
|
# For options in here, a property could have one or the other so all should be considered
|
|
multi_path_options = [
|
|
["air_source_heat_pump", "high_heat_retention_storage_heaters", "gas_boiler"]
|
|
]
|
|
|
|
multi_path_options_pruned = prune_options(multi_path_options, measures)
|
|
|
|
one_choice_combinations = [list(itertools.product(*one_choice_options_pruned))]
|
|
one_choice_combinations = [list(x) for sublist in one_choice_combinations for x in sublist]
|
|
multi_path_combinations = [list(itertools.product(*multi_path_options_pruned))]
|
|
multi_path_combinations = [list(x) for sublist in multi_path_combinations for x in sublist]
|
|
|
|
one_choice_flat = [item for sublist in one_choice_options_pruned for item in sublist]
|
|
multi_path_flat = [item for sublist in multi_path_options_pruned for item in sublist]
|
|
|
|
remaining_measures = [
|
|
measure for measure in measures
|
|
if measure not in one_choice_flat and measure not in multi_path_flat
|
|
]
|
|
|
|
# Combine one_choice and multi_path combinations with remaining measures
|
|
final_combinations = []
|
|
for one_choice in one_choice_combinations:
|
|
for multi_path in multi_path_combinations:
|
|
final_combinations.append([m for m in one_choice + multi_path + remaining_measures])
|
|
|
|
pruned_combinations = []
|
|
# TODO: We can do these checks once, outside of the loop and prune the combinations
|
|
for combination in final_combinations:
|
|
pruned_measures = []
|
|
for measure in combination:
|
|
if measure not in measures:
|
|
continue
|
|
# There are certain measures where we need to
|
|
if measure == "external_wall_insulation":
|
|
# Check if the wall is not cavity since the other wall types can take external wall insulation
|
|
if (
|
|
self.wall_recommender.ewi_valid() and
|
|
not self.property_instance.walls["insulation_thickness"] in ["average", "above average"]
|
|
):
|
|
pruned_measures.append(measure)
|
|
continue
|
|
|
|
if measure == "cavity_wall_insulation":
|
|
# Check if the wall is cavity
|
|
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 and the loft isn't already done
|
|
# Or, if the home had a u-value for the roof, we don't recommend loft insulation
|
|
if (
|
|
self.property_instance.roof["is_pitched"] and
|
|
not self.roof_recommender.is_loft_already_insulated() and
|
|
self.property_instance.roof["thermal_transmittance_unit"] is None
|
|
):
|
|
pruned_measures.append(measure)
|
|
continue
|
|
|
|
if measure == "solid_floor_insulation":
|
|
# Check if the floor is solid
|
|
if (
|
|
self.property_instance.floor["is_solid"] and
|
|
self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] and
|
|
self.property_instance.floor["thermal_transmittance_unit"] is not None
|
|
):
|
|
pruned_measures.append(measure)
|
|
continue
|
|
|
|
if measure == "suspended_floor_insulation":
|
|
# Check if the floor is suspended
|
|
if (
|
|
self.property_instance.floor["is_suspended"] and
|
|
self.property_instance.floor["insulation_thickness"] not in ["average", "above average"] and
|
|
self.property_instance.floor["thermal_transmittance_unit"] is not None
|
|
):
|
|
pruned_measures.append(measure)
|
|
continue
|
|
|
|
if measure == "high_heat_retention_storage_heaters":
|
|
|
|
# For the moment, we recommend storage heaters if the property doesn't already
|
|
# and don't make it contngent on controls
|
|
already_has_hhr = self.heating_recommender.is_hhr_already_installed()
|
|
|
|
if (
|
|
self.heating_recommender.is_high_heat_retention_valid() and
|
|
not already_has_hhr
|
|
):
|
|
pruned_measures.append(measure)
|
|
continue
|
|
|
|
if measure == "air_source_heat_pump":
|
|
if self.heating_recommender.is_ashp_valid():
|
|
pruned_measures.append(measure)
|
|
continue
|
|
|
|
if measure == "solar_pv":
|
|
if self.solar_recommender.is_solar_pv_valid():
|
|
pruned_measures.append(measure)
|
|
continue
|
|
|
|
raise NotImplementedError("Implement me")
|
|
|
|
if not pruned_measures:
|
|
continue
|
|
|
|
pruned_measures_formatted = []
|
|
for pm in pruned_measures:
|
|
pruned_measures_formatted.append({pm: self.format_map[pm]})
|
|
|
|
pruned_combinations.append(pruned_measures_formatted)
|
|
|
|
# We're left with the subset of measures that are possible for this property
|
|
# These are the possible groups of measures that could be applied to this home
|
|
|
|
return pruned_combinations
|
|
|
|
def _build(self, measure_config_list, measures):
|
|
not_implemented_measures = [
|
|
"party_wall_insulation",
|
|
"ground_source_heat_pump",
|
|
"shared_ground_loops",
|
|
"communal_heat_networks",
|
|
"district_heating_networks",
|
|
"solar_thermal",
|
|
"draught_proofing",
|
|
"ev_charging",
|
|
"battery",
|
|
]
|
|
# Check if we have a not implemented measure
|
|
if any([m in not_implemented_measures for m in measure_config_list]):
|
|
raise NotImplementedError("Not implemented measure in the property - implement me")
|
|
|
|
mds_recommendations = []
|
|
errors = []
|
|
phase = 0
|
|
|
|
# TODO: Could use a decarator to reduce the boilerplate code - insert_recommendation_id and then the append
|
|
|
|
if "external_wall_insulation" in measure_config_list:
|
|
recs = self.wall_recommender.mds_recommend_ewi(phase=phase)
|
|
if not recs:
|
|
raise Exception("No recommendations for external wall insulation")
|
|
recs = self.insert_recommendation_id(recs, measures, "external_wall_insulation")
|
|
mds_recommendations.append(recs)
|
|
if self.optimise_measures and len(recs):
|
|
phase += 1
|
|
|
|
if "cavity_wall_insulation" in measure_config_list:
|
|
recs = self.wall_recommender.mds_recommend_cavity_wall_insulation(phase=phase)
|
|
recs = self.insert_recommendation_id(recs, measures, "cavity_wall_insulation")
|
|
mds_recommendations.append(recs)
|
|
if self.optimise_measures and len(recs):
|
|
phase += 1
|
|
|
|
if "loft_insulation" in measure_config_list:
|
|
# Check if the roof is suitable for loft insulation
|
|
if self.property_instance.roof['is_roof_room']:
|
|
errors.append("Roof is a room")
|
|
else:
|
|
recs = self.roof_recommender.mds_loft_insulation(phase=phase)
|
|
if not recs:
|
|
raise Exception("No recommendations for loft insulation")
|
|
recs = self.insert_recommendation_id(recs, measures, "loft_insulation")
|
|
mds_recommendations.append(recs)
|
|
if self.optimise_measures and len(recs):
|
|
phase += 1
|
|
|
|
if "internal_wall_insulation" in measure_config_list:
|
|
raise Exception("check me out 4")
|
|
self.wall_recommender.recommend(phase=phase)
|
|
|
|
if "suspended_floor_insulation" in measure_config_list:
|
|
raise Exception("check me out 5")
|
|
self.floor_recommender.recommend(phase=phase)
|
|
|
|
if "solid_floor_insulation" in measure_config_list:
|
|
raise Exception("check me out 6")
|
|
self.floor_recommender.recommend(phase=phase)
|
|
|
|
if "air_source_heat_pump" in measure_config_list:
|
|
recs = self.heating_recommender.recommend_air_source_heat_pump(
|
|
phase=phase, has_cavity_or_loft_recommendations=False, _return=True
|
|
)
|
|
recs = self.insert_recommendation_id(recs, measures, "air_source_heat_pump")
|
|
mds_recommendations.append(recs)
|
|
if self.optimise_measures and len(recs):
|
|
phase += 1
|
|
|
|
if "high_heat_retention_storage_heaters" in measure_config_list:
|
|
recs = self.heating_recommender.recommend_hhr_storage_heaters(
|
|
phase=phase, system_change=True, heating_controls_only=False, _return=True
|
|
)
|
|
if recs is None:
|
|
logger.info(
|
|
f"No recommendations for high heat retention storage heaters, current heating "
|
|
f"{self.property_instance.main_heating['clean_description']}"
|
|
)
|
|
else:
|
|
recs = self.insert_recommendation_id(recs, measures, "high_heat_retention_storage_heaters")
|
|
mds_recommendations.append(recs)
|
|
if self.optimise_measures and len(recs):
|
|
phase += 1
|
|
|
|
if "low_energy_lighting" in measure_config_list:
|
|
raise Exception("check me out 9")
|
|
self.lighting_recommender.recommend(phase=phase)
|
|
|
|
if "cylinder_insulation" in measure_config_list:
|
|
raise Exception("check me out 10")
|
|
self.hotwater_recommender.recommend(phase=phase)
|
|
|
|
if "smart_controls" in measure_config_list:
|
|
raise Exception("check me out 11")
|
|
self.heating_recommender.recommend(phase=phase)
|
|
|
|
if "zone_controls" in measure_config_list:
|
|
raise Exception("check me out 12")
|
|
self.heating_recommender.recommend(phase=phase)
|
|
|
|
if "trvs" in measure_config_list:
|
|
raise Exception("check me out 13")
|
|
self.heating_recommender.recommend(phase=phase)
|
|
|
|
if "solar_pv" in measure_config_list:
|
|
recs = self.solar_recommender.mds_recommend(phase=phase, solar_pv_percentage=0.5)
|
|
recs = self.insert_recommendation_id(recs, measures, "solar_pv")
|
|
mds_recommendations.append(recs)
|
|
if self.optimise_measures and len(recs):
|
|
phase += 1
|
|
|
|
if "double_glazing" in measure_config_list:
|
|
raise Exception("check me out 15")
|
|
self.windows_recommender.recommend(phase=phase)
|
|
|
|
if "mechanical_ventilation" in measure_config_list:
|
|
raise Exception("check me out 16")
|
|
self.ventilation_recomender.recommend(phase=phase)
|
|
|
|
if "gas_boiler" in measure_config_list:
|
|
raise Exception("check me out 17")
|
|
self.heating_recommender.recommend(phase=phase)
|
|
|
|
if "flat_roof_insulation" in measure_config_list:
|
|
raise Exception("check me out 18")
|
|
self.roof_recommender.recommend(phase=phase)
|
|
|
|
if "room_in_roof_insulation" in measure_config_list:
|
|
raise Exception("check me out 19")
|
|
self.roof_recommender.recommend(phase=phase)
|
|
|
|
property_representative_recommendations = Recommendations.create_representative_recommendations(
|
|
mds_recommendations, non_invasive_recommendations=[]
|
|
)
|
|
|
|
return mds_recommendations, property_representative_recommendations, errors
|
|
|
|
def build(self):
|
|
if self.property_instance.measures is None:
|
|
raise NotImplementedError("No measures in the property - implement me")
|
|
|
|
if self.optimise_measures:
|
|
measures_set = self.select_optimal_measure_set(self.property_instance.measures)
|
|
mds_recommendations_map = {}
|
|
representative_recommendations_map = {}
|
|
errors_map = {}
|
|
for measures in measures_set:
|
|
measure_config_list = [list(x.keys())[0] for x in measures]
|
|
mds_recommendations, rep_recommendations, errors = self._build(
|
|
measure_config_list=measure_config_list,
|
|
measures=measures
|
|
)
|
|
if errors:
|
|
logger.info(f"Errors: {errors}")
|
|
|
|
mds_recommendations_map[str(measure_config_list)] = mds_recommendations
|
|
representative_recommendations_map[str(measure_config_list)] = rep_recommendations
|
|
errors_map[str(measure_config_list)] = errors
|
|
|
|
return mds_recommendations_map, representative_recommendations_map, errors_map
|
|
|
|
else:
|
|
measure_config_list = [list(m.keys())[0] for m in self.property_instance.measures]
|
|
return self._build(measure_config_list=measure_config_list, measures=self.property_instance.measures)
|
|
|
|
@staticmethod
|
|
def insert_recommendation_id(recommendations, measures, measure_name):
|
|
# Insert the recommendation identifier into this recommendation
|
|
measure_config = [m for m in measures if measure_name in m][0]
|
|
|
|
idx = 0
|
|
for r in recommendations:
|
|
r["recommendation_id"] = list(measure_config.values())[0] + "-" + str(idx)
|
|
idx += 1
|
|
|
|
return recommendations
|