Skip to content

Commit 551331a

Browse files
committed
rewrite, reduce helpers, remove while loop
1 parent 6c2372f commit 551331a

File tree

3 files changed

+116
-220
lines changed

3 files changed

+116
-220
lines changed

pvlib/snowcoverage.py

Lines changed: 65 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -8,90 +8,33 @@
88
from 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

pvlib/test/test_snowcoverage.py

Lines changed: 0 additions & 76 deletions
This file was deleted.

0 commit comments

Comments
 (0)