implemented unit level solar api

This commit is contained in:
Khalim Conn-Kowlessar 2024-07-29 15:16:48 +01:00
parent 754d46073e
commit b85fde1b21
3 changed files with 71 additions and 106 deletions

View file

@ -159,7 +159,7 @@ class Property:
self.floor_height = epc_record.prepared_epc.get("floor_height")
self.insulation_wall_area = None
self.floor_area = epc_record.prepared_epc.get("total_floor_area")
self.pitched_roof_area = None
self.roof_area = None
self.insulation_floor_area = None
self.number_lighting_outlets = epc_record.prepared_epc.get(
"fixed_lighting_outlets_count"
@ -604,18 +604,12 @@ class Property:
def get_components(
self,
cleaned,
photo_supply_lookup,
floor_area_decile_thresholds,
energy_consumption_client
):
"""
Given the cleaning that has been performed, we'll use this to identify the property
components, from roof to walls to windows, heating and hot water
:param cleaned: This is the dictionary of components found in cleaner.cleaned
:param photo_supply_lookup: This is the lookup table for the photo supply, used to estimate the percentage
of the roof that is suitable for solar panels
:param floor_area_decile_thresholds: This is the decile thresholds for the floor area, used in estimating the
solar pv roof area
:param energy_consumption_client: Contains the heating and hot water kwh models - used to predict current
energy annual consumption in kWh
:return:
@ -680,20 +674,21 @@ class Property:
self.set_floor_type()
self.set_floor_level()
self.set_windows_count()
self.set_solar_panel_area(
photo_supply_lookup=photo_supply_lookup,
floor_area_decile_thresholds=floor_area_decile_thresholds,
)
self.set_energy_source()
self.find_energy_sources()
self.set_current_energy_bill(energy_consumption_client)
def set_solar_panel_configuration(self, solar_panel_configuration):
def set_solar_panel_configuration(
self, solar_panel_configuration, roof_area
):
"""
This funtion inserts the solar panel configuration into the property object
"""
self.solar_panel_configuration = solar_panel_configuration
# We also set the roof area
self.roof_area = roof_area
def set_current_energy_bill(self, energy_consumption_client):
"""
Given what we know about the property now, estimates the current energy consumption using the UCL paper
@ -1079,9 +1074,9 @@ class Property:
if condition_data["main_dwelling_ground_floor_area"] is not None \
else self.floor_area / self.number_of_floors
self.pitched_roof_area = esimtate_pitched_roof_area(
floor_area=self.insulation_floor_area, floor_height=self.floor_height
)
# self.pitched_roof_area = esimtate_pitched_roof_area(
# floor_area=self.insulation_floor_area, floor_height=self.floor_height
# )
def set_floor_level(self):
self.floor_level = (
@ -1195,48 +1190,6 @@ class Property:
if condition_data["windows_area"] is not None \
else None
def set_solar_panel_area(self, photo_supply_lookup, floor_area_decile_thresholds):
"""
Sets the approximate area of the solar panels
:return:
"""
if (self.insulation_floor_area is None) and (self.pitched_roof_area is None):
raise ValueError(
"Need to set insulation floor area and pitched roof area before setting solar pv roof area"
)
photo_supply_matched = SolarPhotoSupply.filter_photo_supply_lookup(
photo_supply_lookup=photo_supply_lookup,
floor_area_decile_thresholds=floor_area_decile_thresholds,
tenure=self.data["tenure"],
built_form=self.data["built-form"],
property_type=self.data["property-type"],
construction_age_band=self.construction_age_band,
is_flat=self.roof["is_flat"],
is_pitched=self.roof["is_pitched"],
is_roof_room=self.roof["is_roof_room"],
floor_area=self.floor_area,
)
percentage_of_roof = photo_supply_matched["photo_supply_median"].mean()
percentage_of_roof = percentage_of_roof / 100
self.solar_pv_percentage = percentage_of_roof
def get_solar_pv_roof_area(self, percentage_of_roof):
"""
Given a percentage of the roof, this method will return the estimated area of the solar panels
:param percentage_of_roof:
:return:
"""
return (
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.
@ -1335,6 +1288,23 @@ class Property:
return suitable_property_type and not has_air_source_heat_pump
def is_solar_pv_valid(self):
# If the property is a flat but we are looking at building solar potential, we can include this
if (self.building_id is not None) and (self.solar_panel_configuration is not None):
return True
is_valid_property_type = self.data["property-type"] in ["House", "Bungalow", "Maisonette"]
is_valid_roof_type = (
self.roof["is_flat"] or self.roof["is_pitched"] or self.roof["is_roof_room"]
)
# If there is no existing solar PV, the photo-supply field will be None or a missing value
has_no_existing_solar_pv = self.data["photo-supply"] in [
None, 0, self.DATA_ANOMALY_MATCHES
]
return is_valid_property_type and is_valid_roof_type and has_no_existing_solar_pv
def estimate_electrical_consumption(self, assumed_ashp_efficiency, exclusions):
"""
Given a property, this method estimates the electrical consumption of the property, based on the energy

View file

@ -408,7 +408,6 @@ async def trigger_plan(body: PlanTriggerRequest):
uprn_filenames = read_dataframe_from_s3_parquet(
bucket_name=get_settings().DATA_BUCKET, file_key="spatial/filename_meta.parquet"
)
photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(bucket=get_settings().DATA_BUCKET)
solar_api_client = GoogleSolarApi(api_key=get_settings().GOOGLE_SOLAR_API_KEY)
dataset_version = "2024-07-08"
@ -425,10 +424,10 @@ async def trigger_plan(body: PlanTriggerRequest):
logger.info("Getting spatial data")
for p in input_properties:
p.get_components(cleaned, photo_supply_lookup, floor_area_decile_thresholds, energy_consumption_client)
p.get_components(cleaned=cleaned, energy_consumption_client=energy_consumption_client)
p.get_spatial_data(uprn_filenames)
# TODO: Handle the case of modelling some units as buildings and some as properties individually
# TODO: Tidy this up
building_ids = [
{
"building_id": p.building_id,
@ -439,7 +438,7 @@ async def trigger_plan(body: PlanTriggerRequest):
# property to achieve post retrofit of just the fabric
"energy_consumption": energy_consumption_client.estimate_new_consumption(
current_energy_efficiency=p.data["current-energy-efficiency"],
target_efficiency="C",
target_efficiency="69",
current_consumption=p.estimate_electrical_consumption(
assumed_ashp_efficiency=assumptions.AVERAGE_ASHP_EFFICIENCY, exclusions=body.exclusions
)
@ -448,6 +447,24 @@ async def trigger_plan(body: PlanTriggerRequest):
"uprn": p.uprn
} for p in input_properties if p.building_id is not None
]
individual_units = [
{
"longitude": p.spatial["longitude"],
"latitude": p.spatial["latitude"],
# Energy consumption is adjusted for the property's expected post retrofit state
# We set the target rating to EPC C, which is the typical EPC rating we would expect the
# property to achieve post retrofit of just the fabric
"energy_consumption": energy_consumption_client.estimate_new_consumption(
current_energy_efficiency=p.data["current-energy-efficiency"],
target_efficiency="69",
current_consumption=p.estimate_electrical_consumption(
assumed_ashp_efficiency=assumptions.AVERAGE_ASHP_EFFICIENCY, exclusions=body.exclusions
),
),
"property_id": p.id,
"uprn": p.uprn
} for p in input_properties if p.building_id is None
]
if building_ids:
# Find the unique longitude and latitude pairs for each building id
unique_coordinates = {}
@ -511,32 +528,21 @@ async def trigger_plan(body: PlanTriggerRequest):
)
p.set_solar_panel_configuration(unit_solar_panel_configuration)
else:
if individual_units:
# Model the solar potential at the property level
for p in input_properties:
# TODO: Complete me! - we probably won't do this for individual flats - IGNORE FLATS FROM THIS WITHOUT
# BUILDING IDS
electric_consumption = p.estimate_electrical_consumption(
assumed_ashp_efficiency=assumptions.AVERAGE_ASHP_EFFICIENCY, exclusions=body.exclusions
)
# We now decrease this, based on the expected energy efficiency of the property post retrofit to a C,
# which is the common level we would expect the property to reach when treating the fabric of the
# home
electric_consumption = energy_consumption_client.estimate_new_consumption(
current_energy_efficiency=p.data["current-energy-efficiency"],
target_efficiency="69",
current_consumption=electric_consumption
)
for unit in individual_units:
property_instance = [p for p in input_properties if p.id == unit["property_id"]][0]
# At this level, we check if the property is suitable for solar and if now, skip
if not property_instance.is_solar_pv_valid():
continue
solar_api_client.get(
longitude=p.spatial["longitude"],
latitude=p.spatial["latitude"],
energy_consumption=electric_consumption,
longitude=unit["longitude"],
latitude=unit["latitude"],
energy_consumption=unit["energy_consumption"],
is_building=False,
session=session,
uprn=p.uprn
uprn=unit["uprn"]
)
# Store the data in the database
@ -544,16 +550,22 @@ async def trigger_plan(body: PlanTriggerRequest):
solar_api_client.save_to_db(
session=session,
uprns_to_location=[
{"uprn": p.uprn, "longitude": p.spatial["longitude"], "latitude": p.spatial["latitude"]}
{
"uprn": property_instance.uprn,
"longitude": property_instance.spatial["longitude"],
"latitude": property_instance.spatial["latitude"]
}
],
scenario_type="unit"
)
# TODO: Insert the pitched roof area into the property class as we store the solar performance
# in the property class
print("Implement me")
# TODO: We can set the pitched roof area based on the results of the solar api!
property_instance.set_solar_panel_configuration(
solar_panel_configuration={
"insights_data": solar_api_client.insights_data,
"panel_performance": solar_api_client.panel_performance
},
roof_area=solar_api_client.roof_area
)
logger.info("Getting components and epc recommendations")
recommendations = {}

View file

@ -78,23 +78,6 @@ class SolarPvRecommendations:
}
]
def is_solar_pv_valid(self):
# If the property is a flat but we are looking at building solar potential, we can include this
if (self.property.building_id is not None) and (self.property.solar_panel_configuration is not None):
return True
is_valid_property_type = self.property.data["property-type"] in ["House", "Bungalow", "Maisonette"]
is_valid_roof_type = (
self.property.roof["is_flat"] or self.property.roof["is_pitched"] or self.property.roof["is_roof_room"]
)
# If there is no existing solar PV, the photo-supply field will be None or a missing value
has_no_existing_solar_pv = self.property.data["photo-supply"] in [
None, 0, self.property.DATA_ANOMALY_MATCHES
]
return is_valid_property_type and is_valid_roof_type and has_no_existing_solar_pv
def recommend_building_analysis(self, phase):
"""
This recommendation approach handles the case of producing solar PV recommendations at the building level,
@ -159,7 +142,7 @@ class SolarPvRecommendations:
:return:
"""
if not self.is_solar_pv_valid():
if not self.property.is_solar_pv_valid():
return
# If we have a buiilding level analysis, we implement separate logic