10000 fix incorrect nan assignments in singleaxis, allow scalar and 1d arra… · lboeman/pvlib-python@e1dacb2 · GitHub
[go: up one dir, main page]

Skip to content

Commit e1dacb2

Browse files
authored
fix incorrect nan assignments in singleaxis, allow scalar and 1d array input (pvlib#573)
* fix singleaxis horizon test * singleaxis now accepts scalar and 1d array input * add dimension check * more tests, better warning control * update ci tool configs * more config * fix fail_on_pvlib_version decorator does not run code when pass expected * use predefined bools
1 parent f8bbf95 commit e1dacb2

File tree

3 files changed

+170
-58
lines changed

3 files changed

+170
-58
lines changed

docs/sphinx/source/whatsnew/v0.6.0.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ Enhancements
125125
`doc` (requirements for minimal documentation build), `test` (requirements
126126
for testing), and `all` (optional + doc + test). (:issue:`553`, :issue:`483`)
127127
* Set default alpha to 1.14 in :func:`~pvlib.atmosphere.angstrom_aod_at_lambda` (:issue:`563`)
128+
* tracking.singleaxis now accepts scalar and 1D-array input.
128129

129130

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

156160

157161
Documentation

pvlib/test/test_tracking.py

Lines changed: 131 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,102 @@
1414
SINGLEAXIS_COL_ORDER = ['tracker_theta', 'aoi',
1515
'surface_azimuth', 'surface_tilt']
1616

17+
1718
def test_solar_noon():
18-
apparent_zenith = pd.Series([10])
19-
apparent_azimuth = pd.Series([180])
19+
index = pd.DatetimeIndex(start='20180701T1200', freq='1s', periods=1)
20+
apparent_zenith = pd.Series([10], index=index)
21+
apparent_azimuth = pd.Series([180], index=index)
2022
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
2123
axis_tilt=0, axis_azimuth=0,
2224
max_angle=90, backtrack=True,
2325
gcr=2.0/7.0)
2426

2527
expect = pd.DataFrame({'tracker_theta': 0, 'aoi': 10,
2628
'surface_azimuth': 90, 'surface_tilt': 0},
27-
index=[0], dtype=np.float64)
29+
index=index, dtype=np.float64)
2830
expect = expect[SINGLEAXIS_COL_ORDER]
2931

3032
assert_frame_equal(expect, tracker_data)
3133

3234

35+
def test_scalars():
36+
apparent_zenith = 10
37+
apparent_azimuth = 180
38+
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
39+
axis_tilt=0, axis_azimuth=0,
40+
max_angle=90, backtrack=True,
41+
gcr=2.0/7.0)
42+
assert isinstance(tracker_data, dict)
43+
expect = {'tracker_theta': 0, 'aoi': 10, 'surface_azimuth': 90,
44+
'surface_tilt': 0}
45+
for k, v in expect.items():
46+
assert_allclose(tracker_data[k], v)
47+
48+
49+
def test_arrays():
50+
apparent_zenith = np.array([10])
51+
apparent_azimuth = np.array([180])
52+
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
53+
axis_tilt=0, axis_azimuth=0,
54+
max_angle=90, backtrack=True,
55+
gcr=2.0/7.0)
56+
assert isinstance(tracker_data, dict)
57+
expect = {'tracker_theta': 0, 'aoi': 10, 'surface_azimuth': 90,
58+
'surface_tilt': 0}
59+
for k, v in expect.items():
60+
assert_allclose(tracker_data[k], v)
61+
62+
63+
def test_nans():
64+
apparent_zenith = np.array([10, np.nan, 10])
65+
apparent_azimuth = np.array([180, 180, np.nan])
66+
with np.errstate(invalid='ignore'):
67+
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
68+
axis_tilt=0, axis_azimuth=0,
69+
max_angle=90, backtrack=True,
70+
gcr=2.0/7.0)
71+
expect = {'tracker_theta': np.array([0, nan, nan]),
72+
'aoi': np.array([10, nan, nan]),
73+
'surface_azimuth': np.array([90, nan, nan]),
74+
'surface_tilt': np.array([0, nan, nan])}
75+
for k, v in expect.items():
76+
assert_allclose(tracker_data[k], v)
77+
78+
# repeat with Series because nans can differ
79+
apparent_zenith = pd.Series(apparent_zenith)
80+
apparent_azimuth = pd.Series(apparent_azimuth)
81+
with np.errstate(invalid='ignore'):
82+
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
83+
axis_tilt=0, axis_azimuth=0,
84+
max_angle=90, backtrack=True,
85+
gcr=2.0/7.0)
86+
expect = pd.DataFrame(np.array(
87+
[[ 0., 10., 90., 0.],
88+
[nan, nan, nan, nan],
89+
[nan, nan, nan, nan]]),
90+
columns=['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt'])
91+
assert_frame_equal(tracker_data, expect)
92+
93+
94+
def test_arrays_multi():
95+
apparent_zenith = np.array([[10, 10], [10, 10]])
96+
apparent_azimuth = np.array([[180, 180], [180, 180]])
97+
# singleaxis should fail for num dim > 1
98+
with pytest.raises(ValueError):
99+
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
100+
axis_tilt=0, axis_azimuth=0,
101+
max_angle=90, backtrack=True,
102+
gcr=2.0/7.0)
103+
# uncomment if we ever get singleaxis to support num dim > 1 arrays
104+
# assert isinstance(tracker_data, dict)
105+
# expect = {'tracker_theta': np.full_like(apparent_zenith, 0),
106+
# 'aoi': np.full_like(apparent_zenith, 10),
107+
# 'surface_azimuth': np.full_like(apparent_zenith, 90),
108+
# 'surface_tilt': np.full_like(apparent_zenith, 0)}
109+
# for k, v in expect.items():
110+
# assert_allclose(tracker_data[k], v)
111+
112+
33113
def test_azimuth_north_south():
34114
apparent_zenith = pd.Series([60])
35115
apparent_azimuth = pd.Series([90])
@@ -163,14 +243,38 @@ def test_axis_azimuth():
163243
assert_frame_equal(expect, tracker_data)
164244

165245

166-
def test_index_mismatch():
167-
apparent_zenith = pd.Series([30])
168-
apparent_azimuth = pd.Series([90,180])
169-
with pytest.raises(ValueError):
170-
tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth,
171-
axis_tilt=0, axis_azimuth=90,
172-
max_angle=90, backtrack=True,
173-
gcr=2.0/7.0)
246+
def test_horizon_flat():
247+
# GH 569
248+
solar_azimuth = np.array([0, 180, 359])
249+
solar_zenith = np.array([100, 45, 100])
250+
solar_azimuth = pd.Series(solar_azimuth)
251+
solar_zenith = pd.Series(solar_zenith)
252+
# depending on platform and numpy versions this will generate
253+
# RuntimeWarning: invalid value encountered in > < >=
254+
out = tracking.singleaxis(solar_zenith, solar_azimuth, axis_tilt=0,
255+
axis_azimuth=180, backtrack=False, max_angle=180)
256+
expected = pd.DataFrame(np.array(
257+
[[ nan, nan, nan, nan],
258+
[ 0., 45., 270., 0.],
259+
[ nan, nan, nan, nan]]),
260+
columns=['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt'])
261+
assert_frame_equal(out, expected)
262+
263+
264+
def test_horizon_tilted():
265+
# GH 569
266+
solar_azimuth = np.array([0, 180, 359])
267+
solar_zenith = np.full_like(solar_azimuth, 45)
268+
solar_azimuth = pd.Series(solar_azimuth)
269+
solar_zenith = pd.Series(solar_zenith)
270+
out = tracking.singleaxis(solar_zenith, solar_azimuth, axis_tilt=90,
271+
axis_azimuth=180, backtrack=False, max_angle=180)
272+
expected = pd.DataFrame(np.array(
273+
[[ 180., 45., 0., 90.],
274+
[ 0., 45., 180., 90.],
275+
[ 179., 45., 359., 90.]]),
276+
columns=['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt'])
277+
assert_frame_equal(out, expected)
174278

175279

176280
def test_SingleAxisTracker_creation():
@@ -285,19 +389,25 @@ def test_get_irradiance():
285389
end='20160101 1800-0700', freq='6H')
286390
location = Location(latitude=32, longitude=-111)
287391
solar_position = location.get_solarposition(times)
288-
irrads = pd.DataFrame({'dni':[900,0], 'ghi':[600,0], 'dhi':[100,0]},
392+
irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]},
289393
index=times)
290394
solar_zenith = solar_position['apparent_zenith']
291395
solar_azimuth = solar_position['azimuth']
292-
tracker_data = system.singleaxis(solar_zenith, solar_azimuth)
293-
294-
irradiance = system.get_irradiance(tracker_data['surface_tilt'],
295-
tracker_data['surface_azimuth'],
296-
solar_zenith,
297-
solar_azimuth,
298-
irrads['dni'],
299-
irrads['ghi'],
300-
irrads['dhi'])
396+
397+
# invalid warnings already generated in horizon test above,
398+
# no need to clutter test output here
399+
with np.errstate(invalid='ignore'):
400+
tracker_data = system.singleaxis(solar_zenith, solar_azimuth)
401+
402+
# some invalid values in irradiance.py. not our problem here
403+
with np.errstate(invalid='ignore'):
404+
irradiance = system.get_irradiance(tracker_data['surface_tilt'],
405+
tracker_data['surface_azimuth'],
406+
solar_zenith,
407+
solar_azimuth,
408+
irrads['dni'],
409+
irrads['ghi'],
410+
irrads['dhi'])
301411

302412
expected = pd.DataFrame(data=np.array(
303413
[[961.80070, 815.94490, 145.85580, 135.32820, 10.52757492],

pvlib/tracking.py

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,10 @@ def singleaxis(apparent_zenith, apparent_azimuth,
260260
261261
Parameters
262262
----------
263-
apparent_zenith : Series
263+
apparent_zenith : float, 1d array, or Series
264264
Solar apparent zenith angles in decimal degrees.
265265
266-
apparent_azimuth : Series
266+
apparent_azimuth : float, 1d array, or Series
267267
Solar apparent azimuth angles in decimal degrees.
268268
269269
axis_tilt : float, default 0
@@ -296,7 +296,7 @@ def singleaxis(apparent_zenith, apparent_azimuth,
296296
297297
Returns
298298
-------
299-
DataFrame with the following columns:
299+
dict or DataFrame with the following columns:
300300
301301
* tracker_theta: The rotation angle of the tracker.
302302
tracker_theta = 0 is horizontal, and positive rotation angles are
@@ -318,6 +318,18 @@ def singleaxis(apparent_zenith, apparent_azimuth,
318318
# MATLAB to Python conversion by
319319
# Will Holmgren (@wholmgren), U. Arizona. March, 2015.
320320

321+
if isinstance(apparent_zenith, pd.Series):
322+
index = apparent_zenith.index
323+
else:
324+
index = None
325+
326+
# convert scalars to arrays
327+
apparent_azimuth = np.atleast_1d(apparent_azimuth)
328+
apparent_zenith = np.atleast_1d(apparent_zenith)
329+
330+
if apparent_azimuth.ndim > 1 or apparent_zenith.ndim > 1:
331+
raise ValueError('Input dimensions must not exceed 1')
332+
321333
# Calculate sun position x, y, z using coordinate system as in [1], Eq 2.< 10000 /div>
322334

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

337-
try:
338-
pd.util.testing.assert_index_equal(apparent_azimuth.index,
339-
apparent_zenith.index)
340-
except AssertionError:
341-
raise ValueError('apparent_azimuth.index and '
342-
'apparent_zenith.index must match.')
343-
344-
times = apparent_azimuth.index
345-
346349
az = apparent_azimuth - 180
347350
apparent_elevation = 90 - apparent_zenith
348351
x = cosd(apparent_elevation) * sind(az)
@@ -408,10 +411,11 @@ def singleaxis(apparent_zenith, apparent_azimuth,
408411

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

413416
# filter for sun above panel horizon
414-
wid[zp <= 0] = np.nan
417+
zen_gt_90 = apparent_zenith > 90
418+
wid[zen_gt_90] = np.nan
415419

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

426-
v = wid < 0
427-
widc = pd.Series(index=times)
428-
widc[~v] = wid[~v] - wc[~v] # Eq 4 applied when wid in QI
429-
widc[v] = wid[v] + wc[v] # Eq 4 applied when wid in QIV
430+
# Eq 4 applied when wid in QIV (wid < 0 evalulates True), QI
431+
tracker_theta = np.where(wid < 0, wid + wc, wid - wc)
430432
else:
431-
widc = wid
433+
tracker_theta = wid
432434

433-
tracker_theta = widc.copy()
434435
tracker_theta[tracker_theta > max_angle] = max_angle
435436
tracker_theta[tracker_theta < -max_angle] = -max_angle
436437

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

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

452452
# calculate panel tilt and azimuth
453453
# in a coordinate system where the panel tilt is the
@@ -491,9 +491,8 @@ def singleaxis(apparent_zenith, apparent_azimuth,
491491
# surface_azimuth = pd.Series(
492492
# np.degrees(np.arctan(projected_normal[:,1]/projected_normal[:,0])),
493493
# index=times)
494-
surface_azimuth = pd.Series(
495-
np.degrees(np.arctan2(projected_normal[:, 1], projected_normal[:, 0])),
496-
index=times)
494+
surface_azimuth = \
495+
np.degrees(np.arctan2(projected_normal[:, 1], projected_normal[:, 0]))
497496

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

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

553550
# Bundle DataFrame for return values and filter for sun below horizon.
554-
df_out = pd.DataFrame({'tracker_theta': tracker_theta, 'aoi': aoi,
555-
'surface_azimuth': surface_azimuth,
556-
'surface_tilt': surface_tilt},
557-
index=times)
558-
df_out = df_out[['tracker_theta', 'aoi',
559-
'surface_azimuth', 'surface_tilt']]
560-
df_out[apparent_zenith > 90] = np.nan
561-
562-
return df_out
551+
out = {'tracker_theta': tracker_theta, 'aoi': aoi,
552+
'surface_azimuth': surface_azimuth, 'surface_tilt': surface_tilt}
553+
if index is not None:
554+
out = pd.DataFrame(out, index=index)
555+
out = out[['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']]
556+
out[zen_gt_90] = np.nan
557+
else:
558+
out = {k: np.where(zen_gt_90, np.nan, v) for k, v in out.items()}
559+
560+
return out

0 commit comments

Comments
 (0)
0