import numpy as np import pandas as pd import logging import math import pvlib from Utilities.Processes import ( calculate_no_of_panels, ) 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 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"] ) # calculate pitch 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( panel_tilt / 180 * math.pi ) if pitch < min_pitch: logger.warning( f"Spacing is less than minimum pitch. Setting spacing to {min_pitch}." ) pitch = min_pitch logger.info(f"Pitch between panels: {pitch}m") # get maximum number of panels based on spacing and dimensions max__panels_per_row = np.floor( ( c["environment"]["roof"]["dimensions"]["width"] - (2 * c["array"]["edge_setback"] + c["panel"]["dimensions"]["width"]) ) / c["panel"]["dimensions"]["width"] ) max_number_of_rows = np.floor( ( c["environment"]["roof"]["dimensions"]["length"] - (2 * c["array"]["edge_setback"] + c["panel"]["dimensions"]["length"]) ) / pitch ) max_no_of_panels = max__panels_per_row * max_number_of_rows logger.info( f"Number of panels required: {no_of_panels}, Maximum panels possible: {max_no_of_panels}" ) if no_of_panels > max_no_of_panels: no_of_panels = max_no_of_panels logger.warning( f"Number of panels required exceeds maximum possible. Setting number of panels to {no_of_panels}." ) else: logger.info( f"Number of panels required is within the maximum possible. Setting number of panels to {no_of_panels}." ) # coordinate of panel determined by bottom left corner # x - row wise position, y - column wise position, z - height # first panel in row 1 is at (0, 0, 0) # nth panel in row 1 is at ((n-1)*panel_width, 0, 0) # first panel in nth row is at (0, (n-1)*(panel_thickness + spacing), 0) # create matrices for x, y, z coordinates of panels x = [] y = [] z = [] counter = 0 for j in range(int(max_number_of_rows)): 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) z.append(0) counter += 1 else: break coordinates = pd.DataFrame( { "x": x, "y": y, "z": z, } ) return coordinates, no_of_panels def get_solar_data(c): logger.info( f"Getting solar position data for {c['simulation_date_time']['start']} to {c['simulation_date_time']['end']}" ) """ Function to get solar position from PVLib """ location = get_location(c) times = pd.date_range( c["simulation_date_time"]["start"], c["simulation_date_time"]["end"], freq="15min", tz=location.tz, ) # Get solar position data using PVLib solar_positions = location.get_solarposition(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_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 # 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 = 90 shading_row_rotation = 90 axis_tilt = 0 axis_azimuth = 90 morning_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, ) morning_shaded_fraction = morning_shaded_fraction * no_of_shaded_rows / no_of_rows afternoon_shaded_fraction = pvlib.shading.shaded_fraction1d( solar_zenith=solar_positions["zenith"], solar_azimuth=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=90, 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", ) # 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 - 90, surface_azimuth=axis_azimuth + 180, 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", ) # 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 total_hourly_energy_m2 = energy_front + energy_rear 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 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