diff --git a/.idea/Model.iml b/.idea/Model.iml
index 4413bb06..b0f9c00d 100644
--- a/.idea/Model.iml
+++ b/.idea/Model.iml
@@ -7,7 +7,7 @@
-
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 6f308057..1122b380 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/backend/Property.py b/backend/Property.py
index 3599f21b..fde0802d 100644
--- a/backend/Property.py
+++ b/backend/Property.py
@@ -18,6 +18,7 @@ from recommendations.recommendation_utils import (
esimtate_pitched_roof_area,
estimate_windows,
)
+from backend.ml_models.AnnualBillSavings import AnnualBillSavings
ENVIRONMENT = os.environ.get("ENVIRONMENT", "dev")
DATA_BUCKET = os.environ.get(
@@ -590,6 +591,23 @@ class Property:
self.set_energy_source()
self.find_energy_sources()
+ def set_current_energy_bill(self):
+ """
+ Given what we know about the property now, estimates the current energy consumption using the UCL paper
+ https://www.sciencedirect.com/science/article/pii/S0378778823002542
+ :return:
+ """
+ starting_heat_demand = (
+ float(self.data["energy-consumption-current"]) * self.floor_area
+ )
+
+ self.current_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
+ epc_energy_consumption=starting_heat_demand,
+ current_epc_rating=self.data["current-energy-rating"],
+ )
+
+ self.current_energy_bill = AnnualBillSavings.calculate_annual_bill(self.current_adjusted_energy)
+
def set_spatial(self, spatial: pd.DataFrame):
"""
Sets whether the property is in a conservation area given the output of the ConservationAreaClient
@@ -909,14 +927,13 @@ class Property:
return component_data
def set_adjusted_energy(
- self, current_adjusted_energy, expected_adjusted_energy, current_energy_bill, expected_energy_bill
+ self, expected_adjusted_energy, expected_energy_bill
):
"""
Stores these values for usage later
"""
- self.current_adjusted_energy = current_adjusted_energy
+
self.expected_adjusted_energy = expected_adjusted_energy
- self.current_energy_bill = current_energy_bill
self.expected_energy_bill = expected_energy_bill
def set_windows_count(self):
diff --git a/backend/apis/GoogleSolarApi.py b/backend/apis/GoogleSolarApi.py
index cac82f4b..99c49b2f 100644
--- a/backend/apis/GoogleSolarApi.py
+++ b/backend/apis/GoogleSolarApi.py
@@ -14,6 +14,31 @@ class GoogleSolarApi:
# be exported
SOLAR_CONSUMPTION_PROPORTION = 0.5
+ # These are variables, described in the documentation for cost analysis for non-us locations, seen here
+ # https://developers.google.com/maps/documentation/solar/calculate-costs-non-us
+ # We use the default figures that the API uses for US locations
+
+ # The factor by which the cost of electricity increases annually. The Solar API uses 1.022 (2.2% annual increase)
+ # for US locations.
+ cost_increase_factor = 1.022
+
+ # The efficiency at which an inverter converts the DC electricity that is produced by the solar panels to the AC
+ # electricity that is used in a household. The Solar API uses 85% for US locations. We use 0.95.5 which is the
+ # middle value of the 93-98% range, cited by Sunsave:
+ # https://www.sunsave.energy/solar-panels-advice/system-size/inverters
+ dc_to_ac_rate = 0.955
+
+ # The Solar API uses 1.04 (4% annual increase) for US locations
+ discount_rate = 1.04
+
+ # How much the efficiency of the solar panels declines each year. The Solar API uses 0.995 (0.5% annual decrease)
+ # for US locations
+ efficiency_depreciation_factor = 0.995
+
+ # The expected lifespan of the solar installation. The Solar API uses 20 years. Adjust this value as needed for
+ # your area
+ installation_life_span = 20
+
def __init__(self, api_key, max_retries=5):
"""
Initialize the GoogleSolarApi class with the provided API key and maximum retries.
@@ -94,6 +119,13 @@ class GoogleSolarApi:
self.insights_data["solarPotential"]["panelWidthMeters"]
)
self.panel_wattage = self.insights_data["solarPotential"]["panelCapacityWatts"]
+ if self.panel_wattage != 400:
+ # In the API documentation, it claims that the default output is 250W, however we've only seen 400W, so if
+ # we get anything other than 400W, we'll need to adjust the calculations in the output. For this, we should
+ # refer to https://developers.google.com/maps/documentation/solar/calculate-costs-non-us
+ # Where the documentation explains how to adjust the yearlyEnergyDcKwh figures.
+ # It should be straightforward, but I'd rather see an actual instance of this happening
+ raise NotImplementedError("Panel wattage is not 400W - implement me")
# Automatically exclude north-facing segments
self.exclude_north_facing_segments()
diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py
index 54e02766..0957b2d2 100644
--- a/backend/app/plan/router.py
+++ b/backend/app/plan/router.py
@@ -426,9 +426,7 @@ async def trigger_plan(body: PlanTriggerRequest):
(
recommendations_with_impact,
- current_adjusted_energy,
expected_adjusted_energy,
- current_energy_bill,
expected_energy_bill
) = (
Recommendations.calculate_recommendation_impact(
@@ -440,9 +438,7 @@ async def trigger_plan(body: PlanTriggerRequest):
# 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,
- current_energy_bill=current_energy_bill,
expected_energy_bill=expected_energy_bill
)
diff --git a/backend/ml_models/AnnualBillSavings.py b/backend/ml_models/AnnualBillSavings.py
index d88fe677..7395ab6b 100644
--- a/backend/ml_models/AnnualBillSavings.py
+++ b/backend/ml_models/AnnualBillSavings.py
@@ -1,3 +1,6 @@
+import numpy as np
+
+
class AnnualBillSavings:
"""
This is a simple class which will estimate the annual bill savings, based on the kwh savings.
@@ -60,8 +63,58 @@ class AnnualBillSavings:
return cls.ELECTRICITY_PRICE_CAP * kwh + (cls.DAILY_STANDARD_CHARGE_ELECTRICITY * 365)
+ @staticmethod
+ def calculate_occupants(total_floor_area):
+ """
+ From Table 1b of the SAP 2012 documentation https://bregroup.com/documents/d/bre-group/sap-2012_9-92
+ Provides a methodology to estimate occupancy, based on floor area. This is used to calculate the amount of
+ electricity used be appliances and during cooking.
+ :param total_floor_area:
+ :return:
+ """
+
+ if total_floor_area <= 13.9:
+ return 1
+
+ return 1 + (1.76 * (1 - np.exp(-0.000349 * (total_floor_area - 13.9) * (total_floor_area - 13.9))) + 0.0013 * (
+ total_floor_area - 13.9))
+
+ @staticmethod
+ def estimate_electrical_appliances(occupants, total_floor_area):
+ """
+ From secion L2 of SAP2012 Electrical appliances
+ https://bregroup.com/documents/d/bre-group/sap-2012_9-92
+ Used to estimate the amount of energy used by electrical appliances
+ :param occupants:
+ :param total_floor_area:
+ :return:
+ """
+ e_a = 207.8 * np.power(total_floor_area * occupants, 0.4717)
+
+ days_in_month = {
+ 1: 31,
+ 2: 28,
+ 3: 31,
+ 4: 30,
+ 5: 31,
+ 6: 30,
+ 7: 31,
+ 8: 31,
+ 9: 30,
+ 10: 31,
+ 11: 30,
+ 12: 31
+ }
+
+ eam = 0
+ for m in range(1, 13):
+ nm = days_in_month[m]
+ eam += e_a * (1 + 0.157 * np.cos(2 * np.pi * (m - 1.78) / 12)) * nm / 365
+
+ return eam
+
@classmethod
- def adjust_energy_to_metered(cls, epc_energy_consumption, current_epc_rating):
+ def adjust_energy_to_metered(cls, epc_energy_consumption, current_epc_rating, total_floor_area):
"""
The over-prediction of energy use by EPCs in Great Britain: A comparison
of EPC-modelled and metered primary energy use intensity
@@ -72,6 +125,13 @@ class AnnualBillSavings:
:return:
"""
+ # The EPC energy consumption does not factor in cooking and applicance use, so this is estimated using the
+ # methodology outlined in SAP, and is discussed in the UCL paper in section 3.1.1
+ estimated_occupants = cls.calculate_occupants(total_floor_area=total_floor_area)
+ appliances_energy_use = cls.estimate_electrical_appliances(estimated_occupants, total_floor_area)
+
+ epc_energy_consumption += appliances_energy_use
+
gradients = {
"A": -0.1,
"B": -0.1,
diff --git a/etl/customers/stonewater/shdf_3_clustering.py b/etl/customers/stonewater/shdf_3_clustering.py
index 5129dfb1..6c7a0fc6 100644
--- a/etl/customers/stonewater/shdf_3_clustering.py
+++ b/etl/customers/stonewater/shdf_3_clustering.py
@@ -6,6 +6,7 @@ from backend.SearchEpc import SearchEpc
import urllib.parse
import requests
from datetime import datetime
+from scipy import stats
from fuzzywuzzy import fuzz
import numpy as np
@@ -1598,7 +1599,6 @@ def compile_data_final():
property_attributes[c] = property_attributes[c].fillna(0)
property_attributes[c] = property_attributes[c].astype(float)
- from scipy import stats
for col in fill_with_mode:
property_attributes[col] = property_attributes[col].replace('', None)
mode_val = stats.mode([float(x) for x in property_attributes[col].values if x not in [None, "", np.nan]])[0]
@@ -1632,6 +1632,12 @@ def compile_data_final():
# s3_file_name="customers/Stonewater/clustering/clustering_dataframe.pkl"
# )
+ # from utils.s3 import read_pickle_from_s3
+ # data = read_pickle_from_s3(
+ # bucket_name="retrofit-data-dev",
+ # s3_file_name="customers/Stonewater/clustering/clustering_dataframe.pkl"
+ # )
+
# CLUSTERING!!
# from sklearn.cluster import KMeans
diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py
index 19fba581..c9ac1072 100644
--- a/recommendations/Recommendations.py
+++ b/recommendations/Recommendations.py
@@ -311,14 +311,6 @@ class Recommendations:
# This is the unadjusted resulting heat demand
predicted_heat_demand_change = starting_heat_demand - expected_heat_demand
- # 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"],
- )
-
# TODO: This isn't quite right as this is based on EVERY possible measure, not just the ones that are
# actually implemented
expected_adjusted_energy = AnnualBillSavings.adjust_energy_to_metered(
@@ -327,11 +319,10 @@ class Recommendations:
)
adjusted_heat_demand_change = (
- current_adjusted_energy - expected_adjusted_energy
+ property_instance.current_adjusted_energy - expected_adjusted_energy
)
# TODO: We should determine if the home is gas & electricity or just electricity
- current_energy_bill = AnnualBillSavings.calculate_annual_bill(current_adjusted_energy)
expected_energy_bill = AnnualBillSavings.calculate_annual_bill(expected_adjusted_energy)
for recommendations_by_type in property_recommendations:
@@ -410,8 +401,6 @@ class Recommendations:
return (
property_recommendations,
- current_adjusted_energy,
expected_adjusted_energy,
- current_energy_bill,
expected_energy_bill
)