SINGLEAXIS
This function calculates the optimal rotation angle for a single-axis tracker to minimize the angle of incidence (AOI) with the sun.
It can optionally account for axis tilt, axis azimuth, and backtracking to avoid row-to-row shading. The function returns the tracker rotation angle, AOI, and the resulting surface orientation (tilt and azimuth).
Excel Usage
=SINGLEAXIS(apparent_zenith, solar_azimuth, axis_tilt, axis_azimuth, max_angle, backtrack, gcr, cross_axis_tilt)
apparent_zenith(list[list], required): Solar apparent zenith angles (degrees).solar_azimuth(list[list], required): Solar apparent azimuth angles (degrees).axis_tilt(float, optional, default: 0): Tilt of the rotation axis (degrees).axis_azimuth(float, optional, default: 0): Compass direction of the rotation axis (degrees).max_angle(float, optional, default: 90): Maximum rotation angle in either direction (degrees).backtrack(bool, optional, default: true): Enable backtracking to avoid row shading.gcr(float, optional, default: 0.2857): Ground coverage ratio (ratio of panel area to ground area).cross_axis_tilt(float, optional, default: 0): Terrain slope perpendicular to the tracker axis (degrees).
Returns (list[list]): 2D list [[tracker_theta, aoi, surface_tilt, surface_azimuth]], or an error string.
Example 1: Perfect south tracker at noon
Inputs:
| apparent_zenith | solar_azimuth | axis_tilt | axis_azimuth | max_angle | backtrack | gcr |
|---|---|---|---|---|---|---|
| 30 | 180 | 0 | 180 | 60 | true | 0.3 |
Excel formula:
=SINGLEAXIS({30}, {180}, 0, 180, 60, TRUE, 0.3)
Expected output:
| Result | |||
|---|---|---|---|
| 0 | 30 | 0 | 270 |
Example 2: Morning sun rotates tracker eastward
Inputs:
| apparent_zenith | solar_azimuth | axis_tilt | axis_azimuth | max_angle | backtrack | gcr |
|---|---|---|---|---|---|---|
| 45 | 120 | 0 | 180 | 60 | true | 0.3 |
Excel formula:
=SINGLEAXIS({45}, {120}, 0, 180, 60, TRUE, 0.3)
Expected output:
| Result | |||
|---|---|---|---|
| -40.8934 | 20.7048 | 40.8934 | 90 |
Example 3: Vectorized tracker results for two timesteps
Inputs:
| apparent_zenith | solar_azimuth | axis_tilt | axis_azimuth | max_angle | backtrack | gcr | ||
|---|---|---|---|---|---|---|---|---|
| 30 | 60 | 180 | 220 | 0 | 180 | 60 | true | 0.3 |
Excel formula:
=SINGLEAXIS({30,60}, {180,220}, 0, 180, 60, TRUE, 0.3)
Expected output:
| Result | |||
|---|---|---|---|
| 0 | 30 | 0 | 270 |
| 48.0699 | 41.5608 | 48.0699 | 270 |
Example 4: True-tracking without backtracking
Inputs:
| apparent_zenith | solar_azimuth | axis_tilt | axis_azimuth | max_angle | backtrack | gcr |
|---|---|---|---|---|---|---|
| 50 | 200 | 0 | 180 | 75 | false | 0.3 |
Excel formula:
=SINGLEAXIS({50}, {200}, 0, 180, 75, FALSE, 0.3)
Expected output:
| Result | |||
|---|---|---|---|
| 22.176 | 46.0418 | 22.176 | 270 |
Python Code
Show Code
import pandas as pd
import numpy as np
from pvlib.tracking import singleaxis as result_func
def singleaxis(apparent_zenith, solar_azimuth, axis_tilt=0, axis_azimuth=0, max_angle=90, backtrack=True, gcr=0.2857, cross_axis_tilt=0):
"""
Determine the rotation angle and incidence angle for a single-axis tracker.
See: https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.tracking.singleaxis.html
This example function is provided as-is without any representation of accuracy.
Args:
apparent_zenith (list[list]): Solar apparent zenith angles (degrees).
solar_azimuth (list[list]): Solar apparent azimuth angles (degrees).
axis_tilt (float, optional): Tilt of the rotation axis (degrees). Default is 0.
axis_azimuth (float, optional): Compass direction of the rotation axis (degrees). Default is 0.
max_angle (float, optional): Maximum rotation angle in either direction (degrees). Default is 90.
backtrack (bool, optional): Enable backtracking to avoid row shading. Default is True.
gcr (float, optional): Ground coverage ratio (ratio of panel area to ground area). Default is 0.2857.
cross_axis_tilt (float, optional): Terrain slope perpendicular to the tracker axis (degrees). Default is 0.
Returns:
list[list]: 2D list [[tracker_theta, aoi, surface_tilt, surface_azimuth]], or an error string.
"""
try:
def flatten_num(data):
if not isinstance(data, list): return [float(data)]
flat = []
for row in data:
row = row if isinstance(row, list) else [row]
for val in row:
if val == "": flat.append(float('nan'))
else: flat.append(float(val))
return flat
zen_list = flatten_num(apparent_zenith)
az_list = flatten_num(solar_azimuth)
n = len(zen_list)
if n == 0 or len(az_list) != n:
return "Error: zenith and azimuth arrays must have the same non-zero length"
at = float(axis_tilt) if axis_tilt is not None else 0.0
aa = float(axis_azimuth) if axis_azimuth is not None else 0.0
ma = float(max_angle) if max_angle is not None else 90.0
bt = bool(backtrack) if backtrack is not None else True
gc = float(gcr) if gcr is not None else 0.2857
cat = float(cross_axis_tilt) if cross_axis_tilt is not None else 0.0
res = result_func(
apparent_zenith=np.array(zen_list),
solar_azimuth=np.array(az_list),
axis_tilt=at,
axis_azimuth=aa,
max_angle=ma,
backtrack=bt,
gcr=gc,
cross_axis_tilt=cat
)
# res is a DataFrame with columns: tracker_theta, aoi, surface_tilt, surface_azimuth
out = []
tt = res['tracker_theta']
aoi = res['aoi']
st = res['surface_tilt']
sa = res['surface_azimuth']
for i in range(n):
out.append([
float(tt[i]) if not pd.isna(tt[i]) else "",
float(aoi[i]) if not pd.isna(aoi[i]) else "",
float(st[i]) if not pd.isna(st[i]) else "",
float(sa[i]) if not pd.isna(sa[i]) else ""
])
return out
except Exception as e:
return f"Error: {str(e)}"Online Calculator
Solar apparent zenith angles (degrees).
Solar apparent azimuth angles (degrees).
Tilt of the rotation axis (degrees).
Compass direction of the rotation axis (degrees).
Maximum rotation angle in either direction (degrees).
Enable backtracking to avoid row shading.
Ground coverage ratio (ratio of panel area to ground area).
Terrain slope perpendicular to the tracker axis (degrees).