From ca4fcd8f497ab7d9eed9b76828aeeaa24c6046bd Mon Sep 17 00:00:00 2001 From: Lucas Tan Date: Tue, 1 Apr 2025 23:04:08 +0800 Subject: [PATCH] energy calculation including shading effects --- Utilities/Optimisers.py | 0 Utilities/Shading.py | 114 +++++++++++++++++++++++++++++++++++++--- config.yml | 5 +- main.py | 4 +- 4 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 Utilities/Optimisers.py diff --git a/Utilities/Optimisers.py b/Utilities/Optimisers.py new file mode 100644 index 0000000..e69de29 diff --git a/Utilities/Shading.py b/Utilities/Shading.py index 1e1db71..30ab8f9 100644 --- a/Utilities/Shading.py +++ b/Utilities/Shading.py @@ -12,13 +12,14 @@ from Utilities.Processes import ( logger = logging.getLogger(__name__) + def get_location(c): location = pvlib.location.Location( latitude=c["environment"]["location"]["latitude"], longitude=c["environment"]["location"]["longitude"], tz=c["simulation_date_time"]["tz"], ) - + return location @@ -85,9 +86,7 @@ def define_grid_layout(c): for i in range(int(max__panels_per_row)): if counter < no_of_panels: x.append(i * c["panel"]["dimensions"]["width"]) - y.append( - j * pitch - ) + y.append(j * pitch) z.append(0) counter += 1 else: @@ -121,10 +120,111 @@ def get_solar_data(c): # Get solar position data using PVLib solar_positions = location.get_solarposition(times) - clearsky_data = location.get_clearsky(times) + # filter solar positions to only include times when the sun is above the horizon + solar_positions = solar_positions[solar_positions["apparent_elevation"] > 0] + # get datetime range from solar_positions + day_times = solar_positions.index + clearsky_data = location.get_clearsky(day_times) return solar_positions, clearsky_data -def calculate_shading(c, coordinates, solar_positions): - \ No newline at end of file +def calculate_shading(c): + panel_coordinates, no_of_panels = define_grid_layout(c) + solar_positions, clearsky_data = get_solar_data(c) + + # split the solar positions data into morning and afternoon, using solar azimuth of + # 180 degrees as the threshold + morning_solar_positions = solar_positions[solar_positions["azimuth"] <= 180] + afternoon_solar_positions = solar_positions[solar_positions["azimuth"] > 180] + + # the first row is always not shaded so exclude + no_of_rows = np.unique(panel_coordinates["y"]).shape[0] + no_of_shaded_rows = no_of_rows - 1 + + collector_width = c["panel"]["dimensions"]["length"] + # calculate delta between unique y coordinates of panels to get pitch + pitch = np.unique(panel_coordinates["y"])[1] - np.unique(panel_coordinates["y"])[0] + surface_to_axis_offset = 0 + shaded_row_rotation = c["array"]["tilt"] + shading_row_rotation = c["array"]["tilt"] + axis_tilt = 0 + axis_azimuth = c["array"]["front_face_azimuth"] + + morning_shaded_fraction = pvlib.shading.shaded_fraction1d( + solar_zenith=morning_solar_positions["zenith"], + solar_azimuth=morning_solar_positions["azimuth"], + axis_azimuth=axis_azimuth, + shaded_row_rotation=shaded_row_rotation, + shading_row_rotation=shading_row_rotation, + collector_width=collector_width, + pitch=pitch, + surface_to_axis_offset=surface_to_axis_offset, + axis_tilt=axis_tilt, + ) + morning_shaded_fraction = morning_shaded_fraction * no_of_shaded_rows / no_of_rows + + afternoon_shaded_fraction = pvlib.shading.shaded_fraction1d( + solar_zenith=afternoon_solar_positions["zenith"], + solar_azimuth=afternoon_solar_positions["azimuth"], + axis_azimuth=axis_azimuth + 180, + shaded_row_rotation=shaded_row_rotation, + shading_row_rotation=shading_row_rotation, + collector_width=collector_width, + pitch=pitch, + surface_to_axis_offset=surface_to_axis_offset, + axis_tilt=axis_tilt, + ) + afternoon_shaded_fraction = ( + afternoon_shaded_fraction * no_of_shaded_rows / no_of_rows + ) + logger.info( + f"Shaded fraction calculated for morning and afternoon solar positions." + ) + + # calculate irradiance on plane of array + poa_front = pvlib.irradiance.get_total_irradiance( + surface_tilt=c["array"]["tilt"], + surface_azimuth=c["array"]["front_face_azimuth"], + solar_zenith=morning_solar_positions["zenith"], + solar_azimuth=morning_solar_positions["azimuth"], + dni=clearsky_data["dni"], + ghi=clearsky_data["ghi"], + dhi=clearsky_data["dhi"], + ) + # drop rows with poa_global NaN values + poa_front = poa_front.dropna(subset=["poa_global"]) + + poa_rear = pvlib.irradiance.get_total_irradiance( + surface_tilt=180 - c["array"]["tilt"], + surface_azimuth=c["array"]["front_face_azimuth"] + 180, + solar_zenith=afternoon_solar_positions["zenith"], + solar_azimuth=afternoon_solar_positions["azimuth"], + dni=clearsky_data["dni"], + ghi=clearsky_data["ghi"], + dhi=clearsky_data["dhi"], + ) + # drop rows with poa_global NaN values + poa_rear = poa_rear.dropna(subset=["poa_global"]) + + effective_front = ( + poa_front["poa_global"] + * (1 - morning_shaded_fraction) + * c["panel"]["efficiency"] + ) + effective_rear = ( + poa_rear["poa_global"] + * (1 - afternoon_shaded_fraction) + * c["panel"]["bifaciality"] + * c["panel"]["efficiency"] + ) + + energy_front = effective_front * 15 / 60 / 1e3 + energy_rear = effective_rear * 15 / 60 / 1e3 + energy_total = energy_front.sum() + energy_rear.sum() + logger.info(f"Energy yield calculated: {energy_total} kWh/m2") + + panel_area = c["panel"]["dimensions"]["length"] * c["panel"]["dimensions"]["width"] + total_area = panel_area * no_of_panels + total_energy = energy_total * total_area + logger.info(f"Total energy yield calculated: {total_energy} kWh") diff --git a/config.yml b/config.yml index 0725ee5..6acde60 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,6 @@ array: - system_size: 100 # in kWp - spacing: 1.5 # spacing between adjacent panel rows in m + system_size: 400 # in kWp + spacing: 0.5 # spacing between adjacent panel rows in m edge_setback: 1.8 # distance from the edge of the roof to the array front_face_azimuth: 90 # 90=east, 180=south, 270=west tilt: 90 # just 0 and 90 are supported for now @@ -24,6 +24,7 @@ environment: panel: peak_power: 710 # in Wp + efficiency: 0.229 # max efficiency of the front face bifaciality: 0.85 # rear face efficiency relative to front face # dimensions all in m dimensions: diff --git a/main.py b/main.py index eb695d1..83f5c5d 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ # %% import yaml import logging -from Utilities.Shading import define_grid_layout +from Utilities.Shading import calculate_shading logging.basicConfig( level=logging.INFO, @@ -24,7 +24,7 @@ with open(config_path, "r") as file: logger.info("Configuration loaded successfully.") logger.debug(f"Configuration: {c}") -shading = define_grid_layout(c) +shading = calculate_shading(c) logger.info("Shading calculation completed successfully.") # %%