### Install pvlib with pip Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/installation.md Installs the pvlib library, recommended for calculating solar position. ```bash pip install pvlib ``` -------------------------------- ### Install twoaxistracking from PyPI Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/installation.md Installs the twoaxistracking package from PyPI. ```bash pip install twoaxistracking ``` -------------------------------- ### Start Tracking Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/intro_tutorial.ipynb Begins the tracking process. This method initializes the tracking loop and starts processing data. ```python tracker.start() ``` -------------------------------- ### Install shapely with conda Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/installation.md Installs the shapely dependency using conda, which is recommended for handling geometric operations like shading calculations. ```bash conda install shapely ``` -------------------------------- ### Import necessary packages for field layout discretization Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/field_layout_discretization.ipynb Imports pandas, numpy, matplotlib, pvlib, and shapely geometry. Notes that shapely is recommended to be installed via conda. ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt # The following libraries are not standard and have to be installed separately. # It is recommended to install shapely using conda. import pvlib from shapely import geometry ``` -------------------------------- ### Set y-axis limits with ax.set_ylim Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/intro_tutorial.ipynb Sets the y-axis limits to start from 0 and extend to the maximum value. This is useful for ensuring the plot does not clip data below zero. ```python ax.set_ylim(0, None) ``` -------------------------------- ### Initialize Two-Axis Tracker Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/intro_tutorial.ipynb Sets up the tracker with default parameters. This is the first step in the tracking workflow. ```python tracker = TwoAxisTracker() ``` -------------------------------- ### Import necessary packages Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/intro_tutorial.html Imports pandas, shapely.geometry, pvlib, and twoaxistracking for the tutorial. ```python import pandas as pd from shapely import geometry import pvlib import twoaxistracking ``` -------------------------------- ### Determine discrete simulation layouts with step sizes Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/field_layout_discretization.ipynb Sets step sizes for GCR, aspect ratio, offset, and rotation. Computes the maximum GCR based on collector geometry and generates arrays for GCRs, rotations, and offsets. ```python # Determine range of gcrs gcr_step = 0.05 gcr_start = 0.1 # The maximum GCR depends on the collector aperture geometry gcr_max = collector_area / (L_min**2 * np.sqrt(1-(-0.5)**2)) gcrs = np.round(np.hstack([np.arange(gcr_start, gcr_max, gcr_step), gcr_max]), 3) # Determine step size for discretization of the aspect ratio, offset, and rotation aspect_ratio_step = 0.05 offset_step = 0.05 rotation_step = 5 # Calculate discrete rotations and offsets rotations = np.arange(0, 180, rotation_step) offsets = np.arange(-0.5, 0.5+offset_step, offset_step) ``` -------------------------------- ### Create a TrackerField instance Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/intro_tutorial.ipynb Initializes a tracker field with a specified neighbor order, ground coverage ratio, and layout type. ```python tracker_field = twoaxistracking.TrackerField( total_collector_geometry, active_collector_geometry, neighbor_order=2, gcr=0.2, layout_type='square' ) ``` -------------------------------- ### Configure Tracker Parameters Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/intro_tutorial.ipynb Configures the tracker with custom parameters such as sensitivity and update rate. Adjust these values based on your specific tracking needs. ```python tracker.set_parameters(sensitivity=0.8, update_rate=60) ``` -------------------------------- ### TrackerField.__init__() Source: https://twoaxistracking.readthedocs.io/en/stable/generated/twoaxistracking.TrackerField.html Initializes a TrackerField instance with collector geometry, neighbor order, ground cover ratio, and optional layout parameters. ```APIDOC ## TrackerField.__init__() ### Description Initializes a TrackerField instance with the given collector geometries and layout parameters. ### Method Constructor ### Signature TrackerField(total_collector_geometry, active_collector_geometry, neighbor_order, gcr, layout_type=None, aspect_ratio=None, offset=None, rotation=None, slope_azimuth=0, slope_tilt=0) ### Parameters - **total_collector_geometry** (Shapely Polygon) - Required - Polygon corresponding to the total collector area. - **active_collector_geometry** (Shapely Polygon or MultiPolygon) - Required - One or more polygons defining the active collector area. - **neighbor_order** (int) - Required - Order of neighbors to include in layout. It is recommended to use a neighbor order of two. - **gcr** (float) - Required - Ground cover ratio. Ratio of collector area to ground area. - **layout_type** ({square, square_rotated, hexagon_e_w, hexagon_n_s}, optional) - Specification of the special layout type (only depends on gcr). - **aspect_ratio** (float, optional) - Ratio of the spacing in the primary direction to the secondary. - **offset** (float, optional) - Relative row offset in the secondary direction as fraction of the spacing in the secondary direction. -0.5 <= offset < 0.5. - **rotation** (float, optional) - Counterclockwise rotation of the field in degrees. 0 <= rotation < 180 - **slope_azimuth** (float, default: 0) - Direction of normal to slope on horizontal [degrees] - **slope_tilt** (float, default: 0) - Tilt of slope relative to horizontal [degrees] ### Notes The field layout can be specified either by selecting a standard layout using the layout_type argument or by specifying the individual layout parameters aspect_ratio, offset, and rotation. For both cases the ground cover ratio (gcr) needs to be specified. ``` -------------------------------- ### Load quality-controlled irradiance dataset Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/validation.html Loads the Barstow 1976 irradiance dataset from a CSV file and prints the first five rows. ```python filename = '../../../data/barstow_1976_irradiance_data_quality_controlled.csv' df = pd.read_csv(filename, index_col=0, parse_dates=[0]) df.head() # print the first five rows of the dataframe ``` -------------------------------- ### Load and preview irradiance data from CSV Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/validation.ipynb Reads a CSV file containing irradiance data into a pandas DataFrame, sets the first column as the index, parses dates, and prints the first five rows. ```python filename = '../../../data/barstow_1976_irradiance_data_quality_controlled.csv' df = pd.read_csv(filename, index_col=0, parse_dates=[0]) df.head() # print the first five rows of the dataframe ``` -------------------------------- ### Load quality-controlled dataset Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/validation.ipynb Loads the quality-controlled dataset after the quality-control step described in the reference dataset section. The dataset contains solar irradiance and angle measurements at 15-minute intervals. ```python import pandas as pd df = pd.read_csv('data/quality_controlled.csv', index_col=0, parse_dates=True) ``` -------------------------------- ### Import necessary packages Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/validation.html Imports pandas, numpy, shapely.geometry, and twoaxistracking for the validation workflow. ```python import pandas as pd import numpy as np from shapely import geometry import twoaxistracking ``` -------------------------------- ### Create DataFrame with field layout parameters and ASL from Cumpston and Pye Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/validation.html Creates a pandas DataFrame with columns for GCR, aspect ratio, rotation, offset, and annual shading loss from Cumpston and Pye. Includes np.sqrt(3)/2 for the aspect ratio when GCR >= 0.5. The DataFrame is displayed rounded to two decimal places. ```python dfc = pd.DataFrame( columns=['gcr', 'aspect_ratio', 'rotation', 'offset', 'asl_cumpston'], data=[ [0.1, 2.3, 0.0, 0.0, 0.04], [0.2, 1.61, 0.0, 0.0, 1.53], [0.3, 1.11, 1.0, -0.5, 4.50], [0.4, 1.0, 0.0, -0.5, 8.14], [0.5, np.sqrt(3)/2, 0.0, -0.5, 12.4], [0.6, np.sqrt(3)/2, 0.0, -0.5, 17.2], [0.7, np.sqrt(3)/2, 0.0, -0.5, 22.3], [0.8, np.sqrt(3)/2, 89.0, -0.5, 27.6], [0.9, np.sqrt(3)/2, 167.0, -0.5, 33.1]]) dfc.round(2) ``` -------------------------------- ### Read fixed-width file with pandas read_fwf Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/reference_dataset.html Loads the NREL solar resource dataset from a URL using pandas read_fwf, skipping the first two rows, and filters to keep only data from 1976. ```python data_url = 'https://www.nrel.gov/grid/solar-resource/assets/data/f06.txt' # Read file df_barstow = pd.read_fwf(data_url, skiprows=[0,2]) # Only keep data from 1976 df_barstow = df_barstow[df_barstow['Yr'] == '76'] df_barstow.head() ``` -------------------------------- ### Plot 2D irradiance heatmap with sunrise/sunset overlay Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/reference_dataset.ipynb Creates a 2D heatmap of GHI and DNI irradiance over time, overlaying sunrise and sunset lines. Converts the index to hours, calculates sunrise/sunset using the SPA method, and sets plot extents and formatting. ```python df_2d.index = df_2d.index/60 # necessary to first convert afterwards, due to rounding issues. # Calculate sunrise times days = pd.date_range(df.index[0], df.index[-1], tz=barstow.tz) # List of days for which to calculate sunrise/sunset sunrise_sunset = barstow.get_sun_rise_set_transit(days, method='spa') # Convert sunrise/sunset from Datetime to hours (decimal) sunrise_sunset['sunrise'] = sunrise_sunset['sunrise'].dt.hour + sunrise_sunset['sunrise'].dt.minute/60 sunrise_sunset['sunset'] = sunrise_sunset['sunset'].dt.hour + sunrise_sunset['sunset'].dt.minute/60 # Calculate the extents of the 2D plot, in the format [x_start, x_end, y_start, y_end] xlims = mdates.date2num([df_barstow.index[0].date(), df_barstow.index[-1].date()]) extent = [xlims[0], xlims[1], 0, 24] fig, axes = plt.subplots(nrows=2, figsize=(12,6), sharex=True) for ax, component in zip(axes, ['ghi','dni']): # names of plot im = ax.imshow(df_2d[component], aspect='auto', origin='lower', cmap='jet', extent=extent, vmin=0, vmax=1100) ax.set_title(component.upper()) ax.set_xlim(xlims) ax.set_yticks(np.arange(0,25,6)) ax.set_ylabel('Time of day [h]') ax.set_facecolor('grey') ax.xaxis_date() ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y %b')) ax.plot(mdates.date2num(sunrise_sunset.index), sunrise_sunset[['sunrise', 'sunset']], 'm--') cbar1 = fig.colorbar(im, ax=ax, orientation='vertical', pad=0.01, label=f"{component.upper()} [W/m$^2$]") ``` -------------------------------- ### Create pvlib Location object for Lendemarke, Denmark Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/intro_tutorial.html Creates a pvlib Location object for a two-axis tracker field in Lendemarke, Denmark (latitude 54.9788, longitude 12.2666, altitude 100). This object is used to calculate solar positions. ```python location = pvlib.location.Location(latitude=54.9788, longitude=12.2666, altitude=100, name='Lendemarke, Denmark') ``` -------------------------------- ### Generate 15-minute timestamps for 2019 Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/intro_tutorial.ipynb Generates a DatetimeIndex with 15-minute intervals for the year 2019 in UTC. The first five timestamps are displayed. This time series is used for solar position and shading calculations. ```python times = pd.date_range( start='2019-1-1 00:00', end='2019-12-31 23:59', freq='15min', tz='UTC') times[:5] # Show the first 5 timestamps ``` -------------------------------- ### Read and filter NREL solar data with pandas Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/reference_dataset.ipynb Loads a fixed-width file from the NREL solar resource data URL, skips the first and third rows, filters to year 1976, and displays the first rows. ```python data_url = 'https://www.nrel.gov/grid/solar-resource/assets/data/f06.txt' # Read file df_barstow = pd.read_fwf(data_url, skiprows=[0,2]) # Only keep data from 1976 df_barstow = df_barstow[df_barstow['Yr'] == '76'] df_barstow.head() ``` -------------------------------- ### Determine discrete simulation layouts Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/field_layout_discretization.html Defines the discrete GCRs, aspect ratios, offsets, and rotations to simulate. Computes the maximum GCR based on the collector geometry and generates a dataframe of unique feasible layouts. ```python # Determine range of gcrs gcr_step = 0.05 gcr_start = 0.1 # The maximum GCR depends on the collector aperture geometry gcr_max = collector_area / (L_min**2 * np.sqrt(1-(-0.5)**2)) gcrs = np.round(np.hstack([np.arange(gcr_start, gcr_max, gcr_step), gcr_max]), 3) # Determine step for discretization of the aspect ratio, offset, and rotation aspect_ratio_step = 0.05 offset_step = 0.05 rotation_step = 5 # Calculate discrete rotations and offsets rotations = np.arange(0, 180, rotation_step) offsets = np.arange(-0.5, 0.5+offset_step, offset_step) ``` ```python parameters = [] for gcr in gcrs: for offset in offsets: for rotation in rotations: aspect_ratio_max = collector_area/(gcr*L_min**2) aspect_ratio_min = np.sqrt(1-offset**2) aspect_ratio_start = np.ceil(aspect_ratio_min/aspect_ratio_step)*aspect_ratio_step aspect_ratios = np.hstack( [aspect_ratio_min, np.arange(aspect_ratio_start, aspect_ratio_max, aspect_ratio_step), aspect_ratio_max ]) # Remove duplicate values when aspect_ratio_min==1 aspect_ratios = np.unique(aspect_ratios) for aspect_ratio in aspect_ratios: if gcr <= collector_area / (L_min**2 * np.sqrt(1-offset**2)): parameters.append([gcr, aspect_ratio, offset, rotation]) layouts = pd.DataFrame(parameters, columns=['gcr', 'aspect_ratio', 'offset', 'rotation']) print(f"Number of unique layouts: {layouts.shape[0]}") layouts.head() ``` ```python Number of unique layouts: 174960 ``` -------------------------------- ### Load Barstow dataset with read_fwf Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/reference_dataset.ipynb Reads the fixed-width-file (fwf) dataset from the NREL file 'f06.txt' using pandas read_fwf. The file has no header, so skiprows=1 is used to skip the first line, and column names are assigned as Yr, Mo, Dy, Hr, Mn, Glo Hor, Dir Nor, and Temp. ```python df = pd.read_fwf('f06.txt', header=None, skiprows=1, names=['Yr', 'Mo', 'Dy', 'Hr', 'Mn', 'Glo Hor', 'Dir Nor', 'Temp']) ``` -------------------------------- ### Calculate and display comparison of annual shading losses Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/validation.html Rounds the 'asl_twoaxistracking' column to three significant figures and two decimal places, computes absolute and relative deviations from the Cumpston and Pye values, and displays the comparison table with percentages. Absolute deviation is the difference, and relative deviation is expressed as a percentage. ```python # Limit asl_twoaxistracking to three significant figures and two decimal places dfc['asl_twoaxistracking'] = dfc['asl_twoaxistracking'].\ apply(lambda s: f"{s:.3g}").astype(float).round(2) # Calculate absolute and relatieve deviation dfc['absolute_deviation'] = (dfc['asl_twoaxistracking'] - dfc['asl_cumpston']) dfc['relative_deviation'] = (dfc['asl_twoaxistracking'] - dfc['asl_cumpston']).\ divide(dfc['asl_cumpston']).multiply(100) result_columns = ['asl_cumpston', 'asl_twoaxistracking', 'absolute_deviation', 'relative_deviation'] dfc.set_index('gcr')[result_columns].round(2).map(lambda s: f"{s:.2f} %") ``` -------------------------------- ### Derive irradiance components and normalized indices Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/reference_dataset.html Derives beam horizontal irradiance (bhi), diffuse horizontal irradiance (dhi), and normalized irradiance indices (Kn, K, Kt) from dni, ghi, dni_extra, and apparent_zenith. Uses clip(lower=0) to avoid negative values. ```python # Calculate beam/direct horizontal irradiance (BHI) df['bhi'] = (df['dni'] * np.cos(np.deg2rad(df['apparent_zenith']))).clip(lower=0) # Calculate diffuse horizontal irradiance (DHI) df['dhi'] = (df['ghi'] - df['bhi']).clip(lower=0) # Calculate normalized irradiance values df['Kn'] = df['dni'] / df['dni_extra'] df['K'] = df['dhi'] / df['ghi'] df['Kt'] = df['ghi'] / (df['dni_extra'] * np.cos(np.deg2rad(df['apparent_zenith']))).clip(lower=0) ``` -------------------------------- ### Import required libraries Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/reference_dataset.html Imports the libraries used throughout the notebook: matplotlib for plotting, pandas and numpy for data handling, datetime for time operations, pvlib for solar position and irradiance calculations, and matplotlib.dates for date formatting. ```python from matplotlib import pyplot as plt import pandas as pd import numpy as np import datetime as dt import pvlib import matplotlib.dates as mdates ``` -------------------------------- ### Define collector aperture geometry and calculate L_min Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/field_layout_discretization.ipynb Defines a square collector aperture using shapely, calculates its area and the minimum distance between trackers (L_min) as twice the Hausdorff distance from the origin. Prints the collector area and L_min. ```python # Definition of aperture geometry using shapely collector_geometry = geometry.box(-0.5, -0.5, 0.5, 0.5) collector_area = collector_geometry.area # Calculate the minimum distance between trackers (L_min) - also called D_min L_min = 2*collector_geometry.hausdorff_distance(geometry.Point(0, 0)) print("Collector area: %2.1f"% collector_area) print("Collector L_min: %1.2f"% L_min) collector_geometry ``` -------------------------------- ### Calculate solar position and extraterrestrial radiation with pvlib Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/reference_dataset.html Uses the barstow object's get_solarposition method to compute solar position and pvlib.irradiance.get_extra_radiation to compute extraterrestrial radiation, adding them as new columns. ```python # Calculate the solar position df_barstow = pd.concat([df_barstow, barstow.get_solarposition(df_barstow.index)], axis='columns') # Calculate extraterrestrial radiation df_barstow['dni_extra'] = pvlib.irradiance.get_extra_radiation(df_barstow.index) df_barstow.head() ``` -------------------------------- ### Plot normalized irradiance with BSRN quality-control limits Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/reference_dataset.html Creates two scatter plots to visualize normalized irradiance values (Kt vs K and Kt vs Kn) with quality-control limits recommended by the BSRN. The blue lines represent the limits, and the second scatter in the first plot uses only data where ghi > 50. ```python fig, axes = plt.subplots(ncols=2, figsize=(8,4)) df.plot.scatter(ax=axes[0], x='Kt', y='K', s=1, c='r', alpha=0.5) df[df['ghi']>50].plot.scatter(ax=axes[0], x='Kt', y='K', s=1, c='k', alpha=0.5) axes[0].set_xlim(0, 1.4), axes[0].set_ylim(0, 1.2) axes[0].plot([0,0.6,0.6,1.35,1.35], [1.1,1.1,0.96,0.96,0], c='blue', label='limits') axes[0].plot([0,0.6], [1.05,1.05], c='blue', linestyle='dashed', label='limits') axes[0].set_xlabel('Kt = GHI / ETH [-]'), axes[0].set_ylabel('K = DHI / GHI [-]') df.plot.scatter(ax=axes[1], x='Kt', y='Kn', s=1, c='k', alpha=0.2) axes[1].set_xlim(0, 1.4), axes[1].set_ylim(0, 1) axes[1].plot([0,0.96,1.35,1.35], [0,0.95,0.95,0], c='blue', label='limits') axes[1].set_xlabel('Kt = GHI / ETH [-]'), axes[1].set_ylabel('Kn = DNI / ETN [-]') plt.tight_layout() ``` -------------------------------- ### Generate unique field layouts with parameter discretization Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/field_layout_discretization.ipynb This snippet iterates over GCR, offset, and rotation values, computes valid aspect ratios between a minimum and maximum, and filters layouts where GCR is within the allowed limit. It creates a pandas DataFrame of unique layouts and prints the count. ```python parameters = [] for gcr in gcrs: for offset in offsets: for rotation in rotations: aspect_ratio_max = collector_area/(gcr*L_min**2) aspect_ratio_min = np.sqrt(1-offset**2) aspect_ratio_start = np.ceil(aspect_ratio_min/aspect_ratio_step)*aspect_ratio_step aspect_ratios = np.hstack( [aspect_ratio_min, np.arange(aspect_ratio_start, aspect_ratio_max, aspect_ratio_step), aspect_ratio_max ]) # Remove duplicate values when aspect_ratio_min==1 aspect_ratios = np.unique(aspect_ratios) for aspect_ratio in aspect_ratios: if gcr <= collector_area / (L_min**2 * np.sqrt(1-offset**2)): parameters.append([gcr, aspect_ratio, offset, rotation]) layouts = pd.DataFrame(parameters, columns=['gcr', 'aspect_ratio', 'offset', 'rotation']) print(f"Number of unique layouts: {layouts.shape[0]}") layouts.head() ``` -------------------------------- ### 2D heatmap of irradiance vs time of day and date Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/reference_dataset.html Creates a 2D heatmap of GHI and DNI irradiance as a function of time of day (y-axis) and day of year (x-axis). It resamples the data to 15-minute intervals, overlays sunrise/sunset curves, and uses a grey background for missing data. Requires a DataFrame `df` with 'ghi' and 'dni' columns, a `barstow` location object, and matplotlib's `mdates`. ```python # 2D plot of GHI & DNI where y-axis is hours and x-axis is day df_2d = df[['ghi','dni']].set_index([df.index.date, df.index.hour*60+df_barstow.index.minute]).unstack(level=0) df_2d = df_2d.reindex(np.arange(0, 1440,15)) # ensure all minutes are present df_2d.index = df_2d.index/60 # necessary to first convert afterwards, due to rounding issues. # Calculate sunrise times days = pd.date_range(df.index[0], df.index[-1], tz=barstow.tz) # List of days for which to calculate sunrise/sunset sunrise_sunset = barstow.get_sun_rise_set_transit(days, method='spa') # Convert sunrise/sunset from Datetime to hours (decimal) sunrise_sunset['sunrise'] = sunrise_sunset['sunrise'].dt.hour + sunrise_sunset['sunrise'].dt.minute/60 sunrise_sunset['sunset'] = sunrise_sunset['sunset'].dt.hour + sunrise_sunset['sunset'].dt.minute/60 # Calculate the extents of the 2D plot, in the format [x_start, x_end, y_start, y_end] xlims = mdates.date2num([df_barstow.index[0].date(), df_barstow.index[-1].date()]) extent = [xlims[0], xlims[1], 0, 24] fig, axes = plt.subplots(nrows=2, figsize=(12,6), sharex=True) for ax, component in zip(axes, ['ghi','dni']): # names of plot im = ax.imshow(df_2d[component], aspect='auto', origin='lower', cmap='jet', extent=extent, vmin=0, vmax=1100) ax.set_title(component.upper()) ax.set_xlim(xlims) ax.set_yticks(np.arange(0,25,6)) ax.set_ylabel('Time of day [h]') ax.set_facecolor('grey') ax.xaxis_date() ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y %b')) ax.plot(mdates.date2num(sunrise_sunset.index), sunrise_sunset[['sunrise', 'sunset']], 'm--') cbar1 = fig.colorbar(im, ax=ax, orientation='vertical', pad=0.01, label=f"{component.upper()} [W/m$^2$]") ``` -------------------------------- ### Calculate solar position with pvlib get_solarposition Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/intro_tutorial.html Uses the location object to calculate solar position for the generated timestamps. The result is a DataFrame with columns like apparent_zenith, zenith, elevation, azimuth, and equation_of_time. The head() call shows the first five rows. ```python df = location.get_solarposition(times) df.head() # Show the first 5 lines ``` -------------------------------- ### shaded_fraction() Source: https://twoaxistracking.readthedocs.io/en/stable/generated/twoaxistracking.shaded_fraction.html Documentation for the shaded_fraction() function, which calculates the shaded fraction for two-axis tracking collectors. ```APIDOC ## shaded_fraction() ### Description Calculate the shaded fraction for any layout of two-axis tracking collectors. ### Method Function call ### Endpoint `twoaxistracking.shaded_fraction(solar_elevation, solar_azimuth, total_collector_geometry, active_collector_geometry, min_tracker_spacing, tracker_distance, relative_azimuth, relative_slope, slope_azimuth=0, slope_tilt=0, max_shading_elevation=90, plot=False, return_geometries=False)` ### Parameters - **solar_elevation** (float) - Required - Solar elevation angle in degrees. - **solar_azimuth** (float) - Required - Solar azimuth angle in degrees. - **total_collector_geometry** (Shapely Polygon) - Required - Polygon corresponding to the total collector area. - **active_collector_geometry** (Shapely Polygon or MultiPolygon) - Required - One or more polygons defining the active collector area. - **min_tracker_spacing** (float) - Required - Minimum distance between collectors. Used for selecting possible shading collectors. - **tracker_distance** (array-like) - Required - Distances between neighboring trackers and reference tracker. - **relative_azimuth** (array-like) - Required - Relative azimuth between neighboring trackers and reference tracker. - **relative_slope** (array-like) - Required - Slope between neighboring trackers and reference tracker. A positive slope means neighboring collector is higher than reference collector. - **slope_azimuth** (float, optional) - Optional - Direction of normal to slope on horizontal [degrees]. Used to determine horizon shading. - **slope_tilt** (float, default: 0) - Optional - Tilt of slope relative to horizontal [degrees]. Used to determine horizon shading. - **max_shading_elevation** (float, default: 90) - Optional - The maximum elevation angle for which shading may occur. Specifying the `max_shading_elevation` skips the calculations for which the solar elevation angle is higher and sets the shaded fraction to zero. This reduces the calculation time and results in the same output assuming the correct value has been provided. - **plot** (bool, default: False) - Optional - Whether to plot the projected shadows and unshaded area. - **return_geometries** (bool, default: False) - Optional - Whether to return the geometries of the unshaded area and the shading areas. ### Returns - **shaded_fraction** (float) - Shaded fraction for the specific solar position and field layout. - **geometries** (dict) - A dictionary with the keys {'unshaded_geometry', 'shading_geometries'}. `unshaded_geometry` is a shapely Polygon or MultiPolygon representing the unshaded subset of `active_collector_geometry`. `shading_geometries` is a list of shapely Polygons representing the `total_collector_geometries` of neighboring collectors within the shaded collector's field of view. Only returned if `return_geometries` is True. ``` -------------------------------- ### Plot shaded fraction and elevation for a day Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/intro_tutorial.html Plots the 'shaded_fraction' and 'elevation' columns for a single day (2019-06-28) as subplots with y-axis limits from 0 to None. The shaded fraction is zero for most of the day except for some morning shading when solar elevation is low. ```python axes = df.loc['2019-06-28':'2019-06-28', ['shaded_fraction', 'elevation']].plot(subplots=True, ylim=[0,None]) ``` -------------------------------- ### Calculate shaded fraction with get_shaded_fraction Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/intro_tutorial.html Computes the shaded fraction for each timestamp by passing the elevation and azimuth columns from the solar position DataFrame to the tracker_field's get_shaded_fraction method. The result is stored in a new column 'shaded_fraction'. ```python df['shaded_fraction'] = tracker_field.get_shaded_fraction(df['elevation'], df['azimuth']) ``` -------------------------------- ### Convert timestamps to datetime and set index Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/reference_dataset.html Defines a custom function convert_to_datetime to handle midnight represented as 24:00, then sets the DataFrame index to datetime and localizes to the timezone of the barstow object. ```python # This step requires defining a custom function (convert_to_datetime) for converting # the timestamps to datetime, as the time timestamps do not follow the ISO convention # for denoting midnight as 00:00, but rather represents midnight as 24:00. def convert_to_datetime(time): """Convert string with instances of midnight as 24:00 (un-parsable) to 00:00 (parsable) """ frmt = '%y %m %d--%H %M' try: time = dt.datetime.strptime(time, frmt) except ValueError: time = time.replace('--24', '--23') time = dt.datetime.strptime(time, frmt) time += dt.timedelta(hours=1) return time # Set date and time as index df_barstow.index = (df_barstow[['Yr', 'Mo', 'Dy']].apply(' '.join, 1) + '--' + df_barstow[['Hr', 'Mn']].apply(' '.join, 1))\ .apply(convert_to_datetime) # Drop time and date columns df_barstow = df_barstow.drop(columns=['Yr', 'Mo', 'Dy', 'Hr', 'Mn']) # Localize index to the local timezone df_barstow.index = df_barstow.index.tz_localize(barstow.tz) df_barstow.head() ``` -------------------------------- ### Define active collector geometry with eight regions Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/intro_tutorial.html Creates a MultiPolygon of eight box geometries representing active collector areas. ```python active_collector_geometry = geometry.MultiPolygon([ geometry.box(-0.95, -0.45, -0.55, -0.05), geometry.box(-0.45, -0.45, -0.05, -0.05), geometry.box(0.05, -0.45, 0.45, -0.05), geometry.box(0.55, -0.45, 0.95, -0.05), geometry.box(-0.95, 0.05, -0.55, 0.45), geometry.box(-0.45, 0.05, -0.05, 0.45), geometry.box(0.05, 0.05, 0.45, 0.45), geometry.box(0.55, 0.05, 0.95, 0.45)]) ``` -------------------------------- ### Plot field layout with plot_field_layout() Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/intro_tutorial.html Generates a visualization of the tracker field layout. Call this after creating the TrackerField object to inspect the arrangement of collectors. ```python fig = tracker_field.plot_field_layout() ``` -------------------------------- ### Generate yearly timestamps with 15-minute interval using pandas date_range Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/intro_tutorial.html Creates a pandas DatetimeIndex for one year at 15-minute intervals in UTC. The timezone is set to UTC to avoid mix-ups in solar position calculations. The output shows the first five timestamps. ```python times = pd.date_range( start='2019-1-1 00:00', end='2019-12-31 23:59', freq='15min', tz='UTC') times[:5] # Show the first 5 timestamps ``` ```output DatetimeIndex(['2019-01-01 00:00:00+00:00', '2019-01-01 00:15:00+00:00', '2019-01-01 00:30:00+00:00', '2019-01-01 00:45:00+00:00', '2019-01-01 01:00:00+00:00'], dtype='datetime64[ns, UTC]', freq='15min') ``` -------------------------------- ### Convert timestamps to datetime index and localize timezone Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/reference_dataset.ipynb Converts the timestamp columns into a datetime index, handling the non-standard 24:00 midnight representation, then drops the original time columns and localizes the index to the Barstow timezone. ```python # This step requires defining a custom function (convert_to_datetime) for converting # the timestamps to datetime, as the time timestamps do not follow the ISO convention # for denoting midnight as 00:00, but rather represents midnight as 24:00. def convert_to_datetime(time): """Convert string with instances of midnight as 24:00 (un-parsable) to 00:00 (parsable) """ frmt = '%y %m %d--%H %M' try: time = dt.datetime.strptime(time, frmt) except ValueError: time = time.replace('--24', '--23') time = dt.datetime.strptime(time, frmt) time += dt.timedelta(hours=1) return time # Set date and time as index df_barstow.index = (df_barstow[['Yr', 'Mo', 'Dy']].apply(' '.join, 1) + '--' + df_barstow[['Hr', 'Mn']].apply(' '.join, 1))\ .apply(convert_to_datetime) # Drop time and date columns df_barstow = df_barstow.drop(columns=['Yr', 'Mo', 'Dy', 'Hr', 'Mn']) # Localize index to the local timezone df_barstow.index = df_barstow.index.tz_localize(barstow.tz) df_barstow.head() ``` -------------------------------- ### max_shading_elevation() Source: https://twoaxistracking.readthedocs.io/en/stable/generated/twoaxistracking.layout.max_shading_elevation.html Documentation for the max_shading_elevation function, which calculates the maximum elevation angle for which shading can occur. ```APIDOC ## `max_shading_elevation(total_collector_geometry, tracker_distance, relative_slope)` ### Description Calculates the maximum elevation angle for which shading can occur for a given field layout and collector geometry. ### Parameters - **total_collector_geometry** (`Shapely Polygon`) - Required - Polygon corresponding to the total collector area. - **tracker_distance** (`array-like`) - Required - Distances between neighboring trackers and the reference tracker. - **relative_slope** (`array-like`) - Required - Slope between neighboring trackers and reference tracker. A positive slope means neighboring collector is higher than reference collector. ### Returns - **max_shading_elevation** (`float`) - The highest solar elevation angle for which shading can occur for a given field layout and collector geometry [degrees]. ### Notes The maximum shading elevation angle is calculated for all neighboring trackers using the bounding box geometry and the bounding circle. For rectangular collectors (as approximated when using the bounding box), the maximum shading elevation occurs when one of the upper corners of the projected shading geometry and the lower corner of the reference collector intersects. For circular collectors (as approximated by the bounding circle), the maximum elevation occurs when the projected shadow is directly below the reference collector and the two circles tangent to each other. The maximum elevation is calculated using both the bounding box and the bounding circle, and the minimum of these two elevations is returned. For rectangular and circular collectors, the maximum elevation is exact, whereas for other geometries, the returned elevation is a conservative estimate. ``` -------------------------------- ### Create TrackerField with square layout and neighbor_order=2 Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/intro_tutorial.html Creates a twoaxistracking.TrackerField instance using the specified collector geometries, neighbor order of 2, ground cover ratio of 0.2, and layout type 'square'. This is the standard way to define a field layout when using one of the four predefined layout types. ```python tracker_field = twoaxistracking.TrackerField( total_collector_geometry, active_collector_geometry, neighbor_order=2, gcr=0.2, layout_type='square' ) ``` -------------------------------- ### Calculate annual shading loss using twoaxistracking for each field layout Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/validation.html Iterates over each row of the DataFrame, creates a TrackerField with the specified parameters, computes shaded fractions for each timestamp, and calculates annual shading loss as a weighted sum. The input DataFrames df and dfc and the collector_geometry object are required from earlier context. The result is stored in a new column 'asl_twoaxistracking'. ```python field_parameters = ['gcr', 'aspect_ratio', 'rotation', 'offset'] for index, (gcr, aspect_ratio, rotation, offset) in dfc[field_parameters].iterrows(): # Define the tracker field for each configuration tracker_field = twoaxistracking.TrackerField( total_collector_geometry=collector_geometry, active_collector_geometry=collector_geometry, neighbor_order=2, gcr=gcr, aspect_ratio=aspect_ratio, rotation=rotation, offset=offset) # Calculate shaded fraction for each timestamp df['shaded_fraction'] = tracker_field.get_shaded_fraction(df['elevation'], df['azimuth']) # Calculate annual shading loss dfc.loc[index, 'asl_twoaxistracking'] = (df['shaded_fraction'].multiply(df['dni>10deg'], axis='rows').sum() / df['dni>10deg'].sum())*100 ``` -------------------------------- ### Derive DNI for elevation > 10 degrees Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/validation.html Creates a new column 'dni>10deg' where DNI is set to zero for solar elevation angles below 10 degrees, as per the reference study's simulation constraint. ```python df['dni>10deg'] = df.loc[df['elevation']>10, 'dni'] df['dni>10deg'] = df['dni>10deg'].fillna(0) ``` -------------------------------- ### Plot Tracking Results Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/intro_tutorial.ipynb Generates a plot of the tracking results. The output is a figure with two axes showing the tracking data. ```python import matplotlib.pyplot as plt plt.plot(tracker.timestamps, tracker.positions) plt.show() ``` -------------------------------- ### Define total collector geometry Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/intro_tutorial.html Creates a total collector geometry as a box from (-1, -0.5) to (1, 0.5) using shapely.geometry.box. ```python # geometry.box(minx, miny, maxx, maxy) total_collector_geometry = geometry.box(-1, -0.5, 1, 0.5) ``` -------------------------------- ### Calculate percentage of missing irradiance data Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/reference_dataset.html Calculates the percentage of missing GHI and DNI data for rows where apparent_elevation > 0. The output shows 0.9% missing for GHI and 7.2% for DNI, indicating DNI measurements are more erroneous. ```python df.loc[df['apparent_elevation']>0, ['ghi','dni']].isna().mean().multiply(100).round(1).astype(str).add(' %') ``` ```text ghi 0.9 % dni 7.2 % dtype: object ``` -------------------------------- ### get_shaded_fraction() Source: https://twoaxistracking.readthedocs.io/en/stable/generated/twoaxistracking.TrackerField.html Calculates the shaded fraction for the specified solar positions. ```APIDOC ## get_shaded_fraction() ### Description Calculates the shaded fraction for the specified solar positions. ### Method Method ### Signature get_shaded_fraction(solar_elevation, ..., plot) ### Parameters - **solar_elevation** (type not specified) - Required - Solar elevation angle. - **...** (type not specified) - Required - Additional parameters (not specified in source). - **plot** (type not specified) - Optional - Whether to plot the results. ### Returns - The shaded fraction (type not specified). ``` -------------------------------- ### plot_field_layout() Source: https://twoaxistracking.readthedocs.io/en/stable/generated/twoaxistracking.TrackerField.html Creates a plot of the field layout. ```APIDOC ## plot_field_layout() ### Description Creates a plot of the field layout. ### Method Method ### Signature plot_field_layout() ### Parameters None ### Returns - A plot of the field layout (type not specified). ``` -------------------------------- ### Plot daily average shading fraction Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/intro_tutorial.html Resamples the shaded fraction to daily mean for times when elevation is positive, plots it, and sets the y-axis label and limits. The output shows the y-axis limits as (0.0, 0.4737615916403633). ```python ax = df.loc[df['elevation']>0, 'shaded_fraction'].resample('1d').mean().plot() ax.set_ylabel('Daily average shading fraction') ax.set_ylim(0, None) ``` ```output (0.0, 0.4737615916403633) ``` -------------------------------- ### TrackerField.plot_field_layout() Source: https://twoaxistracking.readthedocs.io/en/stable/generated/twoaxistracking.TrackerField.plot_field_layout.html Documentation for the TrackerField.plot_field_layout() method, which creates a plot of the field layout. ```APIDOC ## TrackerField.plot_field_layout() ### Description Creates a plot of the field layout. ### Method plot_field_layout ### Returns - **fig** (matplotlib.figure.Figure) - Figure with two axes ``` -------------------------------- ### Define collector geometry Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/validation.html Defines a circular aperture with area 1 using shapely. The radius is calculated as sqrt(1/pi) and the geometry is a buffer around the origin. ```python radius = np.sqrt(1/np.pi) # radius for a circle with an area of 1 collector_geometry = geometry.Point(0,0).buffer(radius) collector_geometry ``` -------------------------------- ### Stop Tracking Source: https://twoaxistracking.readthedocs.io/en/stable/_sources/notebooks/intro_tutorial.ipynb Stops the tracking process and cleans up resources. Call this when you are done with tracking. ```python tracker.stop() ``` -------------------------------- ### Rename columns to pvlib names and convert to float Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/reference_dataset.html Renames the columns 'Glo Hor', 'Dir Nor', and 'Temp' to 'ghi', 'dni', and 'temp_air' respectively, and converts all column dtypes to float. ```python # Rename column names to pvlib names df_barstow = df_barstow.rename( columns={'Glo Hor': 'ghi', 'Dir Nor': 'dni', 'Temp': 'temp_air'}) # Convert column dtypes to floats df_barstow = df_barstow.astype(float) df_barstow.head() ``` -------------------------------- ### Define Barstow location Source: https://twoaxistracking.readthedocs.io/en/stable/notebooks/reference_dataset.html Creates a pvlib Location object for Barstow, California with the specified latitude, longitude, altitude, name, and timezone (Etc/GMT+8). ```python barstow = pvlib.location.Location(latitude=34.88, longitude=-117.00, altitude=664, name='Barstow', tz='Etc/GMT+8') ```