Skip to content

Commit a9ff286

Browse files
jranalliwholmgren
authored andcommitted
Create scaling.py and implement WVM model (pvlib#807)
* create initial files for scaling package * Completed draft of WVM using only discrete distance algorithm. * fixed lint errors, moved scipy imports and marked tests @requires_scipy * added tests to complete coverage as far as I know how * added lines to the sphinx documentation * Changes following review. WVM accepts list of xy tuples as inputs. Cleanup of docs. * Added my name to the list in the what's new docs. Hope this is the intent. * rename internal variables, truncate comment for lint * Removed section headings from api.rst to shorten * Change to try catch block. Added additional test to match. * convert tests to use fixtures * fix lint errors * one more lint error
1 parent febc343 commit a9ff286

File tree

4 files changed

+402
-1
lines changed

4 files changed

+402
-1
lines changed

docs/sphinx/source/api.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,9 +560,19 @@ Bifacial
560560
========
561561

562562
Methods for calculating back surface irradiance
563-
-----------------------------------------------
564563

565564
.. autosummary::
566565
:toctree: generated/
567566

568567
bifacial.pvfactors_timeseries
568+
569+
570+
Scaling
571+
=======
572+
573+
Methods for manipulating irradiance for temporal or spatial considerations
574+
575+
.. autosummary::
576+
:toctree: generated/
577+
578+
scaling.wvm

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ Enhancements
136136
* Add :py:func:`~pvlib.ivtools.fit_sdm_desoto`, a method to fit the De Soto single
137137
diode model to the typical specifications given in manufacturers datasheets.
138138
* Add `timeout` to :py:func:`pvlib.iotools.get_psm3`.
139+
* Add :py:func:`~pvlib.scaling.wvm`, a port of the wavelet variability model for
140+
computing reductions in variability due to a spatially distributed plant.
139141
* Created one new incidence angle modifier (IAM) function for diffuse irradiance:
140142
:py:func:`pvlib.iam.martin_ruiz_diffuse`. (:issue:`751`)
141143

@@ -197,5 +199,6 @@ Contributors
197199
* Miguel Sánchez de León Peque (:ghuser:`Peque`)
198200
* Tanguy Lunel (:ghuser:`tylunel`)
199201
* Veronica Guo (:ghuser:`veronicaguo`)
202+
* Joseph Ranalli (:ghuser:`jranalli`)
200203
* Tony Lorenzo (:ghuser:`alorenzo175`)
201204
* Todd Karin (:ghuser:`toddkarin`)

pvlib/scaling.py

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
"""
2+
The ``scaling`` module contains functions for manipulating irradiance
3+
or other variables to account for temporal or spatial characteristics.
4+
"""
5+
6+
import numpy as np
7+
import pandas as pd
8+
9+
10+
def wvm(clearsky_index, positions, cloud_speed, dt=None):
11+
"""
12+
Compute spatial aggregation time series smoothing on clear sky index based
13+
on the Wavelet Variability model of Lave et al [1-2]. Implementation is
14+
basically a port of the Matlab version of the code [3].
15+
16+
Parameters
17+
----------
18+
clearsky_index : numeric or pandas.Series
19+
Clear Sky Index time series that will be smoothed.
20+
21+
positions : numeric
22+
Array of coordinate distances as (x,y) pairs representing the
23+
easting, northing of the site positions in meters [m]. Distributed
24+
plants could be simulated by gridded points throughout the plant
25+
footprint.
26+
27+
cloud_speed : numeric
28+
Speed of cloud movement in meters per second [m/s].
29+
30+
dt : float, default None
31+
The time series time delta. By default, is inferred from the
32+
clearsky_index. Must be specified for a time series that doesn't
33+
include an index. Units of seconds [s].
34+
35+
Returns
36+
-------
37+
smoothed : numeric or pandas.Series
38+
The Clear Sky Index time series smoothed for the described plant.
39+
40+
wavelet: numeric
41+
The individual wavelets for the time series before smoothing.
42+
43+
tmscales: numeric
44+
The timescales associated with the wavelets in seconds [s].
45+
46+
References
47+
----------
48+
[1] M. Lave, J. Kleissl and J.S. Stein. A Wavelet-Based Variability
49+
Model (WVM) for Solar PV Power Plants. IEEE Transactions on Sustainable
50+
Energy, vol. 4, no. 2, pp. 501-509, 2013.
51+
52+
[2] M. Lave and J. Kleissl. Cloud speed impact on solar variability
53+
scaling - Application to the wavelet variability model. Solar Energy,
54+
vol. 91, pp. 11-21, 2013.
55+
56+
[3] Wavelet Variability Model - Matlab Code:
57+
https://pvpmc.sandia.gov/applications/wavelet-variability-model/
58+
"""
59+
60+
# Added by Joe Ranalli (@jranalli), Penn State Hazleton, 2019
61+
62+
try:
63+
import scipy.optimize
64+
from scipy.spatial.distance import pdist
65+
except ImportError:
66+
raise ImportError("The WVM function requires scipy.")
67+
68+
pos = np.array(positions)
69+
dist = pdist(pos, 'euclidean')
70+
wavelet, tmscales = _compute_wavelet(clearsky_index, dt)
71+
72+
# Find effective length of position vector, 'dist' is full pairwise
73+
n_pairs = len(dist)
74+
75+
def fn(x):
76+
return np.abs((x ** 2 - x) / 2 - n_pairs)
77+
n_dist = np.round(scipy.optimize.fmin(fn, np.sqrt(n_pairs), disp=False))
78+
79+
# Compute VR
80+
A = cloud_speed / 2 # Resultant fit for A from [2]
81+
vr = np.zeros(tmscales.shape)
82+
for i, tmscale in enumerate(tmscales):
83+
rho = np.exp(-1 / A * dist / tmscale) # Eq 5 from [1]
84+
85+
# 2*rho is because rho_ij = rho_ji. +n_dist accounts for sum(rho_ii=1)
86+
denominator = 2 * np.sum(rho) + n_dist
87+
vr[i] = n_dist ** 2 / denominator # Eq 6 of [1]
88+
89+
# Scale each wavelet by VR (Eq 7 in [1])
90+
wavelet_smooth = np.zeros_like(wavelet)
91+
for i in np.arange(len(tmscales)):
92+
if i < len(tmscales) - 1: # Treat the lowest freq differently
93+
wavelet_smooth[i, :] = wavelet[i, :] / np.sqrt(vr[i])
94+
else:
95+
wavelet_smooth[i, :] = wavelet[i, :]
96+
97+
outsignal = np.sum(wavelet_smooth, 0)
98+
99+
try: # See if there's an index already, if so, return as a pandas Series
100+
smoothed = pd.Series(outsignal, index=clearsky_index.index)
101+
except AttributeError:
102+
smoothed = outsignal # just output the numpy signal
103+
104+
return smoothed, wavelet, tmscales
105+
106+
107+
def latlon_to_xy(coordinates):
108+
"""
109+
Convert latitude and longitude in degrees to a coordinate system measured
110+
in meters from zero deg latitude, zero deg longitude.
111+
112+
This is a convenience method to support inputs to wvm. Note that the
113+
methodology used is only suitable for short distances. For conversions of
114+
longer distances, users should consider use of Universal Transverse
115+
Mercator (UTM) or other suitable cartographic projection. Consider
116+
packages built for cartographic projection such as pyproj (e.g.
117+
pyproj.transform()) [2].
118+
119+
Parameters
120+
----------
121+
122+
coordinates : numeric
123+
Array or list of (latitude, longitude) coordinate pairs. Use decimal
124+
degrees notation.
125+
126+
Returns
127+
-------
128+
xypos : numeric
129+
Array of coordinate distances as (x,y) pairs representing the
130+
easting, northing of the position in meters [m].
131+
132+
References
133+
----------
134+
[1] H. Moritz. Geodetic Reference System 1980, Journal of Geodesy, vol. 74,
135+
no. 1, pp 128–133, 2000.
136+
137+
[2] https://pypi.org/project/pyproj/
138+
139+
[3] Wavelet Variability Model - Matlab Code:
140+
https://pvpmc.sandia.gov/applications/wavelet-variability-model/
141+
"""
142+
143+
# Added by Joe Ranalli (@jranalli), Penn State Hazleton, 2019
144+
145+
r_earth = 6371008.7714 # mean radius of Earth, in meters
146+
m_per_deg_lat = r_earth * np.pi / 180
147+
try:
148+
meanlat = np.mean([lat for (lat, lon) in coordinates]) # Mean latitude
149+
except TypeError: # Assume it's a single value?
150+
meanlat = coordinates[0]
151+
m_per_deg_lon = r_earth * np.cos(np.pi/180 * meanlat) * np.pi/180
152+
153+
# Conversion
154+
pos = coordinates * np.array(m_per_deg_lat, m_per_deg_lon)
155+
156+
# reshape as (x,y) pairs to return
157+
try:
158+
return np.column_stack([pos[:, 1], pos[:, 0]])
159+
except IndexError: # Assume it's a single value, which has a 1D shape
160+
return np.array((pos[1], pos[0]))
161+
162+
163+
def _compute_wavelet(clearsky_index, dt=None):
164+
"""
165+
Compute the wavelet transform on the input clear_sky time series.
166+
167+
Parameters
168+
----------
169+
clearsky_index : numeric or pandas.Series
170+
Clear Sky Index time series that will be smoothed.
171+
172+
dt : float, default None
173+
The time series time delta. By default, is inferred from the
174+
clearsky_index. Must be specified for a time series that doesn't
175+
include an index. Units of seconds [s].
176+
177+
Returns
178+
-------
179+
wavelet: numeric
180+
The individual wavelets for the time series
181+
182+
tmscales: numeric
183+
The timescales associated with the wavelets in seconds [s]
184+
185+
References
186+
----------
187+
[1] M. Lave, J. Kleissl and J.S. Stein. A Wavelet-Based Variability
188+
Model (WVM) for Solar PV Power Plants. IEEE Transactions on Sustainable
189+
Energy, vol. 4, no. 2, pp. 501-509, 2013.
190+
191+
[3] Wavelet Variability Model - Matlab Code:
192+
https://pvpmc.sandia.gov/applications/wavelet-variability-model/
193+
"""
194+
195+
# Added by Joe Ranalli (@jranalli), Penn State Hazleton, 2019
196+
197+
try: # Assume it's a pandas type
198+
vals = clearsky_index.values.flatten()
199+
except AttributeError: # Assume it's a numpy type
200+
vals = clearsky_index.flatten()
201+
if dt is None:
202+
raise ValueError("dt must be specified for numpy type inputs.")
203+
else: # flatten() succeeded, thus it's a pandas type, so get its dt
204+
try: # Assume it's a time series type index
205+
dt = (clearsky_index.index[1] - clearsky_index.index[0]).seconds
206+
except AttributeError: # It must just be a numeric index
207+
dt = (clearsky_index.index[1] - clearsky_index.index[0])
208+
209+
# Pad the series on both ends in time and place in a dataframe
210+
cs_long = np.pad(vals, (len(vals), len(vals)), 'symmetric')
211+
cs_long = pd.DataFrame(cs_long)
212+
213+
# Compute wavelet time scales
214+
min_tmscale = np.ceil(np.log(dt)/np.log(2)) # Minimum wavelet timescale
215+
max_tmscale = int(12 - min_tmscale) # maximum wavelet timescale
216+
217+
tmscales = np.zeros(max_tmscale)
218+
csi_mean = np.zeros([max_tmscale, len(cs_long)])
219+
# Loop for all time scales we will consider
220+
for i in np.arange(0, max_tmscale):
221+
j = i+1
222+
tmscales[i] = 2**j * dt # Wavelet integration time scale
223+
intvlen = 2**j # Wavelet integration time series interval
224+
# Rolling average, retains only lower frequencies than interval
225+
df = cs_long.rolling(window=intvlen, center=True, min_periods=1).mean()
226+
# Fill nan's in both directions
227+
df = df.fillna(method='bfill').fillna(method='ffill')
228+
# Pop values back out of the dataframe and store
229+
csi_mean[i, :] = df.values.flatten()
230+
231+
# Calculate the wavelets by isolating the rolling mean frequency ranges
232+
wavelet_long = np.zeros(csi_mean.shape)
233+
for i in np.arange(0, max_tmscale-1):
234+
wavelet_long[i, :] = csi_mean[i, :] - csi_mean[i+1, :]
235+
wavelet_long[max_tmscale-1, :] = csi_mean[max_tmscale-1, :] # Lowest freq
236+
237+
# Clip off the padding and just return the original time window
238+
wavelet = np.zeros([max_tmscale, len(vals)])
239+
for i in np.arange(0, max_tmscale):
240+
wavelet[i, :] = wavelet_long[i, len(vals)+1: 2*len(vals)+1]
241+
242+
return wavelet, tmscales

0 commit comments

Comments
 (0)