diff --git a/Utilities/Shading.py b/Utilities/Shading.py index 30ab8f9..ce3480e 100644 --- a/Utilities/Shading.py +++ b/Utilities/Shading.py @@ -2,7 +2,6 @@ import numpy as np import pandas as pd import logging import math -from tqdm import tqdm import pvlib @@ -23,7 +22,7 @@ def get_location(c): return location -def define_grid_layout(c): +def define_grid_layout(c, panel_tilt): # get number of panels required no_of_panels = calculate_no_of_panels( c["array"]["system_size"], c["panel"]["peak_power"] @@ -33,7 +32,7 @@ def define_grid_layout(c): pitch = c["array"]["spacing"] + c["panel"]["dimensions"]["thickness"] # calculate minimum pitch if we don't want panel overlap at all min_pitch = c["panel"]["dimensions"]["length"] * math.cos( - c["array"]["tilt"] / 180 * math.pi + panel_tilt / 180 * math.pi ) if pitch < min_pitch: logger.warning( @@ -129,8 +128,8 @@ def get_solar_data(c): return solar_positions, clearsky_data -def calculate_shading(c): - panel_coordinates, no_of_panels = define_grid_layout(c) +def calculate_energy_production_vertical(c): + panel_coordinates, no_of_panels = define_grid_layout(c, panel_tilt=90) solar_positions, clearsky_data = get_solar_data(c) # split the solar positions data into morning and afternoon, using solar azimuth of @@ -146,10 +145,10 @@ def calculate_shading(c): # 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"] + shaded_row_rotation = 90 + shading_row_rotation = 90 axis_tilt = 0 - axis_azimuth = c["array"]["front_face_azimuth"] + axis_azimuth = 90 morning_shaded_fraction = pvlib.shading.shaded_fraction1d( solar_zenith=morning_solar_positions["zenith"], @@ -184,25 +183,27 @@ def calculate_shading(c): # 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"], + surface_tilt=90, + surface_azimuth=axis_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"], + albedo=0.5, ) # 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, + surface_tilt=180 - 90, + surface_azimuth=axis_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"], + albedo=0.5, ) # drop rows with poa_global NaN values poa_rear = poa_rear.dropna(subset=["poa_global"]) @@ -221,10 +222,74 @@ def calculate_shading(c): energy_front = effective_front * 15 / 60 / 1e3 energy_rear = effective_rear * 15 / 60 / 1e3 - energy_total = energy_front.sum() + energy_rear.sum() + total_hourly_energy_m2 = energy_front.add(energy_rear, fill_value=0) + energy_total = total_hourly_energy_m2.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 + total_hourly_energy = total_hourly_energy_m2 * total_area + total_energy = total_hourly_energy.sum() logger.info(f"Total energy yield calculated: {total_energy} kWh") + + return total_hourly_energy + + +def calculate_energy_production_horizontal(c): + panel_coordinates, no_of_panels = define_grid_layout(c, panel_tilt=0) + solar_positions, clearsky_data = get_solar_data(c) + + # 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 = 0 + shading_row_rotation = 0 + axis_tilt = 0 + axis_azimuth = 180 # south facing + + shaded_fraction = pvlib.shading.shaded_fraction1d( + solar_zenith=solar_positions["zenith"], + solar_azimuth=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, + ) + shaded_fraction = shaded_fraction * no_of_shaded_rows / no_of_rows + logger.info(f"Shaded fraction calculated for solar positions.") + + poa = pvlib.irradiance.get_total_irradiance( + surface_tilt=0, + surface_azimuth=axis_azimuth, + solar_zenith=solar_positions["zenith"], + solar_azimuth=solar_positions["azimuth"], + dni=clearsky_data["dni"], + ghi=clearsky_data["ghi"], + dhi=clearsky_data["dhi"], + surface_type="urban", + ) + poa = poa.dropna(subset=["poa_global"]) + + effective_front = ( + poa["poa_global"] * (1 - shaded_fraction) * c["panel"]["efficiency"] + ) + + total_hourly_energy_m2 = effective_front * 15 / 60 / 1e3 + energy_total = total_hourly_energy_m2.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_hourly_energy = total_hourly_energy_m2 * total_area + total_energy = total_hourly_energy.sum() + logger.info(f"Total energy yield calculated: {total_energy} kWh") + + return total_hourly_energy diff --git a/config.yml b/config.yml index 6acde60..bfc11ed 100644 --- a/config.yml +++ b/config.yml @@ -1,9 +1,8 @@ array: system_size: 400 # in kWp - spacing: 0.5 # spacing between adjacent panel rows in m + spacing: 1 # 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 + roof_slope: 0 slope: 0 # degrees from horizontal (+ve means shaded row is higher than the row in front) simulation_date_time: diff --git a/main.py b/main.py index 83f5c5d..e399ed0 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,12 @@ # %% import yaml import logging -from Utilities.Shading import calculate_shading +import numpy as np +import matplotlib.pyplot as pl +from Utilities.Shading import ( + calculate_energy_production_horizontal, + calculate_energy_production_vertical, +) logging.basicConfig( level=logging.INFO, @@ -24,7 +29,37 @@ with open(config_path, "r") as file: logger.info("Configuration loaded successfully.") logger.debug(f"Configuration: {c}") -shading = calculate_shading(c) -logger.info("Shading calculation completed successfully.") +# calculate energy production for horizontal and vertical panels +horizontal_energy = calculate_energy_production_horizontal(c) +logger.info("Energy production for horizontal panels calculated successfully.") +logger.debug(f"Horizontal Energy Production: {horizontal_energy.sum()}") -# %% +vertical_energy = calculate_energy_production_vertical(c) +logger.info("Energy production for vertical panels calculated successfully.") +logger.debug(f"Vertical Energy Production: {vertical_energy.sum()}") + +NOVA_scaledown = 0.75 + +horizontal_energy_scaled = horizontal_energy * NOVA_scaledown +logger.info("Energy production for horizontal panels scaled down to NOVA requirement.") + +logger.info( + f"Energy production for horizontal panels: {np.round(horizontal_energy_scaled.sum(),0)} kWh" +) +logger.info( + f"Energy production for vertical panels: {np.round(vertical_energy.sum(),0)} kWh" +) + +# overlay horizontal and vertical energy production +pl.figure(figsize=(10, 6)) +pl.plot( + horizontal_energy_scaled.index, + horizontal_energy_scaled.values, + label="Horizontal Panels", +) +pl.plot(vertical_energy.index, vertical_energy.values, label="Vertical Panels") +pl.title("Energy Production Comparison") +pl.xlabel("Time") +pl.ylabel("Energy Production (kWh)") +pl.legend() +pl.show()