|
| 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