Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.6.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Enhancements
`doc` (requirements for minimal documentation build), `test` (requirements
for testing), and `all` (optional + doc + test). (:issue:`553`, :issue:`483`)
* Set default alpha to 1.14 in :func:`~pvlib.atmosphere.angstrom_aod_at_lambda` (:issue:`563`)
* tracking.singleaxis now accepts scalar and 1D-array input.


Bug fixes
Expand Down Expand Up @@ -152,6 +153,9 @@ Bug fixes
* Fix bug in get_relative_airmass(model='youngirvine1967'). (:issue:`545`)
* Fix bug in variable names returned by forecast.py's HRRR_ESRL model.
(:issue:`557`)
* Fixed bug in tracking.singleaxis that mistakenly assigned nan values when
the Sun was still above the horizon. No effect on systems with axis_tilt=0.
(:issue:`569`)


Documentation
Expand Down
152 changes: 131 additions & 21 deletions pvlib/test/test_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,102 @@
SINGLEAXIS_COL_ORDER = ['tracker_theta', 'aoi',
'surface_azimuth', 'surface_tilt']


def test_solar_noon():
apparent_zenith = pd.Series([10])
apparent_azimuth = pd.Series([180])
index = pd.DatetimeIndex(start='20180701T1200', freq='1s', periods=1)
apparent_zenith = pd.Series([10], index=index)
apparent_azimuth = pd.Series([180], index=index)
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
axis_tilt=0, axis_azimuth=0,
max_angle=90, backtrack=True,
gcr=2.0/7.0)

expect = pd.DataFrame({'tracker_theta': 0, 'aoi': 10,
'surface_azimuth': 90, 'surface_tilt': 0},
index=[0], dtype=np.float64)
index=index, dtype=np.float64)
expect = expect[SINGLEAXIS_COL_ORDER]

assert_frame_equal(expect, tracker_data)


def test_scalars():
apparent_zenith = 10
apparent_azimuth = 180
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
axis_tilt=0, axis_azimuth=0,
max_angle=90, backtrack=True,
gcr=2.0/7.0)
assert isinstance(tracker_data, dict)
expect = {'tracker_theta': 0, 'aoi': 10, 'surface_azimuth': 90,
'surface_tilt': 0}
for k, v in expect.items():
assert_allclose(tracker_data[k], v)


def test_arrays():
apparent_zenith = np.array([10])
apparent_azimuth = np.array([180])
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
axis_tilt=0, axis_azimuth=0,
max_angle=90, backtrack=True,
gcr=2.0/7.0)
assert isinstance(tracker_data, dict)
expect = {'tracker_theta': 0, 'aoi': 10, 'surface_azimuth': 90,
'surface_tilt': 0}
for k, v in expect.items():
assert_allclose(tracker_data[k], v)


def test_nans():
apparent_zenith = np.array([10, np.nan, 10])
apparent_azimuth = np.array([180, 180, np.nan])
with np.errstate(invalid='ignore'):
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
axis_tilt=0, axis_azimuth=0,
max_angle=90, backtrack=True,
gcr=2.0/7.0)
expect = {'tracker_theta': np.array([0, nan, nan]),
'aoi': np.array([10, nan, nan]),
'surface_azimuth': np.array([90, nan, nan]),
'surface_tilt': np.array([0, nan, nan])}
for k, v in expect.items():
assert_allclose(tracker_data[k], v)

# repeat with Series because nans can differ
apparent_zenith = pd.Series(apparent_zenith)
apparent_azimuth = pd.Series(apparent_azimuth)
with np.errstate(invalid='ignore'):
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
axis_tilt=0, axis_azimuth=0,
max_angle=90, backtrack=True,
gcr=2.0/7.0)
expect = pd.DataFrame(np.array(
[[ 0., 10., 90., 0.],
[nan, nan, nan, nan],
[nan, nan, nan, nan]]),
columns=['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt'])
assert_frame_equal(tracker_data, expect)


def test_arrays_multi():
apparent_zenith = np.array([[10, 10], [10, 10]])
apparent_azimuth = np.array([[180, 180], [180, 180]])
# singleaxis should fail for num dim > 1
with pytest.raises(ValueError):
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
axis_tilt=0, axis_azimuth=0,
max_angle=90, backtrack=True,
gcr=2.0/7.0)
# uncomment if we ever get singleaxis to support num dim > 1 arrays
# assert isinstance(tracker_data, dict)
# expect = {'tracker_theta': np.full_like(apparent_zenith, 0),
# 'aoi': np.full_like(apparent_zenith, 10),
# 'surface_azimuth': np.full_like(apparent_zenith, 90),
# 'surface_tilt': np.full_like(apparent_zenith, 0)}
# for k, v in expect.items():
# assert_allclose(tracker_data[k], v)


def test_azimuth_north_south():
apparent_zenith = pd.Series([60])
apparent_azimuth = pd.Series([90])
Expand Down Expand Up @@ -163,14 +243,38 @@ def test_axis_azimuth():
assert_frame_equal(expect, tracker_data)


def test_index_mismatch():
apparent_zenith = pd.Series([30])
apparent_azimuth = pd.Series([90,180])
with pytest.raises(ValueError):
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
axis_tilt=0, axis_azimuth=90,
max_angle=90, backtrack=True,
gcr=2.0/7.0)
def test_horizon_flat():
# GH 569
solar_azimuth = np.array([0, 180, 359])
solar_zenith = np.array([100, 45, 100])
solar_azimuth = pd.Series(solar_azimuth)
solar_zenith = pd.Series(solar_zenith)
# depending on platform and numpy versions this will generate
# RuntimeWarning: invalid value encountered in > < >=
out = tracking.singleaxis(solar_zenith, solar_azimuth, axis_tilt=0,
axis_azimuth=180, backtrack=False, max_angle=180)
expected = pd.DataFrame(np.array(
[[ nan, nan, nan, nan],
[ 0., 45., 270., 0.],
[ nan, nan, nan, nan]]),
columns=['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt'])
assert_frame_equal(out, expected)


def test_horizon_tilted():
# GH 569
solar_azimuth = np.array([0, 180, 359])
solar_zenith = np.full_like(solar_azimuth, 45)
solar_azimuth = pd.Series(solar_azimuth)
solar_zenith = pd.Series(solar_zenith)
out = tracking.singleaxis(solar_zenith, solar_azimuth, axis_tilt=90,
axis_azimuth=180, backtrack=False, max_angle=180)
expected = pd.DataFrame(np.array(
[[ 180., 45., 0., 90.],
[ 0., 45., 180., 90.],
[ 179., 45., 359., 90.]]),
columns=['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt'])
assert_frame_equal(out, expected)


def test_SingleAxisTracker_creation():
Expand Down Expand Up @@ -285,19 +389,25 @@ def test_get_irradiance():
end='20160101 1800-0700', freq='6H')
location = Location(latitude=32, longitude=-111)
solar_position = location.get_solarposition(times)
irrads = pd.DataFrame({'dni':[900,0], 'ghi':[600,0], 'dhi':[100,0]},
irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]},
index=times)
solar_zenith = solar_position['apparent_zenith']
solar_azimuth = solar_position['azimuth']
tracker_data = system.singleaxis(solar_zenith, solar_azimuth)

irradiance = system.get_irradiance(tracker_data['surface_tilt'],
tracker_data['surface_azimuth'],
solar_zenith,
solar_azimuth,
irrads['dni'],
irrads['ghi'],
irrads['dhi'])

# invalid warnings already generated in horizon test above,
# no need to clutter test output here
with np.errstate(invalid='ignore'):
tracker_data = system.singleaxis(solar_zenith, solar_azimuth)

# some invalid values in irradiance.py. not our problem here
with np.errstate(invalid='ignore'):
irradiance = system.get_irradiance(tracker_data['surface_tilt'],
tracker_data['surface_azimuth'],
solar_zenith,
solar_azimuth,
irrads['dni'],
irrads['ghi'],
irrads['dhi'])

expected = pd.DataFrame(data=np.array(
[[961.80070, 815.94490, 145.85580, 135.32820, 10.52757492],
Expand Down
72 changes: 35 additions & 37 deletions pvlib/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,10 @@ def singleaxis(apparent_zenith, apparent_azimuth,

Parameters
----------
apparent_zenith : Series
apparent_zenith : float, 1d array, or Series
Solar apparent zenith angles in decimal degrees.

apparent_azimuth : Series
apparent_azimuth : float, 1d array, or Series
Solar apparent azimuth angles in decimal degrees.

axis_tilt : float, default 0
Expand Down Expand Up @@ -296,7 +296,7 @@ def singleaxis(apparent_zenith, apparent_azimuth,

Returns
-------
DataFrame with the following columns:
dict or DataFrame with the following columns:

* tracker_theta: The rotation angle of the tracker.
tracker_theta = 0 is horizontal, and positive rotation angles are
Expand All @@ -318,6 +318,18 @@ def singleaxis(apparent_zenith, apparent_azimuth,
# MATLAB to Python conversion by
# Will Holmgren (@wholmgren), U. Arizona. March, 2015.

if isinstance(apparent_zenith, pd.Series):
index = apparent_zenith.index
else:
index = None

# convert scalars to arrays
apparent_azimuth = np.atleast_1d(apparent_azimuth)
apparent_zenith = np.atleast_1d(apparent_zenith)

if apparent_azimuth.ndim > 1 or apparent_zenith.ndim > 1:
raise ValueError('Input dimensions must not exceed 1')

# Calculate sun position x, y, z using coordinate system as in [1], Eq 2.

# Positive y axis is oriented parallel to earth surface along tracking axis
Expand All @@ -334,15 +346,6 @@ def singleaxis(apparent_zenith, apparent_azimuth,
# Rotate sun azimuth to coordinate system as in [1]
# to calculate sun position.

try:
pd.util.testing.assert_index_equal(apparent_azimuth.index,
apparent_zenith.index)
except AssertionError:
raise ValueError('apparent_azimuth.index and '
'apparent_zenith.index must match.')

times = apparent_azimuth.index

az = apparent_azimuth - 180
apparent_elevation = 90 - apparent_zenith
x = cosd(apparent_elevation) * sind(az)
Expand Down Expand Up @@ -408,10 +411,11 @@ def singleaxis(apparent_zenith, apparent_azimuth,

# Calculate angle from x-y plane to projection of sun vector onto x-z plane
# and then obtain wid by translating tmp to convention for rotation angles.
wid = pd.Series(90 - np.degrees(np.arctan2(zp, xp)), index=times)
wid = 90 - np.degrees(np.arctan2(zp, xp))

# filter for sun above panel horizon
wid[zp <= 0] = np.nan
zen_gt_90 = apparent_zenith > 90
wid[zen_gt_90] = np.nan

# Account for backtracking; modified from [1] to account for rotation
# angle convention being used here.
Expand All @@ -423,14 +427,11 @@ def singleaxis(apparent_zenith, apparent_azimuth,
# (always positive b/c acosd returns values between 0 and 180)
wc = np.degrees(np.arccos(temp))

v = wid < 0
widc = pd.Series(index=times)
widc[~v] = wid[~v] - wc[~v] # Eq 4 applied when wid in QI
widc[v] = wid[v] + wc[v] # Eq 4 applied when wid in QIV
# Eq 4 applied when wid in QIV (wid < 0 evalulates True), QI
tracker_theta = np.where(wid < 0, wid + wc, wid - wc)
else:
widc = wid
tracker_theta = wid

tracker_theta = widc.copy()
tracker_theta[tracker_theta > max_angle] = max_angle
tracker_theta[tracker_theta < -max_angle] = -max_angle

Expand All @@ -447,7 +448,6 @@ def singleaxis(apparent_zenith, apparent_azimuth,

# calculate angle-of-incidence on panel
aoi = np.degrees(np.arccos(np.abs(np.sum(sun_vec*panel_norm, axis=0))))
aoi = pd.Series(aoi, index=times)

# calculate panel tilt and azimuth
# in a coordinate system where the panel tilt is the
Expand Down Expand Up @@ -491,9 +491,8 @@ def singleaxis(apparent_zenith, apparent_azimuth,
# surface_azimuth = pd.Series(
# np.degrees(np.arctan(projected_normal[:,1]/projected_normal[:,0])),
# index=times)
surface_azimuth = pd.Series(
np.degrees(np.arctan2(projected_normal[:, 1], projected_normal[:, 0])),
index=times)
surface_azimuth = \
np.degrees(np.arctan2(projected_normal[:, 1], projected_normal[:, 0]))

# 2. Clean up atan when x-coord or y-coord is zero
# surface_azimuth[(projected_normal[:,0]==0) & (projected_normal[:,1]>0)] = 90
Expand Down Expand Up @@ -545,18 +544,17 @@ def singleaxis(apparent_zenith, apparent_azimuth,
surface_azimuth[surface_azimuth >= 360] -= 360

# Calculate surface_tilt
# Use pandas to calculate the sum because it handles nan values better.
surface_tilt = (90 - np.degrees(np.arccos(
pd.DataFrame(panel_norm_earth * projected_normal,
index=times).sum(axis=1))))
dotproduct = (panel_norm_earth * projected_normal).sum(axis=1)
surface_tilt = 90 - np.degrees(np.arccos(dotproduct))

# Bundle DataFrame for return values and filter for sun below horizon.
df_out = pd.DataFrame({'tracker_theta': tracker_theta, 'aoi': aoi,
'surface_azimuth': surface_azimuth,
'surface_tilt': surface_tilt},
index=times)
df_out = df_out[['tracker_theta', 'aoi',
'surface_azimuth', 'surface_tilt']]
df_out[apparent_zenith > 90] = np.nan

return df_out
out = {'tracker_theta': tracker_theta, 'aoi': aoi,
'surface_azimuth': surface_azimuth, 'surface_tilt': surface_tilt}
if index is not None:
out = pd.DataFrame(out, index=index)
out = out[['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']]
out[zen_gt_90] = np.nan
else:
out = {k: np.where(zen_gt_90, np.nan, v) for k, v in out.items()}

return out