mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
improving logic of heating recommendations
This commit is contained in:
parent
419c74e1e4
commit
7ee9476dc1
10 changed files with 164 additions and 36 deletions
|
|
@ -343,7 +343,7 @@ class Property:
|
|||
else:
|
||||
output["glazed_type_ending"] = "double glazing installed during or after 2002"
|
||||
|
||||
if recommendation["type"] in ["heating", "heating_control"]:
|
||||
if recommendation["type"] == "heating":
|
||||
# We update the data, as defined in the recommendaton
|
||||
|
||||
simulation_config = recommendation["simulation_config"]
|
||||
|
|
@ -363,7 +363,7 @@ class Property:
|
|||
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation",
|
||||
"loft_insulation", "room_roof_insulation", "flat_roof_insulation",
|
||||
"solid_floor_insulation", "suspended_floor_insulation", "exposed_floor_insulation",
|
||||
"windows_glazing", "solar_pv", "heating", "heating_control",
|
||||
"windows_glazing", "solar_pv", "heating",
|
||||
]:
|
||||
raise NotImplementedError("Implement me")
|
||||
|
||||
|
|
@ -623,6 +623,7 @@ class Property:
|
|||
floor_height=self.floor_height,
|
||||
perimeter=self.perimeter,
|
||||
built_form=self.data["built-form"],
|
||||
property_type=self.data["property-type"],
|
||||
)
|
||||
|
||||
self.insulation_floor_area = self.floor_area / self.number_of_floors
|
||||
|
|
|
|||
|
|
@ -146,9 +146,6 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
recommender = Recommendations(property_instance=p, materials=materials)
|
||||
property_recommendations, property_representative_recommendations = recommender.recommend()
|
||||
|
||||
# TODO: Re-run property_dimensions
|
||||
# Do the simulation in adjust_difference_record_with_recommendations, to update the values
|
||||
|
||||
if not property_recommendations:
|
||||
continue
|
||||
|
||||
|
|
@ -184,6 +181,8 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
)
|
||||
|
||||
# Insert the predictions into the recommendations and run the optimiser
|
||||
# TODO: If a recommendation has a negative impact on SAP, we should remove it - this seems to have become a
|
||||
# possibility with heating system
|
||||
logger.info("Optimising recommendations")
|
||||
for property_id in recommendations.keys():
|
||||
|
||||
|
|
@ -203,21 +202,22 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
expected_adjusted_energy=expected_adjusted_energy
|
||||
)
|
||||
|
||||
# TODO: For the private customer, we should probably NOT allow floor insulation, because it often requires
|
||||
# decanting the tenant
|
||||
input_measures = prepare_input_measures(recommendations_with_impact, body.goal)
|
||||
input_measures = prepare_input_measures(recommendations_with_impact, body.goal, body.housing_type)
|
||||
|
||||
current_sap_points = int(property_instance.data["current-energy-efficiency"])
|
||||
target_sap_points = epc_to_sap_lower_bound(body.goal_value)
|
||||
sap_gain = CostOptimiser.calculate_sap_gain_with_slack(target_sap_points - current_sap_points)
|
||||
|
||||
if body.budget:
|
||||
optimiser = GainOptimiser(input_measures, max_cost=body.budget)
|
||||
optimiser = GainOptimiser(
|
||||
input_measures, max_cost=body.budget, max_gain=sap_gain if sap_gain > 0 else 0
|
||||
)
|
||||
else:
|
||||
# The minimum gain is the minimum number of SAP points required to get to the target SAP band
|
||||
current_sap_points = int(property_instance.data["current-energy-efficiency"])
|
||||
target_sap_points = epc_to_sap_lower_bound(body.goal_value)
|
||||
|
||||
# If the gain is negative, the optimiser will return an empty solution
|
||||
optimiser = CostOptimiser(
|
||||
input_measures,
|
||||
min_gain=CostOptimiser.calculate_sap_gain_with_slack(target_sap_points - current_sap_points)
|
||||
min_gain=sap_gain
|
||||
)
|
||||
|
||||
optimiser.setup()
|
||||
|
|
|
|||
|
|
@ -76,20 +76,20 @@ def app():
|
|||
to_append = {
|
||||
**row.to_dict(),
|
||||
"uprn": newest_epc["uprn"],
|
||||
"address": newest_epc["address1"],
|
||||
"postcode": newest_epc["postcode"],
|
||||
"postcode to check": newest_epc["postcode"],
|
||||
# "walls-description": newest_epc["walls-description"],
|
||||
# "roof-description": newest_epc["roof-description"],
|
||||
# "floor-description": newest_epc["floor-description"],
|
||||
# "total-floor-area": newest_epc["total-floor-area"],
|
||||
"full-address": newest_epc["address"],
|
||||
|
||||
}
|
||||
|
||||
processed_asset_list.append(to_append)
|
||||
epc_data.append(newest_epc)
|
||||
|
||||
processed_asset_list_df = pd.DataFrame(processed_asset_list)
|
||||
processed_asset_list_df.to_excel("urban_splash_processed_asset_list.xlsx")
|
||||
|
||||
epc_data_df = pd.DataFrame(epc_data)
|
||||
|
||||
example = epc_data_df.iloc[11, :]
|
||||
|
|
|
|||
|
|
@ -961,3 +961,22 @@ class Costs:
|
|||
"labour_hours": labour_hours,
|
||||
"labour_days": labour_days,
|
||||
}
|
||||
|
||||
def celect_type_controls(self):
|
||||
"""
|
||||
Calculate the cost of installing Celect type controls
|
||||
"""
|
||||
|
||||
# The £50 cost is a rough estimate based on internet research
|
||||
total_cost = 50
|
||||
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
||||
vat = total_cost - subtotal_before_vat
|
||||
|
||||
# We estimate the labour hours to be 4
|
||||
return {
|
||||
"total": total_cost,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"vat": vat,
|
||||
"labour_hours": 4,
|
||||
"labour_days": 1,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,10 +19,16 @@ class HeatingControlRecommender:
|
|||
|
||||
# This first iteration of the recommender will provide very basic recommendation
|
||||
# We recommend heating controls based on the main heating system
|
||||
if heating_description in ["Room heaters, electric", "Electric storage heaters, radiators"]:
|
||||
if heating_description in [
|
||||
"Room heaters, electric", "Electric storage heaters"
|
||||
]:
|
||||
self.recommend_room_heaters_electric_controls()
|
||||
return
|
||||
|
||||
if heating_description in ["Electric storage heaters, radiators"]:
|
||||
self.recommend_celect_type_controls()
|
||||
return
|
||||
|
||||
def recommend_room_heaters_electric_controls(self):
|
||||
"""
|
||||
If the home has Room heaters, electric, we start by identifying potential heating controls that could
|
||||
|
|
@ -63,7 +69,6 @@ class HeatingControlRecommender:
|
|||
|
||||
self.recommendation.append(
|
||||
{
|
||||
"type": "heating_control",
|
||||
"description": "upgrade heating controls to Programmer and Appliance or Smart Thermostats",
|
||||
**self.costs.programmer_and_appliance_thermostat(has_programmer=has_programmer),
|
||||
"simulation_config": simulation_config
|
||||
|
|
@ -72,3 +77,33 @@ class HeatingControlRecommender:
|
|||
|
||||
# We don't implement any other recommendations right now
|
||||
return
|
||||
|
||||
def recommend_celect_type_controls(self):
|
||||
"""
|
||||
If the home has Electric storage heaters, radiators, we start by identifying potential heating controls that
|
||||
could
|
||||
be upgraded, that would provide a practical impact. This will be the least invasive improvement.
|
||||
|
||||
We can then consider the heating system itself
|
||||
:return:
|
||||
"""
|
||||
|
||||
# We recommend upgrading to Celect type controls
|
||||
ending_config = MainheatControlAttributes("Celect-type controls").process()
|
||||
# We look at what has changed in the ending config, and compare it to the current config
|
||||
simulation_config = check_simulation_difference(
|
||||
new_config=ending_config, old_config=self.property.main_heating_controls
|
||||
)
|
||||
# This upgrade will only take the heating system to average energy efficiency
|
||||
simulation_config["mainheatc_energy_eff_ending"] = "Good"
|
||||
|
||||
self.recommendation.append(
|
||||
{
|
||||
"description": "upgrade heating controls to Celect type controls",
|
||||
**self.costs.celect_type_controls(),
|
||||
"simulation_config": simulation_config
|
||||
}
|
||||
)
|
||||
|
||||
# We don't implement any other recommendations right now
|
||||
return
|
||||
|
|
|
|||
|
|
@ -18,8 +18,12 @@ class HeatingRecommender:
|
|||
# This first iteration of the recommender will provide very basic recommendation
|
||||
# We recommend heating controls based on the main heating system
|
||||
if self.property.main_heating["clean_description"] == "Room heaters, electric":
|
||||
self.recommend_room_heaters_electric(phase=phase, heating_controls_only=True)
|
||||
self.recommend_electric_storage_heaters(phase=phase, heating_controls_only=False)
|
||||
self.recommend_room_heaters_electric(phase=phase, system_change=False, heating_controls_only=True)
|
||||
self.recommend_electric_storage_heaters(phase=phase, system_change=True, heating_controls_only=False)
|
||||
return
|
||||
|
||||
if self.property.main_heating["clean_description"] == "Electric storage heaters, radiators":
|
||||
self.recommend_electric_storage_heaters(phase=phase, system_change=False, heating_controls_only=True)
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -37,7 +41,8 @@ class HeatingRecommender:
|
|||
|
||||
@staticmethod
|
||||
def combine_heating_and_controls(
|
||||
controls_recommendations, heating_simulation_config, costs, description, phase, heating_controls_only
|
||||
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
|
||||
|
|
@ -48,6 +53,9 @@ class HeatingRecommender:
|
|||
: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:
|
||||
"""
|
||||
|
||||
|
|
@ -57,6 +65,9 @@ class HeatingRecommender:
|
|||
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()
|
||||
|
|
@ -102,6 +113,10 @@ class HeatingRecommender:
|
|||
output.append(
|
||||
{
|
||||
"phase": phase,
|
||||
"parts": [
|
||||
# TODO
|
||||
],
|
||||
"type": "heating",
|
||||
"starting_u_value": None,
|
||||
"new_u_value": None,
|
||||
"sap_points": None,
|
||||
|
|
@ -111,16 +126,36 @@ class HeatingRecommender:
|
|||
|
||||
return output
|
||||
|
||||
def recommend_electric_storage_heaters(self, phase, heating_controls_only):
|
||||
def recommend_electric_storage_heaters(self, phase, system_change, heating_controls_only):
|
||||
"""
|
||||
We recommend electric storage heaters as an upgrade to the heating system.
|
||||
: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)
|
||||
controls_recommender.recommend(heating_description="Room heaters, electric")
|
||||
# The heating controls we're recommending for are based on the recommended heating system
|
||||
|
||||
if self.property.data["mainheat-energy-eff"] not in ["Poor", "Very Poor"]:
|
||||
# We only recommend Celect-type controls if the current heating system is not Celect-type controls
|
||||
if self.property.main_heating_controls["clean_description"] != "Celect-type controls":
|
||||
controls_recommender.recommend(heating_description="Electric storage heaters, radiators")
|
||||
|
||||
# Conditions for not needing this recommendation
|
||||
efficient_room_heaters = self.property.main_heating["clean_description"] == "Room heaters, electric" and (
|
||||
self.property.data["mainheat-energy-eff"] not in ["Poor", "Very Poor", "Average"]
|
||||
)
|
||||
|
||||
efficient_storage_heaters = (
|
||||
(self.property.main_heating["clean_description"] in [
|
||||
"Electric storage heaters, radiators", "Electric storage heaters"
|
||||
]) and self.property.data["mainheat-energy-eff"] not in ["Poor", "Very Poor", "Average"]
|
||||
)
|
||||
|
||||
# Conditions for not recommending electric storage heaters
|
||||
if efficient_room_heaters or efficient_storage_heaters:
|
||||
# We do just heating controls
|
||||
self.recommendations.extend(
|
||||
self.combine_heating_and_controls(
|
||||
|
|
@ -140,7 +175,6 @@ class HeatingRecommender:
|
|||
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["mainheatc_energy_eff_ending"] = "Good"
|
||||
heating_simulation_config["mainheat_energy_eff_ending"] = "Average"
|
||||
|
||||
# Upgrade to electric storage heaters
|
||||
|
|
@ -155,22 +189,28 @@ class HeatingRecommender:
|
|||
costs=costs,
|
||||
description=description,
|
||||
phase=phase,
|
||||
heating_controls_only=heating_controls_only
|
||||
heating_controls_only=heating_controls_only,
|
||||
system_change=system_change
|
||||
)
|
||||
|
||||
self.recommendations.extend(recommendations)
|
||||
|
||||
def recommend_room_heaters_electric(self, phase, heating_controls_only):
|
||||
def recommend_room_heaters_electric(self, phase, system_change, heating_controls_only):
|
||||
"""
|
||||
If the home has Room heaters, electric, we start by identifying potential heating controls that could
|
||||
be upgraded, that would provide a practical impact. This will be the least invasive improvement.
|
||||
|
||||
We can then consider the heating system itself
|
||||
: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)
|
||||
controls_recommender.recommend(heating_description="Room heaters, electric")
|
||||
if self.property.main_heating_controls["clean_description"] != "Programmer and appliance thermostats":
|
||||
controls_recommender.recommend(heating_description='Room heaters, electric')
|
||||
|
||||
if self.property.data["mainheat-energy-eff"] not in ["Poor", "Very Poor"]:
|
||||
# We do just heating controls
|
||||
|
|
@ -189,7 +229,7 @@ class HeatingRecommender:
|
|||
costs = self.costs.electric_room_heaters(
|
||||
number_heated_rooms=self.property.data["number-heated-rooms"]
|
||||
)
|
||||
description = "Upgrade electric room heaters to more efficient electric radiators"
|
||||
description = "Upgrade electric room heaters to efficient electric radiators"
|
||||
heating_simulation_config = {"mainheat_energy_eff_ending": "Average"}
|
||||
|
||||
recommendations = self.combine_heating_and_controls(
|
||||
|
|
@ -198,7 +238,8 @@ class HeatingRecommender:
|
|||
costs=costs,
|
||||
description=description,
|
||||
phase=phase,
|
||||
heating_controls_only=heating_controls_only
|
||||
heating_controls_only=heating_controls_only,
|
||||
system_change=system_change
|
||||
)
|
||||
|
||||
self.recommendations.extend(recommendations)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ class Recommendations:
|
|||
phase = 0
|
||||
|
||||
# Building Fabric
|
||||
self.wall_recomender.recommend(phase=phase)
|
||||
if self.wall_recomender.recommendations:
|
||||
property_recommendations.append(self.wall_recomender.recommendations)
|
||||
phase += 1
|
||||
|
|
|
|||
|
|
@ -9,10 +9,20 @@ class GainOptimiser:
|
|||
This class is used to maximise gain, given a constrained cost
|
||||
"""
|
||||
|
||||
def __init__(self, components, max_cost):
|
||||
def __init__(self, components, max_cost, max_gain=None):
|
||||
"""
|
||||
This function will try and maximise the gain, given a constrained cost. If we specific a max_gain, then the
|
||||
optimisation routine is constained to try not to exceed a maximum increase
|
||||
|
||||
:param components: List of components, where each component is a dictionary with keys "id", "cost" and "gain"
|
||||
:param max_cost: Maximum cost constraint
|
||||
:param max_gain: Maximum gain constraint
|
||||
"""
|
||||
self.components = components
|
||||
self.max_cost = max_cost
|
||||
self.max_gain = max_gain
|
||||
self.cost_constraint = None
|
||||
self.max_gain_constraint = None
|
||||
self.m = None
|
||||
self.variables = []
|
||||
self.solution = []
|
||||
|
|
@ -50,6 +60,15 @@ class GainOptimiser:
|
|||
|
||||
self.cost_constraint = self.m.add_constr(cost_expression)
|
||||
|
||||
# Add an optional max gain constraint if max_gain is not None
|
||||
if self.max_gain is not None:
|
||||
max_gain_expression = xsum(
|
||||
component['gain'] * var for group, group_vars in zip(self.components, self.variables) for component, var
|
||||
in zip(group, group_vars)
|
||||
) <= self.max_gain
|
||||
|
||||
self.max_gain_constraint = self.m.add_constr(max_gain_expression)
|
||||
|
||||
# This constraint ensures that at most one item from each group is selected
|
||||
# This is expressed by summing up the decision variables for each group and ensuring that the sum is <= 1
|
||||
for group_vars in self.variables:
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
def prepare_input_measures(property_recommendations, goal):
|
||||
def prepare_input_measures(property_recommendations, goal, housing_type):
|
||||
"""
|
||||
Basic function to convert recommendations_to_upload to a format that is
|
||||
suitable for the optimiser - large
|
||||
:param property_recommendations: object containing the recommendations, created in the plan trigger api
|
||||
:param goal: goal to be optimised for, should be one of the keys in gain_map. E.g. if the gain is SAP points,
|
||||
the goal should reflect that desired gain
|
||||
:param housing_type: type of housing the recommendations are for - should be one of "Social" or "Private"
|
||||
:return: Nested list of input measures
|
||||
"""
|
||||
|
||||
if housing_type not in ["Social", "Private"]:
|
||||
raise ValueError("Invalid housing type - investigate me")
|
||||
|
||||
goal_map = {
|
||||
"Increase EPC": "sap_points"
|
||||
}
|
||||
|
|
@ -16,6 +20,10 @@ def prepare_input_measures(property_recommendations, goal):
|
|||
if not goal_key:
|
||||
raise NotImplementedError("Not implemented this gain type - investigate me")
|
||||
|
||||
# We don't include suspended and solid floor insulation as possible measures in private housing, because
|
||||
# of the need to decant the tenant
|
||||
ignored_measures = ["suspended_floor_insulation", "solid_floor_insulation"] if housing_type == "Private" else []
|
||||
|
||||
input_measures = []
|
||||
for recs in property_recommendations:
|
||||
input_measures.append(
|
||||
|
|
@ -26,7 +34,7 @@ def prepare_input_measures(property_recommendations, goal):
|
|||
"gain": rec[goal_key],
|
||||
"type": rec["type"]
|
||||
}
|
||||
for rec in recs
|
||||
for rec in recs if rec["type"] not in ignored_measures
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -544,7 +544,7 @@ def get_wall_type(
|
|||
return None
|
||||
|
||||
|
||||
def estimate_external_wall_area(num_floors, floor_height, perimeter, built_form):
|
||||
def estimate_external_wall_area(num_floors, floor_height, perimeter, built_form, property_type):
|
||||
"""
|
||||
This method estimates the external wall area based on fundamental assumptions about the home
|
||||
|
||||
|
|
@ -553,6 +553,7 @@ def estimate_external_wall_area(num_floors, floor_height, perimeter, built_form)
|
|||
:param floor_height: Height of one floor in meters.
|
||||
:param perimeter: Total perimeter of the building on one floor in meters.
|
||||
:param built_form: The built form of the property. This is used to determine the number of exposed walls.
|
||||
:param property_type: The type of the property. This is used to determine the number of exposed walls.
|
||||
:return:
|
||||
"""
|
||||
wall_area_one_floor = perimeter * floor_height
|
||||
|
|
@ -565,8 +566,11 @@ def estimate_external_wall_area(num_floors, floor_height, perimeter, built_form)
|
|||
'Semi-Detached': 3,
|
||||
'Detached': 4,
|
||||
}
|
||||
|
||||
exposed_wall_area = total_wall_area * (number_exposed_walls.get(built_form, 3) / 4)
|
||||
if built_form == "Detached" and property_type == "Flat":
|
||||
# We don't have 4 exposed walls for a flat
|
||||
exposed_wall_area = total_wall_area * (3 / 4)
|
||||
else:
|
||||
exposed_wall_area = total_wall_area * (number_exposed_walls.get(built_form, 3) / 4)
|
||||
|
||||
return exposed_wall_area
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue