mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
372 lines
17 KiB
Python
372 lines
17 KiB
Python
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)
|
|
return
|
|
|
|
# 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"]
|
|
|
|
if has_boiler or no_heating_has_mains:
|
|
self.recommend_boiler_upgrades(phase=phase, no_heating_has_mains=no_heating_has_mains)
|
|
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, no_heating_has_mains):
|
|
"""
|
|
This boiler recommendation will only recommend a like-for-like upgrade, since changing the system
|
|
is generally more expensive
|
|
:param phase:
|
|
:param no_heating_has_mains: indicaes if the property has no heating system, but has access to the mains gas
|
|
: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"],
|
|
)
|
|
|
|
# If heating and hot water come from the mains, we need a combi boiler, otherwise we need a regular boiler
|
|
hotwater_from_mains = self.property.hotwater["clean_description"] in ["From main system"]
|
|
|
|
is_combi = hotwater_from_mains or no_heating_has_mains
|
|
if is_combi:
|
|
description = "Upgrade to a new combi boiler"
|
|
else:
|
|
description = "Upgrade to a new boiler"
|
|
|
|
simulation_config = {"mainheat_energy_eff_ending": "Good"}
|
|
if no_heating_has_mains:
|
|
# 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
|
|
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
|