-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add Prilliman et al transience model to pvlib.temperature #1391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
7619d04
ff2c006
95bff13
e5f83db
f92e73a
92afe49
56519e2
c384415
d92250c
985baba
01c4afc
188e3df
be1704e
5e2c6da
2197f3c
3095afb
7cecbbe
5fda720
b8d5479
0a0a1f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,10 @@ | |
import pandas as pd | ||
from pvlib.tools import sind | ||
from pvlib._deprecation import warn_deprecated | ||
from pvlib.tools import _get_sample_intervals | ||
import scipy | ||
import warnings | ||
|
||
|
||
TEMPERATURE_MODEL_PARAMETERS = { | ||
'sapm': { | ||
|
@@ -821,3 +825,148 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency, | |
heat_loss = 1 - module_efficiency / tau_alpha | ||
wind_loss = 9.5 / (5.7 + 3.8 * wind_adj) | ||
return temp_air + cell_temp_init * heat_loss * wind_loss | ||
|
||
|
||
def prilliman(temp_cell, wind_speed, unit_mass=11.1, coefficients=None): | ||
""" | ||
Smooth short-term cell temperature transients using the Prilliman model. | ||
|
||
The Prilliman et al. model [1]_ applies a weighted moving average to | ||
the output of a steady-state cell temperature model to account for | ||
a module's thermal inertia by smoothing the cell temperature's | ||
response to changing weather conditions. | ||
|
||
.. warning:: | ||
This implementation requires the time series inputs to be regularly | ||
sampled in time with frequency less than 20 minutes. Data with | ||
irregular time steps should be resampled prior to using this function. | ||
|
||
Parameters | ||
---------- | ||
temp_cell : pandas.Series with DatetimeIndex | ||
Cell temperature modeled with steady-state assumptions. [C] | ||
|
||
wind_speed : pandas.Series | ||
Wind speed, adjusted to correspond to array height [m/s] | ||
|
||
unit_mass : float, default 11.1 | ||
Total mass of module divided by its one-sided surface area [kg/m^2] | ||
|
||
coefficients : 4-element list-like, optional | ||
Values for coefficients a_0 through a_3, see Eq. 9 of [1]_ | ||
|
||
Returns | ||
------- | ||
temp_cell : pandas.Series | ||
Smoothed version of the input cell temperature [C] | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add here that the original data is returned if too coarsely sampled. |
||
Notes | ||
----- | ||
This smoothing model was developed and validated using the SAPM | ||
cell temperature model for the steady-state input. | ||
|
||
At the beginning of the series where a full 20 minute window is not | ||
possible, "partial" windows including whatever values are available | ||
is used instead. | ||
kandersolar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
References | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One more suggestion: explain in Notes how
|
||
---------- | ||
.. [1] M. Prilliman, J. S. Stein, D. Riley and G. Tamizhmani, | ||
"Transient Weighted Moving-Average Model of Photovoltaic Module | ||
Back-Surface Temperature," IEEE Journal of Photovoltaics, 2020. | ||
:doi:`10.1109/JPHOTOV.2020.2992351` | ||
""" | ||
|
||
# `sample_interval` in minutes: | ||
sample_interval, samples_per_window = \ | ||
_get_sample_intervals(times=temp_cell.index, win_length=20) | ||
|
||
if sample_interval >= 20: | ||
warnings.warn("temperature.prilliman only applies smoothing when " | ||
"the sampling interval is shorter than 20 minutes " | ||
f"(input sampling interval: {sample_interval} minutes)") | ||
# too coarsely sampled for smoothing to be relevant | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a warning here would be appropriate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe extend the message to say that the return values are the original temperature? |
||
return temp_cell | ||
|
||
# handle cases where the time series is shorter than 20 minutes total | ||
samples_per_window = min(samples_per_window, len(temp_cell)) | ||
|
||
# prefix with NaNs so that the rolling window is "full", | ||
# even for the first actual value: | ||
prefix = np.full(samples_per_window, np.nan) | ||
temp_cell_prefixed = np.append(prefix, temp_cell.values) | ||
|
||
# generate matrix of integers for creating windows with indexing | ||
H = scipy.linalg.hankel(np.arange(samples_per_window), | ||
np.arange(samples_per_window - 1, | ||
len(temp_cell_prefixed))) | ||
# each row of `subsets` is the values in one window | ||
subsets = temp_cell_prefixed[H].T | ||
|
||
# `subsets` now looks like this (for 5-minute data, so 4 samples/window) | ||
# where "1." is a stand-in for the actual temperature values | ||
# [[nan, nan, nan, nan], | ||
# [nan, nan, nan, 1.], | ||
# [nan, nan, 1., 1.], | ||
# [nan, 1., 1., 1.], | ||
# [ 1., 1., 1., 1.], | ||
# [ 1., 1., 1., 1.], | ||
# [ 1., 1., 1., 1.], | ||
# ... | ||
|
||
# calculate weights for the values in each window | ||
if coefficients is not None: | ||
a = coefficients | ||
else: | ||
# values from [1], Table II | ||
a = [0.0046, 0.00046, -0.00023, -1.6e-5] | ||
|
||
wind_speed = wind_speed.values | ||
p = a[0] + a[1]*wind_speed + a[2]*unit_mass + a[3]*wind_speed*unit_mass | ||
# calculate the time lag for each sample in the window, paying attention | ||
# to units (seconds for `timedeltas`, minutes for `sample_interval`) | ||
timedeltas = np.arange(samples_per_window, 0, -1) * sample_interval * 60 | ||
weights = np.exp(-p[:, np.newaxis] * timedeltas) | ||
|
||
kandersolar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# set weights corresponding to the prefix values to zero; otherwise the | ||
# denominator of the weighted average below would be wrong. | ||
|
||
# The following arcane magic turns `weights` from something like this | ||
# (using 5-minute inputs, so 4 samples per window -> 4 values per row): | ||
# [[0.0611, 0.1229, 0.2472, 0.4972], | ||
# [0.0611, 0.1229, 0.2472, 0.4972], | ||
# [0.0611, 0.1229, 0.2472, 0.4972], | ||
# [0.0611, 0.1229, 0.2472, 0.4972], | ||
# [0.0611, 0.1229, 0.2472, 0.4972], | ||
# [0.0611, 0.1229, 0.2472, 0.4972], | ||
# [0.0611, 0.1229, 0.2472, 0.4972]] | ||
# ... | ||
|
||
# to this: | ||
# [[0. , 0. , 0. , 0. ], | ||
# [0. , 0. , 0. , 0.4972], | ||
# [0. , 0. , 0.2472, 0.4972], | ||
# [0. , 0.1229, 0.2472, 0.4972], | ||
# [0.0611, 0.1229, 0.2472, 0.4972], | ||
# [0.0611, 0.1229, 0.2472, 0.4972], | ||
# [0.0611, 0.1229, 0.2472, 0.4972]] | ||
# ... | ||
|
||
# note that the triangle of zeros here corresponds to nans in `subsets`. | ||
# it is a bit opaque, but it is fast! | ||
mask_idx = np.triu_indices(samples_per_window) | ||
np.fliplr(weights)[mask_idx] = 0 | ||
|
||
# change the first row of weights from zero to nan -- this is a | ||
# trick to prevent div by zero warning when dividing by summed weights | ||
weights[0, :] = np.nan | ||
|
||
# finally, take the weighted average of each window: | ||
# use np.nansum for numerator to ignore nans in input temperature, but | ||
# np.sum for denominator to propagate nans in input wind speed. | ||
numerator = np.nansum(subsets[:-1] * weights, axis=1) | ||
denominator = np.sum(weights, axis=1) | ||
kandersolar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
smoothed = numerator / denominator | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kanderso-nrel this line emitted a warning despite the comment on lines 967-969 that suggested otherwise. Is this a version-specific issue? My versions are below...
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, the only way I can reproduce this is if there is a stretch of NaN in the input lasting longer than 20 minutes -- is that true for your case? That comment is referring to a div by zero warning cause by a quirk of this implementation (no weights for the very first value), not a div by zero warning caused by nans in the input. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, thanks. Yes, I had 38 minutes of NaN temperatures. I filled only the middle point with a valid number and the warning disappeared. |
||
smoothed[0] = temp_cell.values[0] | ||
smoothed = pd.Series(smoothed, index=temp_cell.index) | ||
return smoothed |
Uh oh!
There was an error while loading. Please reload this page.