implementing new process to adjust energy savings and reduce complexity in router

This commit is contained in:
Khalim Conn-Kowlessar 2024-02-16 13:01:19 +00:00
parent 82d19fc6fc
commit 5d45243e99
4 changed files with 105 additions and 186 deletions

View file

@ -42,6 +42,7 @@ class Property:
walls = None
windows = None
lighting = None
energy_source = None
spatial = None
base_difference_record = None
@ -417,6 +418,7 @@ class Property:
self.set_solar_panel_area(
photo_supply_lookup=photo_supply_lookup, floor_area_decile_thresholds=floor_area_decile_thresholds
)
self.set_energy_source()
def set_spatial(self, spatial: pd.DataFrame):
"""
@ -749,3 +751,20 @@ class Property:
self.insulation_floor_area * percentage_of_roof if self.roof["is_flat"] else
self.pitched_roof_area * percentage_of_roof
)
def set_energy_source(self):
"""
This method sets the energy source of the property, based on the mains gas flag and energy tariff.
"""
# Default to "electricity_and_gas" to cover most scenarios including when mains_gas_flag is True
energy_source = "electricity_and_gas"
# If the tariff explicitly indicates electricity use without a dual indication and mains_gas_flag is not True
# We check for the common electricity tariffs
if not self.data["mains_gas_flag"] and self.data["energy_tariff"] in [
"Single", "off-peak 7 hour", "off-peak 10 hour", "off-peak 18 hour", "standard tariff", "24 hour"
]:
energy_source = "electricity"
# Set the energy source based on the conditions above
self.energy_source = energy_source

View file

@ -81,6 +81,7 @@ def upload_recommendations(session: Session, recommendations_to_upload, property
"new_u_value": rec.get("new_u_value"),
"sap_points": rec["sap_points"],
"heat_demand": rec["heat_demand"],
"adjusted_heat_demand": rec["adjusted_heat_demand"],
"co2_equivalent_savings": rec["co2_equivalent_savings"],
"total_work_hours": rec["labour_hours"],
"energy_cost_savings": rec["energy_cost_savings"],

View file

@ -136,7 +136,7 @@ async def trigger_plan(body: PlanTriggerRequest):
recommendations = {}
recommendations_scoring_data = []
representive_recommendations = {}
representative_recommendations = {}
for p in input_properties:
# Property recommendations
@ -151,7 +151,7 @@ async def trigger_plan(body: PlanTriggerRequest):
continue
recommendations[p.id] = property_recommendations
representive_recommendations[p.id] = property_representative_recommendations
representative_recommendations[p.id] = property_representative_recommendations
p.create_base_difference_epc_record(cleaned_lookup=cleaned)
p.adjust_difference_record_with_recommendations(
@ -185,10 +185,18 @@ async def trigger_plan(body: PlanTriggerRequest):
property_instance = [p for p in input_properties if p.id == property_id][0]
recommendations_with_impact = Recommendations.calculate_recommendation_impact(
property_instance=property_instance,
all_predictions=all_predictions,
recommendations=recommendations
recommendations_with_impact, current_adjusted_energy, expected_adjusted_energy = (
Recommendations.calculate_recommendation_impact(
property_instance=property_instance,
all_predictions=all_predictions,
recommendations=recommendations
)
)
# Store the resulting adjusted energy in the property instance
property_instance.set_adjusted_energy(
current_adjusted_energy=current_adjusted_energy,
expected_adjusted_energy=expected_adjusted_energy
)
input_measures = prepare_input_measures(recommendations_with_impact, body.goal)
@ -242,174 +250,6 @@ async def trigger_plan(body: PlanTriggerRequest):
]
recommendations[property_id] = final_recommendations
# This is a temporary step, to estimate the impact of the measured on heat demand and carbon
# TODO: This needs to be cleaned up, if it happens to be kept
representative_recs = {}
for property_id, property_recommendations in recommendations.items():
default_recommendations = [r for r in property_recommendations if r["default"]]
default_types = {x["type"] for x in default_recommendations}
# Missing types
missing_types = list(set([r["type"] for r in property_recommendations if r["type"] not in default_types]))
# We might have a missing type as one of the solid wall options because for a solid wall, you might
# have ewi or iwi but only one of them will be a default
if ("internal_wall_insulation" in default_types) or ("external_wall_insaultion" in default_types):
missing_types = [
t for t in missing_types if t not in ["internal_wall_insulation", "external_wall_insulation"]
]
# We check if NO wall insulation was selected but iwi and ewi are available
# This condition will check
# 1) iwi and ewi are both in missing_types
# 2) iwi and ewi are not in default_types
# If both of these are true, it means that no wall insulation was selected via the optimisation routine
# but both are possible, so we need to select a default. We default to iwi because it's usually cheaper
if (("internal_wall_insulation" in missing_types) and ("external_wall_insulation" in missing_types)) and (
("internal_wall_insulation" not in default_types) and ("external_wall_insulation" not in default_types)
):
missing_types = [t for t in missing_types if t != "external_wall_insulation"]
if missing_types:
for missed_type in missing_types:
missed = [r for r in property_recommendations if r["type"] == missed_type]
min_cost = min([r["total"] for r in missed])
# Grab a representative, based on cheapest cost
representative_rec = [r for r in property_recommendations if np.isclose(r["total"], min_cost)]
default_recommendations.append(representative_rec[0])
representative_recs[property_id] = default_recommendations
# We update the carbon and heat demand predictions
# TODO: The api call producing all_combined_predictions has been removed so we can potentially completely
# refactor this block to just perform the energy adjustments
for property_id, property_recommendations in recommendations.items():
property_instance = [p for p in input_properties if p.id == property_id][0]
heat_demand_change = sum(
x.get("heat_demand", 0) for x in representative_recs[property_id] if
x["type"] not in ["mechanical_ventilation", "low_energy_lighting"]
)
carbon_change = sum(
x.get("co2_equivalent_savings", 0) for x in representative_recs[property_id] if
x["type"] not in ["mechanical_ventilation", "low_energy_lighting"]
)
starting_heat_demand = (
float(property_instance.data["energy-consumption-current"]) * property_instance.floor_area
)
expected_heat_demand = starting_heat_demand - heat_demand_change
# We don't want to adjust the heat demand for mechanical ventilation so we add it back on
# We adjust the heat demand figures to align to the UCL paper
current_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
epc_energy_consumption=starting_heat_demand,
current_epc_rating=property_instance.data["current-energy-rating"],
)
# We sum up the SAP points of the default recommendations and calculate a new EPC category. This
# category is then used to produce adjusted energy figures
expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
epc_energy_consumption=expected_heat_demand,
current_epc_rating=property_instance.data["current-energy-rating"],
)
heat_demand_change = (
current_adjusted_energy - expected_adjusted_energy
)
# update the recommendations
# We need to totals for the representative recommendations
representative_rec_data = [
{
"recommendation_id": r["recommendation_id"],
"co2_equivalent_savings": r.get("co2_equivalent_savings"),
"heat_demand": r.get("heat_demand"),
"type": r["type"]
} for r
in representative_recs[property_id]
]
representative_rec_data = pd.DataFrame(representative_rec_data)
# Suppress mechanical ventilation to have zero heat demand and co2
representative_rec_data.loc[
representative_rec_data["type"] == "mechanical_ventilation", "co2_equivalent_savings"
] = 0
representative_rec_data.loc[
representative_rec_data["type"] == "mechanical_ventilation", "heat_demand"
] = 0
# Supress low energy lighting to have zero heat demand and co2 - this does not get affected by this process
representative_rec_data.loc[
representative_rec_data["type"] == "low_energy_lighting", "co2_equivalent_savings"
] = 0
representative_rec_data.loc[
representative_rec_data["type"] == "low_energy_lighting", "heat_demand"
] = 0
# Convert co2 and heat demand to proportions of their column sums
representative_rec_data["co2_equivalent_savings_percent"] = (
representative_rec_data["co2_equivalent_savings"] /
representative_rec_data["co2_equivalent_savings"].sum()
)
representative_rec_data["heat_demand_percent"] = (
representative_rec_data["heat_demand"] / representative_rec_data["heat_demand"].sum()
)
# We'll use the proportions to update the carbon and heat demand
representative_rec_data["co2_equivalent_savings"] = (
carbon_change * representative_rec_data["co2_equivalent_savings_percent"]
)
representative_rec_data["heat_demand"] = (
heat_demand_change * representative_rec_data["heat_demand_percent"]
)
# Finally, insert these values into the final recommendations
for rec in property_recommendations:
if rec["type"] in ["external_wall_insulation", "internal_wall_insulation"]:
change_data = representative_rec_data[
representative_rec_data["type"].isin(["external_wall_insulation", "internal_wall_insulation"])
]
else:
change_data = representative_rec_data[representative_rec_data["type"] == rec["type"]]
if rec["type"] == "mechanical_ventilation":
rec["co2_equivalent_savings"] = 0
rec["heat_demand"] = 0
rec["energy_cost_savings"] = 0
elif rec["type"] == "low_energy_lighting":
# We do not convert, we just calculate energy cost savings
rec["energy_cost_savings"] = AnnualBillSavings.estimate_electric(rec["heat_demand"])
continue
else:
rec["co2_equivalent_savings"] = change_data["co2_equivalent_savings"].values[0]
rec["heat_demand"] = change_data["heat_demand"].values[0]
# If the recommendation is solar, the savings are entirely in electricity
if rec["type"] == "solar_pv":
rec["energy_cost_savings"] = AnnualBillSavings.estimate_electric(rec["heat_demand"])
else:
rec["energy_cost_savings"] = AnnualBillSavings.estimate(rec["heat_demand"])
# Update recommendations
recommendations[property_id] = property_recommendations
# For expected adjust energy, we don't include mechanical ventilation so we'll add it back on
mechanical_ventilation_rec = representative_rec_data[
representative_rec_data["type"] == "mechanical_ventilation"
]
if not mechanical_ventilation_rec.empty:
expected_adjusted_energy = (
expected_adjusted_energy + mechanical_ventilation_rec["heat_demand"].values[0]
)
property_instance.set_adjusted_energy(
current_adjusted_energy=current_adjusted_energy,
expected_adjusted_energy=expected_adjusted_energy
)
# 1) the property data
# 2) the property details (epc)
# 3) the recommendations

View file

@ -1,3 +1,5 @@
import numpy as np
from backend.Property import Property
from typing import List
from itertools import groupby
@ -213,6 +215,44 @@ class Recommendations:
heat_phase_impact = property_heat_predictions.groupby("phase")["predictions"].median().reset_index()
carbon_phase_impact = property_carbon_predictions.groupby("phase")["predictions"].median().reset_index()
## TODO: NEW
# The heat demand change is the difference between the starting heat demand and the value at the final phase
expected_heat_demand = property_instance.floor_area * (
heat_phase_impact[heat_phase_impact["phase"] == max(heat_phase_impact["phase"])]["predictions"].values[0]
)
expected_carbon = (
carbon_phase_impact[carbon_phase_impact["phase"] == max(carbon_phase_impact["phase"])][
"predictions"].values[0]
)
starting_heat_demand = (
float(property_instance.data["energy-consumption-current"]) * property_instance.floor_area
)
# This is the unadjusted resulting heat demand
predicted_heat_demand_change = starting_heat_demand - expected_heat_demand
starting_carbon = float(property_instance.data["co2-emissions-current"])
# We don't want to adjust the heat demand for mechanical ventilation so we add it back on
# We adjust the heat demand figures to align to the UCL paper
current_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
epc_energy_consumption=starting_heat_demand,
current_epc_rating=property_instance.data["current-energy-rating"],
)
expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
epc_energy_consumption=expected_heat_demand,
current_epc_rating=property_instance.data["current-energy-rating"],
)
adjusted_heat_demand_change = (
current_adjusted_energy - expected_adjusted_energy
)
for recommendations_by_type in property_recommendations:
for rec in recommendations_by_type:
@ -233,39 +273,58 @@ class Recommendations:
)]["predictions"].values[0]
if rec["phase"] == 0:
rec["sap_points"] = new_sap - float(property_instance.data["current-energy-efficiency"])
rec["co2_equivalent_savings"] = float(property_instance.data["co2-emissions-current"]) - new_carbon
rec["heat_demand"] = property_instance.floor_area * (
predicted_sap_points = new_sap - float(property_instance.data["current-energy-efficiency"])
predicted_co2_savings = float(property_instance.data["co2-emissions-current"]) - new_carbon
predicted_heat_demand = property_instance.floor_area * (
float(property_instance.data["energy-consumption-current"]) - new_heat_demand
)
else:
previous_phase = rec["phase"] - 1
rec["sap_points"] = (
predicted_sap_points = (
new_sap - sap_phase_impact[sap_phase_impact["phase"] == previous_phase]["predictions"].values[0]
)
rec["co2_equivalent_savings"] = (
predicted_co2_savings = (
carbon_phase_impact[carbon_phase_impact["phase"] == previous_phase]["predictions"].values[0] -
new_carbon
)
rec["heat_demand"] = property_instance.floor_area * (
predicted_heat_demand = property_instance.floor_area * (
heat_phase_impact[heat_phase_impact["phase"] == previous_phase]["predictions"].values[0] -
new_heat_demand
)
if rec["type"] == "low_energy_lighting":
# For the moment, we cap the number of SAP points that can be achieved by ventilation at 2
rec["sap_points"] = min(rec["sap_points"], LightingRecommendations.SAP_LIMIT)
rec["sap_points"] = min(predicted_sap_points, LightingRecommendations.SAP_LIMIT)
rec["co2_equivalent_savings"] = min(predicted_co2_savings, rec["co2_equivalent_savings"])
rec["heat_demand"] = min(predicted_heat_demand, rec["heat_demand"])
else:
rec["sap_points"] = predicted_sap_points
rec["co2_equivalent_savings"] = predicted_co2_savings
rec["heat_demand"] = predicted_heat_demand
# Round to 2 decimal places
rec["sap_points"] = round(rec["sap_points"], 2)
# Energy consumption current is per meter squared, so we need to multiply by the floor area to get
# an absolute figure for the home
rec["energy_cost_savings"] = AnnualBillSavings.estimate(rec["heat_demand"])
# We now calculate the adjusted heat demand for this recommendation, which is simply the percentage
# of the total adjusted heat demand change. The percentage we use is this recommendation's percentage
# of the total heat demand per square meter change
rec["adjusted_heat_demand"] = adjusted_heat_demand_change * (
rec["heat_demand"] / predicted_heat_demand_change
)
# We make sure this is NOT below 0
rec["adjusted_heat_demand"] = max(0, rec["heat_demand"])
# Depending on the property's tarriff, we calculate the amount of energy savings this measure will bring
if property_instance.energy_source == "electricity":
rec["energy_cost_savings"] = AnnualBillSavings.estimate_electric(rec["heat_demand"])
elif property_instance.energy_source == "electricity_and_gas":
rec["energy_cost_savings"] = AnnualBillSavings.estimate(rec["heat_demand"])
else:
raise ValueError("Invalid value for energy source")
if (rec["sap_points"] is None) and (rec["co2_equivalent_savings"] is None) or (
rec["heat_demand"] is None) or (rec["energy_cost_savings"] is None):
raise ValueError("sap points, co2 or heat demand is missing")
return property_recommendations
return property_recommendations, current_adjusted_energy, expected_adjusted_energy