mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
implementing new process to adjust energy savings and reduce complexity in router
This commit is contained in:
parent
82d19fc6fc
commit
5d45243e99
4 changed files with 105 additions and 186 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue