|
7 | 7 | import pandas as pd
|
8 | 8 | from pvlib.tools import sind
|
9 | 9 | from pvlib._deprecation import warn_deprecated
|
| 10 | +from pvlib.tools import _get_sample_intervals |
| 11 | +import scipy |
| 12 | +import warnings |
| 13 | + |
10 | 14 |
|
11 | 15 | TEMPERATURE_MODEL_PARAMETERS = {
|
12 | 16 | 'sapm': {
|
@@ -821,3 +825,155 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency,
|
821 | 825 | heat_loss = 1 - module_efficiency / tau_alpha
|
822 | 826 | wind_loss = 9.5 / (5.7 + 3.8 * wind_adj)
|
823 | 827 | return temp_air + cell_temp_init * heat_loss * wind_loss
|
| 828 | + |
| 829 | + |
| 830 | +def prilliman(temp_cell, wind_speed, unit_mass=11.1, coefficients=None): |
| 831 | + """ |
| 832 | + Smooth short-term cell temperature transients using the Prilliman model. |
| 833 | +
|
| 834 | + The Prilliman et al. model [1]_ applies a weighted moving average to |
| 835 | + the output of a steady-state cell temperature model to account for |
| 836 | + a module's thermal inertia by smoothing the cell temperature's |
| 837 | + response to changing weather conditions. |
| 838 | +
|
| 839 | + .. warning:: |
| 840 | + This implementation requires the time series inputs to be regularly |
| 841 | + sampled in time with frequency less than 20 minutes. Data with |
| 842 | + irregular time steps should be resampled prior to using this function. |
| 843 | +
|
| 844 | + Parameters |
| 845 | + ---------- |
| 846 | + temp_cell : pandas.Series with DatetimeIndex |
| 847 | + Cell temperature modeled with steady-state assumptions. [C] |
| 848 | +
|
| 849 | + wind_speed : pandas.Series |
| 850 | + Wind speed, adjusted to correspond to array height [m/s] |
| 851 | +
|
| 852 | + unit_mass : float, default 11.1 |
| 853 | + Total mass of module divided by its one-sided surface area [kg/m^2] |
| 854 | +
|
| 855 | + coefficients : 4-element list-like, optional |
| 856 | + Values for coefficients a_0 through a_3, see Eq. 9 of [1]_ |
| 857 | +
|
| 858 | + Returns |
| 859 | + ------- |
| 860 | + temp_cell : pandas.Series |
| 861 | + Smoothed version of the input cell temperature. Input temperature |
| 862 | + with sampling interval >= 20 minutes is returned unchanged. [C] |
| 863 | +
|
| 864 | + Notes |
| 865 | + ----- |
| 866 | + This smoothing model was developed and validated using the SAPM |
| 867 | + cell temperature model for the steady-state input. |
| 868 | +
|
| 869 | + Smoothing is done using the 20 minute window behind each temperature |
| 870 | + value. At the beginning of the series where a full 20 minute window is not |
| 871 | + possible, partial windows are used instead. |
| 872 | +
|
| 873 | + Output ``temp_cell[k]`` is NaN when input ``wind_speed[k]`` is NaN, or |
| 874 | + when no non-NaN data are in the input temperature for the 20 minute window |
| 875 | + preceding index ``k``. |
| 876 | +
|
| 877 | + References |
| 878 | + ---------- |
| 879 | + .. [1] M. Prilliman, J. S. Stein, D. Riley and G. Tamizhmani, |
| 880 | + "Transient Weighted Moving-Average Model of Photovoltaic Module |
| 881 | + Back-Surface Temperature," IEEE Journal of Photovoltaics, 2020. |
| 882 | + :doi:`10.1109/JPHOTOV.2020.2992351` |
| 883 | + """ |
| 884 | + |
| 885 | + # `sample_interval` in minutes: |
| 886 | + sample_interval, samples_per_window = \ |
| 887 | + _get_sample_intervals(times=temp_cell.index, win_length=20) |
| 888 | + |
| 889 | + if sample_interval >= 20: |
| 890 | + warnings.warn("temperature.prilliman only applies smoothing when " |
| 891 | + "the sampling interval is shorter than 20 minutes " |
| 892 | + f"(input sampling interval: {sample_interval} minutes);" |
| 893 | + " returning input temperature series unchanged") |
| 894 | + # too coarsely sampled for smoothing to be relevant |
| 895 | + return temp_cell |
| 896 | + |
| 897 | + # handle cases where the time series is shorter than 20 minutes total |
| 898 | + samples_per_window = min(samples_per_window, len(temp_cell)) |
| 899 | + |
| 900 | + # prefix with NaNs so that the rolling window is "full", |
| 901 | + # even for the first actual value: |
| 902 | + prefix = np.full(samples_per_window, np.nan) |
| 903 | + temp_cell_prefixed = np.append(prefix, temp_cell.values) |
| 904 | + |
| 905 | + # generate matrix of integers for creating windows with indexing |
| 906 | + H = scipy.linalg.hankel(np.arange(samples_per_window), |
| 907 | + np.arange(samples_per_window - 1, |
| 908 | + len(temp_cell_prefixed) - 1)) |
| 909 | + # each row of `subsets` is the values in one window |
| 910 | + subsets = temp_cell_prefixed[H].T |
| 911 | + |
| 912 | + # `subsets` now looks like this (for 5-minute data, so 4 samples/window) |
| 913 | + # where "1." is a stand-in for the actual temperature values |
| 914 | + # [[nan, nan, nan, nan], |
| 915 | + # [nan, nan, nan, 1.], |
| 916 | + # [nan, nan, 1., 1.], |
| 917 | + # [nan, 1., 1., 1.], |
| 918 | + # [ 1., 1., 1., 1.], |
| 919 | + # [ 1., 1., 1., 1.], |
| 920 | + # [ 1., 1., 1., 1.], |
| 921 | + # ... |
| 922 | + |
| 923 | + # calculate weights for the values in each window |
| 924 | + if coefficients is not None: |
| 925 | + a = coefficients |
| 926 | + else: |
| 927 | + # values from [1], Table II |
| 928 | + a = [0.0046, 0.00046, -0.00023, -1.6e-5] |
| 929 | + |
| 930 | + wind_speed = wind_speed.values |
| 931 | + p = a[0] + a[1]*wind_speed + a[2]*unit_mass + a[3]*wind_speed*unit_mass |
| 932 | + # calculate the time lag for each sample in the window, paying attention |
| 933 | + # to units (seconds for `timedeltas`, minutes for `sample_interval`) |
| 934 | + timedeltas = np.arange(samples_per_window, 0, -1) * sample_interval * 60 |
| 935 | + weights = np.exp(-p[:, np.newaxis] * timedeltas) |
| 936 | + |
| 937 | + # Set weights corresponding to the prefix values to zero; otherwise the |
| 938 | + # denominator of the weighted average below would be wrong. |
| 939 | + # Weights corresponding to (non-prefix) NaN values must be zero too |
| 940 | + # for the same reason. |
| 941 | + |
| 942 | + # Right now `weights` is something like this |
| 943 | + # (using 5-minute inputs, so 4 samples per window -> 4 values per row): |
| 944 | + # [[0.0611, 0.1229, 0.2472, 0.4972], |
| 945 | + # [0.0611, 0.1229, 0.2472, 0.4972], |
| 946 | + # [0.0611, 0.1229, 0.2472, 0.4972], |
| 947 | + # [0.0611, 0.1229, 0.2472, 0.4972], |
| 948 | + # [0.0611, 0.1229, 0.2472, 0.4972], |
| 949 | + # [0.0611, 0.1229, 0.2472, 0.4972], |
| 950 | + # [0.0611, 0.1229, 0.2472, 0.4972], |
| 951 | + # ... |
| 952 | + |
| 953 | + # After the next line, the NaNs in `subsets` will be zeros in `weights`, |
| 954 | + # like this (with more zeros for any NaNs in the input temperature): |
| 955 | + |
| 956 | + # [[0. , 0. , 0. , 0. ], |
| 957 | + # [0. , 0. , 0. , 0.4972], |
| 958 | + # [0. , 0. , 0.2472, 0.4972], |
| 959 | + # [0. , 0.1229, 0.2472, 0.4972], |
| 960 | + # [0.0611, 0.1229, 0.2472, 0.4972], |
| 961 | + # [0.0611, 0.1229, 0.2472, 0.4972], |
| 962 | + # [0.0611, 0.1229, 0.2472, 0.4972], |
| 963 | + # ... |
| 964 | + |
| 965 | + weights[np.isnan(subsets)] = 0 |
| 966 | + |
| 967 | + # change the first row of weights from zero to nan -- this is a |
| 968 | + # trick to prevent div by zero warning when dividing by summed weights |
| 969 | + weights[0, :] = np.nan |
| 970 | + |
| 971 | + # finally, take the weighted average of each window: |
| 972 | + # use np.nansum for numerator to ignore nans in input temperature, but |
| 973 | + # np.sum for denominator to propagate nans in input wind speed. |
| 974 | + numerator = np.nansum(subsets * weights, axis=1) |
| 975 | + denominator = np.sum(weights, axis=1) |
| 976 | + smoothed = numerator / denominator |
| 977 | + smoothed[0] = temp_cell.values[0] |
| 978 | + smoothed = pd.Series(smoothed, index=temp_cell.index) |
| 979 | + return smoothed |
0 commit comments