forked from pvlib/pvlib-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsnow.py
More file actions
388 lines (323 loc) · 14.5 KB
/
snow.py
File metadata and controls
388 lines (323 loc) · 14.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
"""
The ``snow`` module contains functions that model module snow cover and the
associated effects on PV module output
"""
import numpy as np
import pandas as pd
from pvlib.tools import sind, cosd, tand
def _time_delta_in_hours(times):
delta = times.to_series().diff()
return delta.dt.total_seconds().div(3600)
def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1.,
threshold_depth=1.):
'''
Calculates the timesteps when modules are fully covered by snow.
Parameters
----------
snowfall: Series
Snowfall in each time period. [cm]
snow_depth: Series, optional
Snow depth on the ground at the beginning of each time period.
Must have the same index as ``snowfall``. [cm]
threshold_snowfall: float, default 1.0
Hourly snowfall above which the row is fully covered for that hour.
[cm/hr]
threshold_depth: float, default 1.0
Snow depth on the ground, above which snow can affect the modules. [cm]
Returns
----------
covered: Series
A Series of boolean, True where the snowfall exceeds the defined
threshold to fully cover the panel.
Notes
-----
Implements the model described in [1]_ with minor improvements in [2]_.
``snow_depth`` is used to return `False` (not fully covered) when snow
is less than ``threshold_depth. This check is described in [2]_ as needed
for systems with low tilt angle.
References
----------
.. [1] Marion, B.; Schaefer, R.; Caine, H.; Sanchez, G. (2013).
"Measured and modeled photovoltaic system energy losses from snow for
Colorado and Wisconsin locations." Solar Energy 97; pp.112-121.
.. [2] Ryberg, D; Freeman, J. "Integration, Validation, and Application
of a PV Snow Coverage Model in SAM" (2017) NREL Technical Report
NREL/TP-6A20-68705
'''
timestep = _time_delta_in_hours(snowfall.index)
hourly_snow_rate = snowfall / timestep
# if we can infer a time frequency, use first snowfall value
# otherwise the first snowfall value is ignored
freq = pd.infer_freq(snowfall.index)
if freq is not None:
timedelta = pd.tseries.frequencies.to_offset(freq) / pd.Timedelta('1h')
hourly_snow_rate.iloc[0] = snowfall.iloc[0] / timedelta
else: # can't infer frequency from index
hourly_snow_rate.iloc[0] = 0 # replaces NaN
covered = (hourly_snow_rate > threshold_snowfall)
# no coverage when no snow on the ground
if snow_depth is not None:
covered = covered & (snow_depth >= threshold_depth)
return covered
def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt,
snow_depth=None, initial_coverage=0, threshold_snowfall=1.,
threshold_depth=1., can_slide_coefficient=-80.,
slide_amount_coefficient=0.197):
'''
Calculates the fraction of the slant height of a row of modules that is
covered by snow at every time step.
Implements the model described in [1]_ with minor improvements in [2]_,
with the change that the output is in fraction of the row's slant height
rather than in tenths of the row slant height. As described in [1]_, model
validation focused on fixed tilt systems.
Parameters
----------
snowfall : Series
Snowfall within each time period. [cm]
poa_irradiance : Series
Total in-plane irradiance [W/m^2]
temp_air : Series
Ambient air temperature [C]
surface_tilt : numeric
Tilt of module's from horizontal, e.g. surface facing up = 0,
surface facing horizon = 90. [degrees]
snow_depth : Series, optional
Snow depth on the ground at the beginning of each time period.
Must have the same index as ``snowfall``. [cm]
initial_coverage : float, default 0
Fraction of row's slant height that is covered with snow at the
beginning of the simulation. [unitless]
threshold_snowfall : float, default 1.0
Hourly snowfall above which snow coverage is set to the row's slant
height. [cm/hr]
threshold_depth: float, default 1.0
Snow depth on the ground, above which snow can affect the modules. [cm]
can_slide_coefficient : float, default -80.
Coefficient to determine if snow can slide given irradiance and air
temperature. [W/(m^2 C)]
slide_amount_coefficient : float, default 0.197
Coefficient to determine fraction of snow that slides off in one hour.
[unitless]
Returns
-------
snow_coverage : Series
The fraction of the slant height of a row of modules that is covered
by snow at each time step.
Notes
-----
In [1]_, ``can_slide_coefficient`` is termed `m`, and the value of
``slide_amount_coefficient`` is given in tenths of a module's slant height.
``snow_depth`` is used to set ``snow_coverage`` to 0 (not fully covered)
when snow is less than ``threshold_depth. . This check is described in
[2]_ as needed for systems with low tilt angle.
References
----------
.. [1] Marion, B.; Schaefer, R.; Caine, H.; Sanchez, G. (2013).
"Measured and modeled photovoltaic system energy losses from snow for
Colorado and Wisconsin locations." Solar Energy 97; pp.112-121.
.. [2] Ryberg, D; Freeman, J. (2017). "Integration, Validation, and
Application of a PV Snow Coverage Model in SAM" NREL Technical Report
NREL/TP-6A20-68705
'''
# find times with new snowfall
new_snowfall = fully_covered_nrel(snowfall, snow_depth, threshold_snowfall,
threshold_depth)
# set up output Series
snow_coverage = pd.Series(np.nan, index=poa_irradiance.index)
# determine amount that snow can slide in each timestep
can_slide = temp_air > poa_irradiance / can_slide_coefficient
slide_amt = slide_amount_coefficient * sind(surface_tilt) * \
_time_delta_in_hours(poa_irradiance.index)
slide_amt[~can_slide] = 0.
# don't slide during snow events
slide_amt[new_snowfall] = 0.
# don't slide in the interval preceding the snowfall data
slide_amt.iloc[0] = 0
if snow_depth is not None:
# all slides off if there is no snow on the ground
# described in [2] to avoid non-sliding snow for low-tilt systems.
# default threshold_depth of 1cm is from SAM's implementation and
# is different than the value of 0cm implied in [2].
# https://github.com.mcas-gov.ms/NREL/ssc/issues/1265
slide_amt[snow_depth < threshold_depth] = 1.
# build time series of cumulative slide amounts
sliding_period_ID = new_snowfall.cumsum()
cumulative_sliding = slide_amt.groupby(sliding_period_ID).cumsum()
# set up time series of snow coverage without any sliding applied
snow_coverage[new_snowfall] = 1.0
if np.isnan(snow_coverage.iloc[0]):
snow_coverage.iloc[0] = initial_coverage
snow_coverage.ffill(inplace=True)
snow_coverage -= cumulative_sliding
return snow_coverage.clip(lower=0)
def dc_loss_nrel(snow_coverage, num_strings):
'''
Calculates the fraction of DC capacity lost due to snow coverage.
DC capacity loss assumes that if a string is partially covered by snow,
the string's capacity is lost; see [1]_, Eq. 11.8.
Module orientation is accounted for by specifying the number of cell
strings in parallel along the slant height.
For example, a typical 60-cell module has 3 parallel strings, each
comprising 20 cells in series, with the cells arranged in 6 columns of 10
cells each. For a row consisting of single modules, if the module is
mounted in portrait orientation, i.e., the row slant height is along a
column of 10 cells, there is 1 string in parallel along the row slant
height, so `num_strings=1`. In contrast, if the module is mounted in
landscape orientation with the row slant height comprising 6 cells, there
are 3 parallel strings along the row slant height, so `num_strings=3`.
Parameters
----------
snow_coverage : numeric
The fraction of row slant height covered by snow at each time step.
num_strings: int
The number of parallel-connected strings along a row slant height.
Returns
-------
loss : numeric
fraction of DC capacity loss due to snow coverage at each time step.
References
----------
.. [1] Gilman, P. et al., (2018). "SAM Photovoltaic Model Technical
Reference Update", NREL Technical Report NREL/TP-6A20-67399.
Available at https://www.nrel.gov/docs/fy18osti/67399.pdf
'''
return np.ceil(snow_coverage * num_strings) / num_strings
def _townsend_effective_snow(snow_total, snow_events):
'''
Calculates effective snow using the total snowfall received each month and
the number of snowfall events each month.
Parameters
----------
snow_total : array-like
Snow received each month. Referred to as S in [1]_. [cm]
snow_events : array-like
Number of snowfall events each month. Referred to as N in [1]_. [-]
Returns
-------
effective_snowfall : array-like
Effective snowfall as defined in the Townsend model. [cm]
References
----------
.. [1] Townsend, Tim & Powers, Loren. (2011). Photovoltaics and snow: An
update from two winters of measurements in the SIERRA. 37th IEEE
Photovoltaic Specialists Conference, Seattle, WA, USA.
:doi:`10.1109/PVSC.2011.6186627`
'''
snow_events_no_zeros = np.maximum(snow_events, 1)
effective_snow = 0.5 * snow_total * (1 + 1 / snow_events_no_zeros)
return np.where(snow_events > 0, effective_snow, 0)
def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
temp_air, poa_global, slant_height, lower_edge_height,
string_factor=1.0, angle_of_repose=40):
'''
Calculates monthly snow loss based on the Townsend monthly snow loss
model.
This model is described in [1]_.
Parameters
----------
snow_total : array-like
Snow received each month. Referred to as S in [1]_. [cm]
snow_events : array-like
Number of snowfall events each month. Snow events are defined as days
in the month that have snowfall greater than 1 inch. May be int or
float type for the average events in a typical month. Referred to as N
in [1]_.
surface_tilt : float
Tilt angle of the array. [deg]
relative_humidity : array-like
Monthly average relative humidity. [%]
temp_air : array-like
Monthly average ambient temperature. [C]
poa_global : array-like
Monthly plane of array insolation. [Wh/m2]
slant_height : float
Row length in the slanted plane of array dimension. [m]
lower_edge_height : float
Distance from array lower edge to the ground. [m]
string_factor : float, default 1.0
Multiplier applied to monthly loss fraction. Use 1.0 if the DC array
has only one string of modules in the slant direction, use 0.75
otherwise. [-]
angle_of_repose : float, default 40
Piled snow angle, assumed to stabilize at 40°, the midpoint of
25°-55° avalanching slope angles. [deg]
Returns
-------
loss : array-like
Monthly average DC capacity loss fraction due to snow coverage.
Notes
-----
This model has not been validated for tracking arrays; however, for
tracking arrays [1]_ suggests using the maximum rotation angle in place
of ``surface_tilt``. The author of [1]_ recommends using one-half the
table width for ``slant_height``, i.e., the distance from the tracker
axis to the module edge.
The parameter `string_factor` is an enhancement added to the model after
publication of [1]_, as described in [2]_.
The definition for snow events documented above is based on [3]_.
References
----------
.. [1] Townsend, Tim & Powers, Loren. (2011). Photovoltaics and snow: An
update from two winters of measurements in the SIERRA. 37th IEEE
Photovoltaic Specialists Conference, Seattle, WA, USA.
:doi:`10.1109/PVSC.2011.6186627`
.. [2] Townsend, T. and Previtali, J. (2023). A Fresh Dusting: Current
Uses of the Townsend Snow Model. In "Photovoltaic Reliability
Workshop (PVRW) 2023 Proceedings: Posters.", ed. Silverman,
T. J. Dec. 2023. NREL/CP-5900-87918.
Available at: https://www.nrel.gov/docs/fy25osti/90585.pdf
.. [3] Townsend, T. (2013). Predicting PV Energy Loss Caused by Snow.
Solar Power International, Chicago IL.
:doi:`10.13140/RG.2.2.14299.68647`
'''
# unit conversions from cm and m to in, from C to K, and from % to fraction
# doing this early to facilitate comparison of this code with [1]
snow_total_inches = snow_total / 2.54 # to inches
relative_humidity_fraction = relative_humidity / 100.
poa_global_kWh = poa_global / 1000.
slant_height_inches = slant_height * 39.37
lower_edge_height_inches = lower_edge_height * 39.37
temp_air_kelvin = temp_air + 273.15
C1 = 5.7e04
C2 = 0.51
snow_total_prev = np.roll(snow_total_inches, 1)
snow_events_prev = np.roll(snow_events, 1)
effective_snow = _townsend_effective_snow(snow_total_inches, snow_events)
effective_snow_prev = _townsend_effective_snow(
snow_total_prev,
snow_events_prev
)
effective_snow_weighted = (
1 / 3 * effective_snow_prev
+ 2 / 3 * effective_snow
)
# the lower limit of 0.1 in^2 is per private communication with the model's
# author. CWH 1/30/2023
lower_edge_distance = np.clip(
lower_edge_height_inches**2 - effective_snow_weighted**2, a_min=0.1,
a_max=None)
gamma = (
slant_height_inches
* effective_snow_weighted
* cosd(surface_tilt)
/ lower_edge_distance
* 2
* tand(angle_of_repose)
)
ground_interference_term = 1 - C2 * np.exp(-gamma)
# Calculate Eqn. 3 in the reference.
# Although the reference says Eqn. 3 calculates percentage loss, the y-axis
# of Figure 7 indicates Eqn. 3 calculates fractional loss. Since the slope
# of the line in Figure 7 is the same as C1 in Eqn. 3, it is assumed that
# Eqn. 3 calculates fractional loss.
loss_fraction = (
C1
* effective_snow_weighted
* cosd(surface_tilt)**2
* ground_interference_term
* relative_humidity_fraction
/ temp_air_kelvin**2
/ poa_global_kWh**0.67
* string_factor
)
return np.clip(loss_fraction, 0, 1)