Skip to content

Commit ca61503

Browse files
jranallicwhanse
andauthored
Fix to WVM methodology to track changes in MATLAB version. (#1213)
* Add theoretical DWT test for scaling.py * Adjust test fixtures relative to outputs from corrected MATLAB version. * Implement fix to Python version of code. Some additional work needed to be done here to get the wavelet levels to match the MATLAB output. The only place python and MATLAB don't agree now is at the edges, where there are different assumptions about how the moving average should be computed for the beginning/end of the time series. In principle, this could actually matter for time series that aren't "much" longer than 4096 seconds. * Update docs * Update reference to WVM in scaling.py and formatting thereof. * Suggested update to the documentation language. Co-authored-by: Cliff Hansen <[email protected]> * docstring and whatsnew adjustments Co-authored-by: Cliff Hansen <[email protected]>
1 parent 6edef7f commit ca61503

File tree

3 files changed

+64
-39
lines changed

3 files changed

+64
-39
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ Bug fixes
117117
* Reindl model fixed to generate sky_diffuse=0 when GHI=0.
118118
(:issue:`1153`, :pull:`1154`)
119119
* Update GFS product names for GFS v16. (:issue:`1202`, :pull:`1203`)
120+
* Corrected methodology error in :py:func:`~pvlib.scaling.wvm`. Tracks with
121+
fix in PVLib for MATLAB. (:issue:`1206`, :pull:`1213`)
120122

121123
Testing
122124
~~~~~~~
@@ -126,7 +128,6 @@ Testing
126128

127129
Documentation
128130
~~~~~~~~~~~~~
129-
130131
* Update intro tutorial to highlight the use of historical meteorological data
131132
and to make the procedural and OO results match exactly. (:issue:`1116`, :pull:`1144`)
132133
* Add a gallery example showing how to appropriately use interval-averaged
@@ -150,3 +151,4 @@ Contributors
150151
* Joshua Stein (:ghuser:`jsstein`)
151152
* Tony Lorenzo (:ghuser:`alorenzo175`)
152153
* Damjan Postolovski (:ghuser:`dpostolovski`)
154+
* Joe Ranalli (:ghuser:`jranalli`)

pvlib/scaling.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
def wvm(clearsky_index, positions, cloud_speed, dt=None):
1414
"""
1515
Compute spatial aggregation time series smoothing on clear sky index based
16-
on the Wavelet Variability model of Lave et al [1-2]. Implementation is
17-
basically a port of the Matlab version of the code [3].
16+
on the Wavelet Variability model of Lave et al. [1]_, [2]_. Implementation
17+
is basically a port of the Matlab version of the code [3]_.
1818
1919
Parameters
2020
----------
@@ -48,16 +48,16 @@ def wvm(clearsky_index, positions, cloud_speed, dt=None):
4848
4949
References
5050
----------
51-
[1] M. Lave, J. Kleissl and J.S. Stein. A Wavelet-Based Variability
52-
Model (WVM) for Solar PV Power Plants. IEEE Transactions on Sustainable
53-
Energy, vol. 4, no. 2, pp. 501-509, 2013.
51+
.. [1] M. Lave, J. Kleissl and J.S. Stein. A Wavelet-Based Variability
52+
Model (WVM) for Solar PV Power Plants. IEEE Transactions on Sustainable
53+
Energy, vol. 4, no. 2, pp. 501-509, 2013.
5454
55-
[2] M. Lave and J. Kleissl. Cloud speed impact on solar variability
56-
scaling - Application to the wavelet variability model. Solar Energy,
57-
vol. 91, pp. 11-21, 2013.
55+
.. [2] M. Lave and J. Kleissl. Cloud speed impact on solar variability
56+
scaling - Application to the wavelet variability model. Solar Energy,
57+
vol. 91, pp. 11-21, 2013.
5858
59-
[3] Wavelet Variability Model - Matlab Code:
60-
https://pvpmc.sandia.gov/applications/wavelet-variability-model/
59+
.. [3] Wavelet Variability Model - Matlab Code:
60+
https://github.com/sandialabs/wvm
6161
"""
6262

6363
# Added by Joe Ranalli (@jranalli), Penn State Hazleton, 2019
@@ -128,13 +128,13 @@ def latlon_to_xy(coordinates):
128128
129129
References
130130
----------
131-
[1] H. Moritz. Geodetic Reference System 1980, Journal of Geodesy, vol. 74,
132-
no. 1, pp 128–133, 2000.
131+
.. [1] H. Moritz. Geodetic Reference System 1980, Journal of Geodesy, vol.
132+
74, no. 1, pp 128–133, 2000.
133133
134-
[2] https://pypi.org/project/pyproj/
134+
.. [2] https://pypi.org/project/pyproj/
135135
136-
[3] Wavelet Variability Model - Matlab Code:
137-
https://pvpmc.sandia.gov/applications/wavelet-variability-model/
136+
.. [3] Wavelet Variability Model - Matlab Code:
137+
https://github.com/sandialabs/wvm
138138
"""
139139

140140
# Added by Joe Ranalli (@jranalli), Penn State Hazleton, 2019
@@ -159,7 +159,12 @@ def latlon_to_xy(coordinates):
159159

160160
def _compute_wavelet(clearsky_index, dt=None):
161161
"""
162-
Compute the wavelet transform on the input clear_sky time series.
162+
Compute the wavelet transform on the input clear_sky time series. Uses a
163+
top hat wavelet [-1,1,1,-1] shape, based on the difference of successive
164+
centered moving averages. Smallest scale (filter size of 2) is a degenerate
165+
case that resembles a Haar wavelet. Returns one level of approximation
166+
coefficient (CAn) and n levels of detail coefficients (CD1, CD2, ...,
167+
CDn-1, CDn).
163168
164169
Parameters
165170
----------
@@ -174,19 +179,20 @@ def _compute_wavelet(clearsky_index, dt=None):
174179
Returns
175180
-------
176181
wavelet: numeric
177-
The individual wavelets for the time series
182+
The individual wavelets for the time series. Format follows increasing
183+
scale (decreasing frequency): [CD1, CD2, ..., CDn, CAn]
178184
179185
tmscales: numeric
180186
The timescales associated with the wavelets in seconds [s]
181187
182188
References
183189
----------
184-
[1] M. Lave, J. Kleissl and J.S. Stein. A Wavelet-Based Variability
185-
Model (WVM) for Solar PV Power Plants. IEEE Transactions on Sustainable
186-
Energy, vol. 4, no. 2, pp. 501-509, 2013.
190+
.. [1] M. Lave, J. Kleissl and J.S. Stein. A Wavelet-Based Variability
191+
Model (WVM) for Solar PV Power Plants. IEEE Transactions on
192+
Sustainable Energy, vol. 4, no. 2, pp. 501-509, 2013.
187193
188-
[3] Wavelet Variability Model - Matlab Code:
189-
https://pvpmc.sandia.gov/applications/wavelet-variability-model/
194+
.. [2] Wavelet Variability Model - Matlab Code:
195+
https://github.com/sandialabs/wvm
190196
"""
191197

192198
# Added by Joe Ranalli (@jranalli), Penn State Hazleton, 2019
@@ -209,31 +215,37 @@ def _compute_wavelet(clearsky_index, dt=None):
209215

210216
# Compute wavelet time scales
211217
min_tmscale = np.ceil(np.log(dt)/np.log(2)) # Minimum wavelet timescale
212-
max_tmscale = int(12 - min_tmscale) # maximum wavelet timescale
218+
max_tmscale = int(13 - min_tmscale) # maximum wavelet timescale
213219

214220
tmscales = np.zeros(max_tmscale)
215221
csi_mean = np.zeros([max_tmscale, len(cs_long)])
222+
# Skip averaging for the 0th scale
223+
csi_mean[0, :] = cs_long.values.flatten()
224+
tmscales[0] = 1
216225
# Loop for all time scales we will consider
217-
for i in np.arange(0, max_tmscale):
218-
j = i+1
219-
tmscales[i] = 2**j * dt # Wavelet integration time scale
220-
intvlen = 2**j # Wavelet integration time series interval
226+
for i in np.arange(1, max_tmscale):
227+
tmscales[i] = 2**i * dt # Wavelet integration time scale
228+
intvlen = 2**i # Wavelet integration time series interval
221229
# Rolling average, retains only lower frequencies than interval
230+
# Produces slightly different end effects than the MATLAB version
222231
df = cs_long.rolling(window=intvlen, center=True, min_periods=1).mean()
223232
# Fill nan's in both directions
224233
df = df.fillna(method='bfill').fillna(method='ffill')
225234
# Pop values back out of the dataframe and store
226235
csi_mean[i, :] = df.values.flatten()
236+
# Shift to account for different indexing in MATLAB moving average
237+
csi_mean[i, :] = np.roll(csi_mean[i, :], -1)
238+
csi_mean[i, -1] = csi_mean[i, -2]
227239

228-
# Calculate the wavelets by isolating the rolling mean frequency ranges
240+
# Calculate detail coefficients by difference between successive averages
229241
wavelet_long = np.zeros(csi_mean.shape)
230242
for i in np.arange(0, max_tmscale-1):
231243
wavelet_long[i, :] = csi_mean[i, :] - csi_mean[i+1, :]
232-
wavelet_long[max_tmscale-1, :] = csi_mean[max_tmscale-1, :] # Lowest freq
244+
wavelet_long[-1, :] = csi_mean[-1, :] # Lowest freq (CAn)
233245

234246
# Clip off the padding and just return the original time window
235247
wavelet = np.zeros([max_tmscale, len(vals)])
236248
for i in np.arange(0, max_tmscale):
237-
wavelet[i, :] = wavelet_long[i, len(vals)+1: 2*len(vals)+1]
249+
wavelet[i, :] = wavelet_long[i, len(vals): 2*len(vals)]
238250

239251
return wavelet, tmscales

pvlib/tests/test_scaling.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,24 @@ def positions():
4848
@pytest.fixture
4949
def expect_tmscale():
5050
# Expected timescales for dt = 1
51-
return [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]
51+
return [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]
5252

5353

5454
@pytest.fixture
5555
def expect_wavelet():
5656
# Expected wavelet for indices 5000:5004 for clear_sky_index above (Matlab)
57-
return np.array([[-0.025, 0.05, 0., -0.05, 0.025],
58-
[0.025, 0., 0., 0., -0.025],
59-
[0., 0., 0., 0., 0.]])
57+
e = np.zeros([13, 5])
58+
e[0, :] = np.array([0, -0.05, 0.1, -0.05, 0])
59+
e[1, :] = np.array([-0.025, 0.05, 0., -0.05, 0.025])
60+
e[2, :] = np.array([0.025, 0., 0., 0., -0.025])
61+
e[-1, :] = np.array([1, 1, 1, 1, 1])
62+
return e
6063

6164

6265
@pytest.fixture
6366
def expect_cs_smooth():
6467
# Expected smoothed clear sky index for indices 5000:5004 (Matlab)
65-
return np.array([1., 1.0289, 1., 0.9711, 1.])
68+
return np.array([1., 1., 1.05774, 0.94226, 1.])
6669

6770

6871
def test_latlon_to_xy_zero():
@@ -94,7 +97,7 @@ def test_compute_wavelet_series(clear_sky_index, time,
9497
csi_series = pd.Series(clear_sky_index, index=time)
9598
wavelet, tmscale = scaling._compute_wavelet(csi_series)
9699
assert_almost_equal(tmscale, expect_tmscale)
97-
assert_almost_equal(wavelet[0:3, 5000:5005], expect_wavelet)
100+
assert_almost_equal(wavelet[:, 5000:5005], expect_wavelet)
98101

99102

100103
def test_compute_wavelet_series_numindex(clear_sky_index, time,
@@ -103,21 +106,29 @@ def test_compute_wavelet_series_numindex(clear_sky_index, time,
103106
csi_series = pd.Series(clear_sky_index, index=dtindex)
104107
wavelet, tmscale = scaling._compute_wavelet(csi_series)
105108
assert_almost_equal(tmscale, expect_tmscale)
106-
assert_almost_equal(wavelet[0:3, 5000:5005], expect_wavelet)
109+
assert_almost_equal(wavelet[:, 5000:5005], expect_wavelet)
107110

108111

109112
def test_compute_wavelet_array(clear_sky_index,
110113
expect_tmscale, expect_wavelet):
111114
wavelet, tmscale = scaling._compute_wavelet(clear_sky_index, dt)
112115
assert_almost_equal(tmscale, expect_tmscale)
113-
assert_almost_equal(wavelet[0:3, 5000:5005], expect_wavelet)
116+
assert_almost_equal(wavelet[:, 5000:5005], expect_wavelet)
114117

115118

116119
def test_compute_wavelet_array_invalid(clear_sky_index):
117120
with pytest.raises(ValueError):
118121
scaling._compute_wavelet(clear_sky_index)
119122

120123

124+
def test_compute_wavelet_dwttheory(clear_sky_index, time,
125+
expect_tmscale, expect_wavelet):
126+
# Confirm detail coeffs sum to original signal
127+
csi_series = pd.Series(clear_sky_index, index=time)
128+
wavelet, tmscale = scaling._compute_wavelet(csi_series)
129+
assert_almost_equal(np.sum(wavelet, 0), csi_series)
130+
131+
121132
def test_wvm_series(clear_sky_index, time, positions, expect_cs_smooth):
122133
csi_series = pd.Series(clear_sky_index, index=time)
123134
cs_sm, _, _ = scaling.wvm(csi_series, positions, cloud_speed)

0 commit comments

Comments
 (0)