88from pvlib .tools import sind
99
1010
11- def _snow_slide_amount (surface_tilt , sliding_coefficient = 1.97 ,
12- time_step = 1 ):
13- '''
14- Calculates the amount of snow that slides off in each time step.
15-
16- Parameters
17- ----------
18- surface_tilt : numeric
19- Surface tilt angles in decimal degrees. Tilt must be >=0 and
20- <=180. The tilt angle is defined as degrees from horizontal
21- (e.g. surface facing up = 0, surface facing horizon = 90).
22-
23-
24- sliding_coefficient : numeric
25- An empirically determined coefficient used in [1] to determine
26- how much snow slides off if a sliding event occurs.
27-
28- time_step: float
29- Period of the data. [hour]
30-
31- Returns
32- ----------
33- slide_amount : numeric
34- The fraction of panel slant height from which snow slides off
35- at each time step, in tenths of the panel's slant height.
36- '''
37-
38- return sliding_coefficient / 10 * sind (surface_tilt ) * time_step
39-
40-
41- def _snow_slide_event (poa_irradiance , temperature ,
42- m = - 80 ):
43- '''
44- Calculates when snow sliding events will occur.
45-
46- Parameters
47- ----------
48- poa_irradiance : numeric
49- Total in-plane irradiance [W/m^2]
50-
51- temperature : numeric
52- Ambient air temperature at the surface [C]
53-
54- m : numeric
55- A coefficient used in the model described in [1]. [W/m^2 C]
56-
57- Returns
58- ----------
59- slide_event : boolean array
60- True if the condiditions are suitable for a snow slide event.
61- False elsewhere.
62- '''
63-
64- return temperature > poa_irradiance / m
11+ def _time_delta_in_hours (times ):
12+ delta = times .to_series ().diff ()
13+ return delta .dt .seconds .div (3600 )
6514
6615
67- def fully_covered_panel (snow_data , time_step = 1 ,
68- snow_data_type = "snowfall" ):
16+ def fully_covered (snowfall , threshold = 1. ):
6917 '''
70- Calculates the timesteps when the panel is presumed to be fully covered
18+ Calculates the timesteps when the row's slant height is fully covered
7119 by snow.
7220
7321 Parameters
7422 ----------
75- snow_data : numeric
76- Time series data on either snowfall (cm/hr) or ground snow depth (cm).
77- The type of data should be specified in snow_data_type.
23+ snowfall : Series
24+ Accumulated snowfall in each time period [cm]
7825
79- time_step: float
80- Period of the data. [hour]
81-
82- snow_data_type : string
83- Defines what type of data is being passed as snow_data. Acceptable
84- values are "snowfall" and "snow_depth".
26+ threshold : float, default 1.0
27+ Minimum hourly snowfall to cover a row's slant height. [cm/hr]
8528
8629 Returns
8730 ----------
88- fully_covered_mask : boolean array
31+ boolean: Series
8932 True where the snowfall exceeds the defined threshold to fully cover
90- the panel. False elsewhere.
33+ the panel.
9134
9235 Notes
9336 -----
94- Implements the model described in [1] with minor improvements in [2].
37+ Implements the model described in [1]_ with minor improvements in [2]_ .
9538
9639 References
9740 ----------
@@ -101,60 +44,38 @@ def fully_covered_panel(snow_data, time_step=1,
10144 .. [2] Ryberg, D; Freeman, J. "Integration, Validation, and Application
10245 of a PV Snow Coverage Model in SAM" (2017) NREL Technical Report
10346 '''
104- if snow_data_type == "snow_depth" :
105- prev_data = snow_data .shift (1 )
106- prev_data [0 ] = 0
107- snowfall = snow_data - prev_data
108- elif snow_data_type == "snowfall" :
109- snowfall = snow_data
110- else :
111- raise ValueError ('snow_data_type was not specified or was not set to a'
112- 'valid option (snowfall, snow_depth).' )
113-
114- time_adjusted = snowfall / time_step
115- fully_covered_mask = time_adjusted >= 1
116- return fully_covered_mask
117-
118-
119- def snow_coverage_model (snow_data , snow_data_type ,
120- poa_irradiance , temperature , surface_tilt ,
121- time_step = 1 , m = - 80 , sliding_coefficient = 1.97 ):
47+ delta = snowfall .index .to_series ().diff () # [0] will be NaT
48+ timestep = delta .dt .seconds .div (3600 ) # convert to hours
49+ time_adjusted = snowfall / timestep
50+ time_adjusted .iloc [0 ] = 0 # replace NaN from NaT / timestep
51+ return time_adjusted >= threshold
52+
53+
54+ def snow_coverage_nrel (snowfall , poa_irradiance , temperature , surface_tilt ,
55+ threshold_snowfall = 1. , m = - 80 ,
56+ sliding_coefficient = 0.197 ):
12257 '''
12358 Calculates the fraction of the slant height of a row of modules covered by
12459 snow at every time step.
12560
12661 Parameters
12762 ----------
128- snow_data : Series
129- Time series data on either snowfall or ground snow depth. The type of
130- data should be specified in snow_data_type. The original model was
131- designed for ground snowdepth only. (cm/hr or cm)
132-
133- snow_data_type : string
134- Defines what type of data is being passed as snow_data. Acceptable
135- values are "snowfall" and "snow_depth". "snowfall" will be in units of
136- cm/hr. "snow_depth" is in units of cm.
137-
63+ snowfall : Series
64+ Accumulated snowfall at the end of each time period. [cm]
13865 poa_irradiance : Series
139- Total in-plane irradiance (W/m^2)
140-
66+ Total in-plane irradiance [W/m^2]
14167 temperature : Series
142- Ambient air temperature at the surface (C)
143-
68+ Ambient air temperature at the surface [C]
14469 surface_tilt : numeric
145- Surface tilt angles in decimal degrees. Tilt must be >=0 and
146- <=180. The tilt angle is defined as degrees from horizontal
147- (e.g. surface facing up = 0, surface facing horizon = 90).
148-
149- time_step: float
150- Period of the data. [hour]
151-
152- sliding coefficient : numeric
153- Empirically determined coefficient used in [1] to determine how much
154- snow slides off if a sliding event occurs.
155-
70+ Tilt of module's from horizontal, e.g. surface facing up = 0,
71+ surface facing horizon = 90. Must be between 0 and 180. [degrees]
72+ threshold_snowfall : float, default 1.0
73+ Minimum hourly snowfall to cover a row's slant height. [cm/hr]
15674 m : numeric
157- A coefficient used in the model described in [1]. [W/(m^2 C)]
75+ A coefficient used in the model described in [1]_. [W/(m^2 C)]
76+ sliding coefficient : numeric
77+ Empirically determined coefficient used in [1]_ to determine how much
78+ snow slides off in each time period. [unitless]
15879
15980 Returns
16081 -------
@@ -164,50 +85,50 @@ def snow_coverage_model(snow_data, snow_data_type,
16485
16586 Notes
16687 -----
167- Implements the model described in [1] with minor improvements in [2].
168- Currently only validated for fixed tilt systems.
88+ Initial snow coverage is assumed to be zero. Implements the model described
89+ in [1]_ with minor improvements in [2]_. Validated for fixed tilt systems.
16990
17091 References
17192 ----------
17293 .. [1] Marion, B.; Schaefer, R.; Caine, H.; Sanchez, G. (2013).
17394 “Measured and modeled photovoltaic system energy losses from snow for
17495 Colorado and Wisconsin locations.” Solar Energy 97; pp.112-121.
175- .. [2] Ryberg, D; Freeman, J. "Integration, Validation, and Application
176- of a PV Snow Coverage Model in SAM" (2017) NREL Technical Report
96+ .. [2] Ryberg, D; Freeman, J. (2017). "Integration, Validation, and
97+ Application of a PV Snow Coverage Model in SAM" NREL Technical Report
98+ NREL/TP-6A20-68705
17799 '''
178100
179- full_coverage_events = fully_covered_panel (snow_data ,
180- time_step = time_step ,
181- snow_data_type = snow_data_type )
182- snow_coverage = pd .Series (np .full (len (snow_data ), np .nan ))
183- snow_coverage = snow_coverage .reindex (snow_data .index )
184- snow_coverage [full_coverage_events ] = 1
185- slide_events = _snow_slide_event (poa_irradiance , temperature )
186- slide_amount = _snow_slide_amount (surface_tilt , sliding_coefficient ,
187- time_step )
188- slidable_snow = ~ np .isnan (snow_coverage )
189- while (np .any (slidable_snow )):
190- new_slides = np .logical_and (slide_events , slidable_snow )
191- snow_coverage [new_slides ] -= slide_amount
192- new_snow_coverage = snow_coverage .fillna (method = "ffill" , limit = 1 )
193- slidable_snow = np .logical_and (~ np .isnan (new_snow_coverage ),
194- np .isnan (snow_coverage ))
195- slidable_snow = np .logical_and (slidable_snow , new_snow_coverage > 0 )
196- snow_coverage = new_snow_coverage
197-
198- new_slides = np .logical_and (slide_events , slidable_snow )
199- snow_coverage [new_slides ] -= slide_amount
200-
201- snow_coverage = snow_coverage .fillna (method = "ffill" )
202- snow_coverage = snow_coverage .fillna (value = 0 )
101+ # set up output Series
102+ snow_coverage = pd .Series (index = poa_irradiance .index , data = np .nan )
103+ snow_events = snowfall [fully_covered (snowfall , threshold_snowfall )]
104+
105+ can_slide = temperature > poa_irradiance / m
106+ slide_amt = sliding_coefficient * sind (surface_tilt ) * \
107+ _time_delta_in_hours (poa_irradiance .index )
108+
109+ uncovered = pd .Series (0.0 , index = poa_irradiance .index )
110+ uncovered [can_slide ] = slide_amt [can_slide ]
111+
112+ windows = [(ev , ne ) for (ev , ne ) in
113+ zip (snow_events .index [:- 1 ], snow_events .index [1 :])]
114+ # add last time window
115+ windows .append ((snow_events .index [- 1 ], snowfall .index [- 1 ]))
116+
117+ for (ev , ne ) in windows :
118+ filt = (snow_coverage .index > ev ) & (snow_coverage .index <= ne )
119+ snow_coverage [ev ] = 1.0
120+ snow_coverage [filt ] = 1.0 - uncovered [filt ].cumsum ()
121+
122+ # clean up periods where row is completely uncovered
203123 snow_coverage [snow_coverage < 0 ] = 0
124+ snow_coverage = snow_coverage .fillna (value = 0. )
204125 return snow_coverage
205126
206127
207- def DC_loss_factor (snow_coverage , num_strings ):
128+ def snow_loss_factor (snow_coverage , num_strings ):
208129 '''
209130 Calculates the DC loss due to snow coverage. Assumes that if a string is
210- even partially covered by snow, it produces 0W.
131+ partially covered by snow, it produces 0W.
211132
212133 Parameters
213134 ----------
@@ -220,6 +141,6 @@ def DC_loss_factor(snow_coverage, num_strings):
220141 Returns
221142 -------
222143 loss : numeric
223- DC loss due to snow coverage at each time step.
144+ fraction of DC capacity loss due to snow coverage at each time step.
224145 '''
225146 return np .ceil (snow_coverage * num_strings ) / num_strings
0 commit comments