mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
implemented unit level solar api
This commit is contained in:
parent
754d46073e
commit
b85fde1b21
3 changed files with 71 additions and 106 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue