mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
updating solar recommender
This commit is contained in:
parent
53b2ca05b6
commit
d45059e40d
4 changed files with 73 additions and 90 deletions
|
|
@ -8,6 +8,7 @@ import time
|
|||
from backend.app.db.functions.solar_functions import get_solar_data, store_batch_data
|
||||
from utils.logger import setup_logger
|
||||
from sklearn.preprocessing import MinMaxScaler
|
||||
from recommendations.Costs import Costs
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
|
@ -107,7 +108,14 @@ class GoogleSolarApi:
|
|||
|
||||
@lru_cache(maxsize=128)
|
||||
def get(
|
||||
self, longitude, latitude, energy_consumption, required_quality="MEDIUM", is_building=False, session=None,
|
||||
self,
|
||||
longitude,
|
||||
latitude,
|
||||
energy_consumption,
|
||||
property_instance=None,
|
||||
required_quality="MEDIUM",
|
||||
is_building=False,
|
||||
session=None,
|
||||
uprn=None
|
||||
):
|
||||
"""
|
||||
|
|
@ -116,6 +124,7 @@ class GoogleSolarApi:
|
|||
:param longitude: The longitude of the location.
|
||||
:param latitude: The latitude of the location.
|
||||
:param energy_consumption: The energy consumption of the building/unit associated to the longitude and latitude.
|
||||
:param property_instance: The property instance associated to the longitude and latitude.
|
||||
:param required_quality: The required quality of the data (default is "MEDIUM").
|
||||
:param is_building: Whether the energy consumption is for a building or a unit.
|
||||
:param session: The database session to use for the query (default is None).
|
||||
|
|
@ -158,7 +167,9 @@ class GoogleSolarApi:
|
|||
self.roof_segment_indexes = [segment['segmentIndex'] for segment in self.roof_segments]
|
||||
|
||||
# We now start finding the solar panel configurations
|
||||
self.optimise_solar_configuration(energy_consumption=energy_consumption, is_building=is_building)
|
||||
self.optimise_solar_configuration(
|
||||
energy_consumption=energy_consumption, is_building=is_building, property_instance=property_instance
|
||||
)
|
||||
|
||||
def save_to_db(self, session, uprns_to_location, scenario_type):
|
||||
if self.insights_data is None:
|
||||
|
|
@ -178,7 +189,7 @@ class GoogleSolarApi:
|
|||
"yearly_dc_energy",
|
||||
"total_cost",
|
||||
"panneled_roof_area",
|
||||
"array_warrage",
|
||||
"array_wattage",
|
||||
"initial_ac_kwh_per_year",
|
||||
"lifetime_ac_kwh",
|
||||
"roi",
|
||||
|
|
@ -191,7 +202,7 @@ class GoogleSolarApi:
|
|||
"yearly_dc_energy": "yearly_dc_kwh",
|
||||
"total_cost": "cost",
|
||||
"panneled_roof_area": "panelled_roof_area",
|
||||
"array_warrage": "array_kwhp",
|
||||
"array_wattage": "array_kwhp",
|
||||
"initial_ac_kwh_per_year": "yearly_ac_kwh",
|
||||
}
|
||||
)
|
||||
|
|
@ -226,12 +237,14 @@ class GoogleSolarApi:
|
|||
installation_life_span)) /
|
||||
(1 - efficiency_depreciation_factor))
|
||||
|
||||
def optimise_solar_configuration(self, energy_consumption, is_building=False):
|
||||
def optimise_solar_configuration(self, energy_consumption, is_building=False, property_instance=None):
|
||||
"""
|
||||
Optimise the solar panel configuration for the building.
|
||||
:return:
|
||||
"""
|
||||
|
||||
cost_instance = Costs(property_instance=property_instance) if property_instance is not None else None
|
||||
|
||||
# Remove any north facing roof segments
|
||||
panel_performance = []
|
||||
for config in self.insights_data["solarPotential"]["solarPanelConfigs"]:
|
||||
|
|
@ -246,7 +259,14 @@ class GoogleSolarApi:
|
|||
wattage = segment["panelsCount"] * self.insights_data["solarPotential"]["panelCapacityWatts"]
|
||||
generated_dc_energy = segment["yearlyEnergyDcKwh"]
|
||||
ratio = generated_dc_energy / wattage
|
||||
cost = MCS_SOLAR_PV_COST_DATA["average_cost_per_kwh"] * (wattage / 1000)
|
||||
|
||||
if cost_instance is None:
|
||||
cost = MCS_SOLAR_PV_COST_DATA["average_cost_per_kwh"] * (wattage / 1000)
|
||||
else:
|
||||
cost = cost_instance.solar_pv(
|
||||
wattage=wattage, has_battery=False
|
||||
)["total"]
|
||||
|
||||
roi_summary.append(
|
||||
{
|
||||
"segmentIndex": segment["segmentIndex"],
|
||||
|
|
@ -274,7 +294,7 @@ class GoogleSolarApi:
|
|||
"total_cost": total_cost,
|
||||
"weighted_ratio": weighted_ratio,
|
||||
"panneled_roof_area": roi_summary["panneled_roof_area"].sum(),
|
||||
"array_warrage": roi_summary["n_panels"].sum() * self.panel_wattage
|
||||
"array_wattage": roi_summary["n_panels"].sum() * self.panel_wattage
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -290,7 +310,7 @@ class GoogleSolarApi:
|
|||
|
||||
# Remove anything where the total ac energy is less than half of the array wattage
|
||||
panel_performance = panel_performance[
|
||||
(panel_performance["initial_ac_kwh_per_year"] / panel_performance["array_warrage"]) >= 0.5
|
||||
(panel_performance["initial_ac_kwh_per_year"] / panel_performance["array_wattage"]) >= 0.5
|
||||
]
|
||||
|
||||
# 2) Calculate the liftime solar energy production
|
||||
|
|
|
|||
|
|
@ -546,7 +546,8 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
energy_consumption=unit["energy_consumption"],
|
||||
is_building=False,
|
||||
session=session,
|
||||
uprn=unit["uprn"]
|
||||
uprn=unit["uprn"],
|
||||
property_instance=property_instance
|
||||
)
|
||||
|
||||
# Store the data in the database
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class SolarPvRecommendations:
|
|||
roof_coverage_percent = round(recommendation_config["panneled_roof_area"] / total_roof_area * 100)
|
||||
# Spread the cost to the individual units - adding a 20% contingency
|
||||
total_cost = recommendation_config["total_cost"] / n_units
|
||||
kw = np.floor(recommendation_config["array_warrage"] / 100) / 10
|
||||
kw = np.floor(recommendation_config["array_wattage"] / 100) / 10
|
||||
# Default to a weeks work for a team of 3 people doing 8 hour days
|
||||
labour_days = 5
|
||||
labour_hours = 3 * 8 * labour_days
|
||||
|
|
@ -150,84 +150,46 @@ class SolarPvRecommendations:
|
|||
self.recommend_building_analysis(phase)
|
||||
return
|
||||
|
||||
solar_pv_percentage = self.property.solar_pv_percentage
|
||||
# We round up to the neaest 10%
|
||||
solar_pv_percentage = np.ceil(solar_pv_percentage * 10) / 10
|
||||
panel_performance = self.property.solar_panel_configuration["panel_performance"]
|
||||
roof_area = self.property.roof_area
|
||||
|
||||
# For the solar recommendations, we produce the following scenarios:
|
||||
# 1) Solar panels only, we present a high, medium and low coverage
|
||||
# 2) With and without battery
|
||||
roof_coverage_scenarios = [
|
||||
solar_pv_percentage - 0.1, solar_pv_percentage,
|
||||
]
|
||||
if solar_pv_percentage <= 0.4:
|
||||
roof_coverage_scenarios.append(solar_pv_percentage + 0.1)
|
||||
# We make sure we haven't gone too low or high - we allow no more than 60% coverage
|
||||
roof_coverage_scenarios = [v for v in roof_coverage_scenarios if 0 <= v <= 0.6]
|
||||
# If we only have two scenarios, we add a coverage scenario 10% less than the smallest
|
||||
if len(roof_coverage_scenarios) == 2:
|
||||
roof_coverage_scenarios.insert(0, roof_coverage_scenarios[0] - 0.1)
|
||||
battery_scenarios = [False, True]
|
||||
solar_configurations = panel_performance.head(3).reset_index(drop=True)
|
||||
|
||||
scenarios_with_wattage = []
|
||||
for roof_coverage in roof_coverage_scenarios:
|
||||
# We now have a property which is potentially suitable for solar PV
|
||||
solar_pv_roof_area = self.property.get_solar_pv_roof_area(roof_coverage)
|
||||
# We combine each of these configurations with estimates with and without a battery
|
||||
for rank, recommendation_config in solar_configurations.iterrows():
|
||||
roof_coverage_percent = round(recommendation_config["panneled_roof_area"] / roof_area * 100)
|
||||
for has_battery in [False, True]:
|
||||
cost_result = self.costs.solar_pv(
|
||||
wattage=recommendation_config["array_wattage"], has_battery=has_battery
|
||||
)
|
||||
kw = np.floor(recommendation_config["array_wattage"] / 100) / 10
|
||||
if has_battery:
|
||||
description = (f"Install a {kw} kilowatt-peak (kWp) solar photovoltaic (PV) panel system on "
|
||||
f"{round(roof_coverage_percent)}% the roof, with a battery storage system.")
|
||||
else:
|
||||
description = (f"Install a {kw} kilowatt-peak (kWp) solar photovoltaic (PV) p"
|
||||
f"anel system on {round(roof_coverage_percent)}% the roof.")
|
||||
|
||||
number_solar_panels = np.floor(solar_pv_roof_area / self.SOLAR_PANEL_AREA)
|
||||
solar_panel_wattage = number_solar_panels * self.SOLAR_PANEL_WATTAGE
|
||||
already_installed = "solar_pv" in self.property.already_installed
|
||||
if already_installed:
|
||||
cost_result = override_costs(cost_result)
|
||||
|
||||
if solar_panel_wattage < self.MIN_SYSTEM_WATTAGE:
|
||||
continue
|
||||
|
||||
solar_panel_wattage = np.clip(
|
||||
a=solar_panel_wattage, a_min=self.MIN_SYSTEM_WATTAGE, a_max=self.MAX_SYSTEM_WATTAGE
|
||||
)
|
||||
scenarios_with_wattage.append((roof_coverage, solar_panel_wattage))
|
||||
|
||||
# We trim the scenarios, so that we don't have duplicate wattages
|
||||
scenarios_with_wattage = self.trim_solar_wattage_options(scenarios_with_wattage)
|
||||
|
||||
# Produce the cross product of the scenarios
|
||||
scenarios = [
|
||||
(roof, wattage, battery) for roof, wattage in scenarios_with_wattage for battery in battery_scenarios
|
||||
]
|
||||
# We deduce the wattage of the solar panels based on the roof coverage
|
||||
|
||||
for roof_coverage, solar_panel_wattage, has_battery in scenarios:
|
||||
# We now have a property which is potentially suitable for solar PV
|
||||
roof_coverage_percent = round(roof_coverage * 100)
|
||||
# Given the wattage, we estimate the cost of the solar PV system. This is based on the MCS database
|
||||
# of solar PV installations
|
||||
cost_result = self.costs.solar_pv(wattage=solar_panel_wattage, has_battery=has_battery)
|
||||
kw = np.floor(solar_panel_wattage / 100) / 10
|
||||
|
||||
if has_battery:
|
||||
description = (f"Install a {kw} kilowatt-peak (kWp) solar photovoltaic (PV) panel system on "
|
||||
f"{round(roof_coverage_percent)}% the roof, with a battery storage system.")
|
||||
else:
|
||||
description = (f"Install a {kw} kilowatt-peak (kWp) solar photovoltaic (PV) p"
|
||||
f"anel system on {round(roof_coverage_percent)}% the roof.")
|
||||
|
||||
already_installed = "solar_pv" in self.property.already_installed
|
||||
if already_installed:
|
||||
cost_result = override_costs(cost_result)
|
||||
|
||||
self.recommendation.append(
|
||||
{
|
||||
"phase": phase,
|
||||
"parts": [],
|
||||
"type": "solar_pv",
|
||||
"description": description,
|
||||
"starting_u_value": None,
|
||||
"new_u_value": None,
|
||||
"sap_points": None,
|
||||
"already_installed": already_installed,
|
||||
**cost_result,
|
||||
# This is required for simulating the SAP impact. solar_pv_percentage is between 0 & 1 so we scale
|
||||
# back up here
|
||||
"photo_supply": 100 * roof_coverage,
|
||||
"has_battery": has_battery,
|
||||
"description_simulation": {"photo-supply": 100 * roof_coverage},
|
||||
}
|
||||
)
|
||||
self.recommendation.append(
|
||||
{
|
||||
"phase": phase,
|
||||
"parts": [],
|
||||
"type": "solar_pv",
|
||||
"description": description,
|
||||
"starting_u_value": None,
|
||||
"new_u_value": None,
|
||||
"sap_points": None,
|
||||
"already_installed": already_installed,
|
||||
**cost_result,
|
||||
# This is required for simulating the SAP impact. solar_pv_percentage is between 0 & 1 so we
|
||||
# scale
|
||||
# back up here
|
||||
"photo_supply": roof_coverage_percent,
|
||||
"has_battery": has_battery,
|
||||
"description_simulation": {"photo-supply": roof_coverage_percent},
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -53,14 +53,14 @@ class WindowsRecommendations:
|
|||
if not number_of_windows:
|
||||
raise ValueError("Number of windows not specified")
|
||||
|
||||
if windows_area is not None:
|
||||
raise Exception("We have windows area, we should use this data for our recommendations!!!")
|
||||
|
||||
if self.property.windows["has_glazing"] & (
|
||||
self.property.windows["glazing_coverage"] == "full"
|
||||
):
|
||||
return
|
||||
|
||||
if windows_area is not None:
|
||||
raise Exception("We have windows area, we should use this data for our recommendations!!!")
|
||||
|
||||
# We scale the number of windows based on the proportion of existing glazing
|
||||
if self.property.data["multi-glaze-proportion"] != "":
|
||||
n_windows_scalar = 1 - (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue