Skip to content

Commit fa474cc

Browse files
committed
bugfix for high frequency time series in scaling.py, regarding issue 1257. Includes refactor to separate out VR computation within pvlib.scaling.wvm to a new function pvlib.scaling._compute_vr.
1 parent 699f591 commit fa474cc

File tree

3 files changed

+125
-20
lines changed

3 files changed

+125
-20
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ Bug fixes
175175
* Corrected an error affecting :py:func:`~pvlib.clearsky.detect_clearsky`
176176
when data time step is not one minute. Error was introduced in v0.8.1.
177177
(:issue:`1241`, :pull:`1242`)
178+
* Corrected error affecting :py:func:`~pvlib.scaling._compute_wavelet` when
179+
passing a pandas time series with a sampling rate faster than 1 second.
180+
(:issue:`1257`, :pull:`1258`)
181+
(:issue:`1257`, :pull:``)
178182
179183
Testing
180184
~~~~~~~

pvlib/scaling.py

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -62,26 +62,9 @@ def wvm(clearsky_index, positions, cloud_speed, dt=None):
6262

6363
# Added by Joe Ranalli (@jranalli), Penn State Hazleton, 2019
6464

65-
pos = np.array(positions)
66-
dist = pdist(pos, 'euclidean')
6765
wavelet, tmscales = _compute_wavelet(clearsky_index, dt)
6866

69-
# Find effective length of position vector, 'dist' is full pairwise
70-
n_pairs = len(dist)
71-
72-
def fn(x):
73-
return np.abs((x ** 2 - x) / 2 - n_pairs)
74-
n_dist = np.round(scipy.optimize.fmin(fn, np.sqrt(n_pairs), disp=False))
75-
76-
# Compute VR
77-
A = cloud_speed / 2 # Resultant fit for A from [2]
78-
vr = np.zeros(tmscales.shape)
79-
for i, tmscale in enumerate(tmscales):
80-
rho = np.exp(-1 / A * dist / tmscale) # Eq 5 from [1]
81-
82-
# 2*rho is because rho_ij = rho_ji. +n_dist accounts for sum(rho_ii=1)
83-
denominator = 2 * np.sum(rho) + n_dist
84-
vr[i] = n_dist ** 2 / denominator # Eq 6 of [1]
67+
vr = _compute_vr(positions, cloud_speed, tmscales)
8568

8669
# Scale each wavelet by VR (Eq 7 in [1])
8770
wavelet_smooth = np.zeros_like(wavelet)
@@ -101,6 +84,68 @@ def fn(x):
10184
return smoothed, wavelet, tmscales
10285

10386

