handling edge cases for solar api

This commit is contained in:
Khalim Conn-Kowlessar 2024-10-02 10:48:54 +01:00
parent 37bd18642b
commit e4aa4cbb2f
5 changed files with 107 additions and 3 deletions

View file

@ -18,6 +18,7 @@ from recommendations.recommendation_utils import (
get_wall_type,
estimate_external_wall_area,
estimate_windows,
estimate_pitched_roof_area
)
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
from backend.app.utils import sap_to_epc
@ -631,7 +632,17 @@ class Property:
self.solar_panel_configuration = solar_panel_configuration
# We also set the roof area
self.roof_area = roof_area
if roof_area is None:
if self.roof["is_flat"]:
self.roof_area = estimate_pitched_roof_area(
floor_area=self.insulation_floor_area,
floor_height=self.floor_height
)
else:
self.roof_area = self.insulation_floor_area
else:
self.roof_area = roof_area
def set_current_energy_bill(self, kwh_client, kwh_predictions):
"""

View file

@ -589,3 +589,61 @@ class GoogleSolarApi:
# we need to do is perform the solar analysis and then half the results. We set an indicator which
# implies we should do this
self.double_property = True
@classmethod
def default_panel_performance(cls, property_instance):
"""
In a small number of cases, where properties have simulated uprns, we do not have a longitude and latitude
value and therefore we just return a default panel performance
:param property_instance:
:return:
"""
cost_instance = Costs(property_instance=property_instance)
# We return a 2.4 and 4 kwp system
panel_performance = pd.DataFrame(
[
{
'n_panels': 10,
'yearly_dc_energy': 4000 * 0.99, # Assumed 99% efficient wattage -> dc
'total_cost': cost_instance.solar_pv(
n_panels=10, has_battery=False, n_floors=property_instance.number_of_floors
)["total"],
'weighted_ratio': None,
'panneled_roof_area': 10 * 1.8,
'array_wattage': 4000,
'initial_ac_kwh_per_year': 4000 * 0.95, # Assumed 95% efficient wattage -> ac
'lifetime_ac_kwh': None,
'lifetime_dc_kwh': None,
'roi': None,
'generation_value': None,
'generation_deficit': None,
'expected_payback_years': None,
'surplus': None,
'combined_score': None,
'rank': None
},
{
'n_panels': 6,
'yearly_dc_energy': 2400 * 0.99, # Assumed 99% efficient wattage -> dc
'total_cost': cost_instance.solar_pv(
n_panels=6, has_battery=False, n_floors=property_instance.number_of_floors
)["total"],
'weighted_ratio': None,
'panneled_roof_area': 6 * 1.8,
'array_wattage': 2400,
'initial_ac_kwh_per_year': 2400 * 0.95, # Assumed 95% efficient wattage -> ac
'lifetime_ac_kwh': None,
'lifetime_dc_kwh': None,
'roi': None,
'generation_value': None,
'generation_deficit': None,
'expected_payback_years': None,
'surplus': None,
'combined_score': None,
'rank': None
},
]
)
return panel_performance

View file

@ -636,6 +636,21 @@ async def trigger_plan(body: PlanTriggerRequest):
if not property_instance.is_solar_pv_valid():
continue
if unit["longitude"] is None or unit["latitude"] is None:
# At this point, we've checked that solar PV is valid, and so we provide some defaults
property_instance.set_solar_panel_configuration(
solar_panel_configuration={
"insights_data": None,
"panel_performance": GoogleSolarApi.default_panel_performance(
property_instance=property_instance
),
"unit_share_of_energy": 1
},
roof_area=None
)
continue
# We check if we have a solar non-invasive recommendation
if [r for r in property_instance.non_invasive_recommendations if r["type"] == "solar_pv"]:
continue

View file

@ -5,6 +5,7 @@ import geopandas as gpd
from utils.logger import setup_logger
from utils.s3 import read_io_from_s3, save_dataframe_to_s3_parquet, read_dataframe_from_s3_parquet
from backend.Property import Property
from backend.SearchEpc import SearchEpc
logger = setup_logger()
@ -151,7 +152,7 @@ class OpenUprnClient:
bucket_name=bucket_name, file_key="spatial/filename_meta.parquet"
)
uprns = [p.uprn for p in input_properties]
uprns = [p.uprn for p in input_properties if p.uprn_source != SearchEpc.UPRN_SOURCE_SIMULATED]
uprn_map = cls.make_uprn_map(uprns, uprn_filenames)
for filename, associated_uprn in tqdm(uprn_map.items(), total=len(uprn_map)):
@ -165,6 +166,9 @@ class OpenUprnClient:
if p.uprn in associated_uprn:
p.set_spatial(spatial_df[spatial_df["UPRN"] == p.uprn])
if p.uprn_source == SearchEpc.UPRN_SOURCE_SIMULATED:
p.set_spatial(cls.empty_spatial_df())
# Perform a final check to ensure that all properties have spatial data
for p in input_properties:
if p.spatial is None:
@ -172,6 +176,22 @@ class OpenUprnClient:
return input_properties
@staticmethod
def empty_spatial_df():
return pd.DataFrame(
[
{
"X_COORDINATE": None,
"Y_COORDINATE": None,
"LATITUDE": None,
"LONGITUDE": None,
"conservation_status": False,
"is_listed_building": False,
"is_heritage_building": False,
}
]
)
@classmethod
def get_spatial_data(cls, uprns: list[int], bucket_name):
"""

View file

@ -655,7 +655,7 @@ def convert_thickness_to_numeric(string_thickness, is_pitched, is_flat):
return int(string_thickness)
def esimtate_pitched_roof_area(floor_area: float, floor_height: float) -> float:
def estimate_pitched_roof_area(floor_area: float, floor_height: float) -> float:
"""
This function will estimate the area of a pitched roof, given the floor area below the roof and the floor
height of the property.