87+
def _compute_vr(positions, cloud_speed, tmscales):
88+
"""
89+
Compute the variability reduction factors for each wavelet mode for the
90+
Wavelet Variability Model [1-3].
91+
92+
Parameters
93+
----------
94+
positions : numeric
95+
Array of coordinate distances as (x,y) pairs representing the
96+
easting, northing of the site positions in meters [m]. Distributed
97+
plants could be simulated by gridded points throughout the plant
98+
footprint.
99+
100+
cloud_speed : numeric
101+
Speed of cloud movement in meters per second [m/s].
102+
103+
tmscales: numeric
104+
The timescales associated with the wavelets in seconds [s].
105+
106+
Returns
107+
-------
108+
vr : numeric
109+
an array of variability reduction factors for each tmscale.
110+
111+
References
112+
----------
113+
.. [1] M. Lave, J. Kleissl and J.S. Stein. A Wavelet-Based Variability
114+
Model (WVM) for Solar PV Power Plants. IEEE Transactions on Sustainable
115+
Energy, vol. 4, no. 2, pp. 501-509, 2013.
116+
117+
.. [2] M. Lave and J. Kleissl. Cloud speed impact on solar variability
118+
scaling - Application to the wavelet variability model. Solar Energy,
119+
vol. 91, pp. 11-21, 2013.
120+
121+
.. [3] Wavelet Variability Model - Matlab Code:
122+
https://github.com/sandialabs/wvm
123+
"""
124+
125+
# Added by Joe Ranalli (@jranalli), Penn State Hazleton, 2021
126+
127+
pos = np.array(positions)
128+
dist = pdist(pos, 'euclidean')
129+
130+
# Find effective length of position vector, 'dist' is full pairwise
131+
n_pairs = len(dist)
132+
133+
def fn(x):
134+
return np.abs((x ** 2 - x) / 2 - n_pairs)
135+
136+
n_dist = np.round(scipy.optimize.fmin(fn, np.sqrt(n_pairs), disp=False))
137+
# Compute VR
138+
A = cloud_speed / 2 # Resultant fit for A from [2]
139+
vr = np.zeros(tmscales.shape)
140+
for i, tmscale in enumerate(tmscales):
141+
rho = np.exp(-1 / A * dist / tmscale) # Eq 5 from [1]
142+
143+
# 2*rho is because rho_ij = rho_ji. +n_dist accounts for sum(rho_ii=1)
144+
denominator = 2 * np.sum(rho) + n_dist
145+
vr[i] = n_dist ** 2 / denominator # Eq 6 of [1]
146+
return vr
147+
148+
104149
def latlon_to_xy(coordinates):
105150
"""
106151
Convert latitude and longitude in degrees to a coordinate system measured
@@ -205,7 +250,8 @@ def _compute_wavelet(clearsky_index, dt=None):
205250
raise ValueError("dt must be specified for numpy type inputs.")
206251
else: # flatten() succeeded, thus it's a pandas type, so get its dt
207252
try: # Assume it's a time series type index
208-
dt = (clearsky_index.index[1] - clearsky_index.index[0]).seconds
253+
dt = clearsky_index.index[1] - clearsky_index.index[0]
254+
dt = dt.seconds + dt.microseconds/1e6
209255
except AttributeError: # It must just be a numeric index
210256
dt = (clearsky_index.index[1] - clearsky_index.index[0])
211257

@@ -221,7 +267,7 @@ def _compute_wavelet(clearsky_index, dt=None):
221267
csi_mean = np.zeros([max_tmscale, len(cs_long)])
222268
# Skip averaging for the 0th scale
223269
csi_mean[0, :] = cs_long.values.flatten()
224-
tmscales[0] = 1
270+
tmscales[0] = dt
225271
# Loop for all time scales we will consider
226272
for i in np.arange(1, max_tmscale):
227273
tmscales[i] = 2**i * dt # Wavelet integration time scale

pvlib/tests/test_scaling.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ def time(clear_sky_index):
3737
return np.arange(0, len(clear_sky_index))
3838

3939

40+
@pytest.fixture
41+
def time_60s(clear_sky_index):
42+
# Sample time vector 60s resolution
43+
return np.arange(0, len(clear_sky_index))*60
44+
45+
46+
@pytest.fixture
47+
def time_500ms(clear_sky_index):
48+
# Sample time vector 0.5s resolution
49+
return np.arange(0, len(clear_sky_index))*0.5
50+
51+
4052
@pytest.fixture
4153
def positions():
4254
# Sample positions based on the previous lat/lon (calculated manually)
@@ -51,6 +63,18 @@ def expect_tmscale():
5163
return [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]
5264

5365

66+
@pytest.fixture
67+
def expect_tmscale_1min():
68+
# Expected timescales for dt = 60
69+
return [60, 120, 240, 480, 960, 1920, 3840]
70+
71+
72+
@pytest.fixture
73+
def expect_tmscale_500ms():
74+
# Expected timescales for dt = 0.5
75+
return [0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]
76+
77+
5478
@pytest.fixture
5579
def expect_wavelet():
5680
# Expected wavelet for indices 5000:5004 for clear_sky_index above (Matlab)
@@ -68,6 +92,13 @@ def expect_cs_smooth():
6892
return np.array([1., 1., 1.05774, 0.94226, 1.])
6993

7094

95+
@pytest.fixture
96+
def expect_vr():
97+
# Expected VR for expecttmscale
98+
return np.array([3., 3., 3., 3., 3., 3., 2.9997844, 2.9708118, 2.6806291,
99+
2.0726611, 1.5653324, 1.2812714, 1.1389995])
100+
101+
71102
def test_latlon_to_xy_zero():
72103
coord = [0, 0]
73104
pos_e = [0, 0]
@@ -109,6 +140,25 @@ def test_compute_wavelet_series_numindex(clear_sky_index, time,
109140
assert_almost_equal(wavelet[:, 5000:5005], expect_wavelet)
110141

111142

143+
def test_compute_wavelet_series_highres(clear_sky_index, time_500ms,
144+
expect_tmscale_500ms, expect_wavelet):
145+
dtindex = pd.to_datetime(time_500ms, unit='s')
146+
csi_series = pd.Series(clear_sky_index, index=dtindex)
147+
wavelet, tmscale = scaling._compute_wavelet(csi_series)
148+
assert_almost_equal(tmscale, expect_tmscale_500ms)
149+
assert_almost_equal(wavelet[:, 5000:5005].shape, (14, 5))
150+
151+
152+
def test_compute_wavelet_series_minuteres(clear_sky_index, time_60s,
153+
expect_tmscale_1min, expect_wavelet):
154+
dtindex = pd.to_datetime(time_60s, unit='s')
155+
csi_series = pd.Series(clear_sky_index, index=dtindex)
156+
wavelet, tmscale = scaling._compute_wavelet(csi_series)
157+
assert_almost_equal(tmscale, expect_tmscale_1min)
158+
assert_almost_equal(wavelet[:, 5000:5005].shape,
159+
expect_wavelet[0:len(tmscale), :].shape)
160+
161+
112162
def test_compute_wavelet_array(clear_sky_index,
113163
expect_tmscale, expect_wavelet):
114164
wavelet, tmscale = scaling._compute_wavelet(clear_sky_index, dt)
@@ -129,6 +179,11 @@ def test_compute_wavelet_dwttheory(clear_sky_index, time,
129179
assert_almost_equal(np.sum(wavelet, 0), csi_series)
130180

131181

182+
def test_compute_vr(positions, expect_tmscale, expect_vr):
183+
vr = scaling._compute_vr(positions, cloud_speed, np.array(expect_tmscale))
184+
assert_almost_equal(vr, expect_vr)
185+
186+
132187
def test_wvm_series(clear_sky_index, time, positions, expect_cs_smooth):
133188
csi_series = pd.Series(clear_sky_index, index=time)
134189
cs_sm, _, _ = scaling.wvm(csi_series, positions, cloud_speed)

0 commit comments

Comments
 (0)