Skip to content

Commit 4b25801

Browse files
cwhansekandersolar
andauthored
Fix omission in snow_coverage_nrel (#2292)
* add snow_depth inputs * linter * double ticks * review comments, improve some descriptions * whatsnew * clarify threshold_depth * add note about default value * move check for snow depth * comments * update whatsnew * edit whatsnew * Apply suggestions from code review Co-authored-by: Kevin Anderson <[email protected]> * edits --------- Co-authored-by: Kevin Anderson <[email protected]>
1 parent b4916e1 commit 4b25801

File tree

3 files changed

+95
-26
lines changed

3 files changed

+95
-26
lines changed

docs/sphinx/source/whatsnew/v0.11.3.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Breaking Changes
1515

1616
Bug fixes
1717
~~~~~~~~~
18+
* Add a check to :py:func:`~pvlib.snow.fully_covered_nrel` and
19+
:py:func:`~pvlib.snow.coverage_nrel`. The check uses snow depth on the ground
20+
to improve modeling for systems with shallow tilt angles. The check
21+
adds a new, optional parameter snow_depth. (:issue:`1171`, :pull:`2292`)
1822
* Fix a bug in :py:func:`pvlib.bifacial.get_irradiance_poa` which may have yielded non-zero
1923
ground irradiance when the sun was below the horizon. (:issue:`2245`, :pull:`2359`)
2024
* Fix a bug where :py:func:`pvlib.transformer.simple_efficiency` could only be imported
@@ -28,7 +32,6 @@ Bug fixes
2832
of time-zone truth, is the single time-zone setter interface, and its getter
2933
returns an IANA string. (:issue:`2340`, :pull:`2341`)
3034

31-
3235
Deprecations
3336
~~~~~~~~~~~~
3437

pvlib/snow.py

+54-25
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,38 @@ def _time_delta_in_hours(times):
1313
return delta.dt.total_seconds().div(3600)
1414

1515

16-
def fully_covered_nrel(snowfall, threshold_snowfall=1.):
16+
def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1.,
17+
threshold_depth=1.):
1718
'''
18-
Calculates the timesteps when the row's slant height is fully covered
19-
by snow.
19+
Calculates the timesteps when modules are fully covered by snow.
2020
2121
Parameters
2222
----------
23-
snowfall : Series
24-
Accumulated snowfall in each time period [cm]
25-
26-
threshold_snowfall : float, default 1.0
27-
Hourly snowfall above which snow coverage is set to the row's slant
28-
height. [cm/hr]
23+
snowfall: Series
24+
Snowfall in each time period. [cm]
25+
snow_depth: Series, optional
26+
Snow depth on the ground at the beginning of each time period.
27+
Must have the same index as ``snowfall``. [cm]
28+
threshold_snowfall: float, default 1.0
29+
Hourly snowfall above which the row is fully covered for that hour.
30+
[cm/hr]
31+
threshold_depth: float, default 1.0
32+
Snow depth on the ground, above which snow can affect the modules. [cm]
2933
3034
Returns
3135
----------
32-
boolean: Series
33-
True where the snowfall exceeds the defined threshold to fully cover
34-
the panel.
36+
covered: Series
37+
A Series of boolean, True where the snowfall exceeds the defined
38+
threshold to fully cover the panel.
3539
3640
Notes
3741
-----
3842
Implements the model described in [1]_ with minor improvements in [2]_.
3943
44+
``snow_depth`` is used to return `False` (not fully covered) when snow
45+
is less than ``threshold_depth``. This check is described in [2]_ as needed
46+
for systems with low tilt angle.
47+
4048
References
4149
----------
4250
.. [1] Marion, B.; Schaefer, R.; Caine, H.; Sanchez, G. (2013).
@@ -56,15 +64,20 @@ def fully_covered_nrel(snowfall, threshold_snowfall=1.):
5664
hourly_snow_rate.iloc[0] = snowfall.iloc[0] / timedelta
5765
else: # can't infer frequency from index
5866
hourly_snow_rate.iloc[0] = 0 # replaces NaN
59-
return hourly_snow_rate > threshold_snowfall
67+
covered = (hourly_snow_rate > threshold_snowfall)
68+
# no coverage when no snow on the ground
69+
if snow_depth is not None:
70+
covered = covered & (snow_depth >= threshold_depth)
71+
return covered
6072

6173

6274
def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt,
63-
initial_coverage=0, threshold_snowfall=1.,
64-
can_slide_coefficient=-80., slide_amount_coefficient=0.197):
75+
snow_depth=None, initial_coverage=0, threshold_snowfall=1.,
76+
threshold_depth=1., can_slide_coefficient=-80.,
77+
slide_amount_coefficient=0.197):
6578
'''
66-
Calculates the fraction of the slant height of a row of modules covered by
67-
snow at every time step.
79+
Calculates the fraction of the slant height of a row of modules that is
80+
covered by snow at every time step.
6881
6982
Implements the model described in [1]_ with minor improvements in [2]_,
7083
with the change that the output is in fraction of the row's slant height
@@ -74,20 +87,25 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt,
7487
Parameters
7588
----------
7689
snowfall : Series
77-
Accumulated snowfall within each time period. [cm]
90+
Snowfall within each time period. [cm]
7891
poa_irradiance : Series
7992
Total in-plane irradiance [W/m^2]
8093
temp_air : Series
8194
Ambient air temperature [C]
8295
surface_tilt : numeric
8396
Tilt of module's from horizontal, e.g. surface facing up = 0,
8497
surface facing horizon = 90. [degrees]
98+
snow_depth : Series, optional
99+
Snow depth on the ground at the beginning of each time period.
100+
Must have the same index as ``snowfall``. [cm]
85101
initial_coverage : float, default 0
86102
Fraction of row's slant height that is covered with snow at the
87103
beginning of the simulation. [unitless]
88-
threshold_snowfall : float, default 1.0
89-
Hourly snowfall above which snow coverage is set to the row's slant
90-
height. [cm/hr]
104+
threshold_snowfall: float, default 1.0
105+
Hourly snowfall above which the row is fully covered for that hour.
106+
[cm/hr]
107+
threshold_depth: float, default 1.0
108+
Snow depth on the ground, above which snow can affect the modules. [cm]
91109
can_slide_coefficient : float, default -80.
92110
Coefficient to determine if snow can slide given irradiance and air
93111
temperature. [W/(m^2 C)]
@@ -103,8 +121,12 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt,
103121
104122
Notes
105123
-----
106-
In [1]_, `can_slide_coefficient` is termed `m`, and the value of
107-
`slide_amount_coefficient` is given in tenths of a module's slant height.
124+
In [1]_, ``can_slide_coefficient`` is termed `m`, and the value of
125+
``slide_amount_coefficient`` is given in tenths of a module's slant height.
126+
127+
``snow_depth`` is used to set ``snow_coverage`` to 0 (not fully covered)
128+
when snow is less than ``threshold_depth``. This check is described in
129+
[2]_ as needed for systems with low tilt angle.
108130
109131
References
110132
----------
@@ -117,7 +139,8 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt,
117139
'''
118140

119141
# find times with new snowfall
120-
new_snowfall = fully_covered_nrel(snowfall, threshold_snowfall)
142+
new_snowfall = fully_covered_nrel(snowfall, snow_depth, threshold_snowfall,
143+
threshold_depth)
121144

122145
# set up output Series
123146
snow_coverage = pd.Series(np.nan, index=poa_irradiance.index)
@@ -132,6 +155,13 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt,
132155
# don't slide in the interval preceding the snowfall data
133156
slide_amt.iloc[0] = 0
134157

158+
if snow_depth is not None:
159+
# All slides off if snow on the ground is less than threshold_depth.
160+
# Described in [2] to avoid non-sliding snow for low-tilt systems.
161+
# Default threshold_depth of 1cm is from [2[ and SAM's implementation.
162+
# https://github.com/NREL/ssc/issues/1265
163+
slide_amt[snow_depth < threshold_depth] = 1.
164+
135165
# build time series of cumulative slide amounts
136166
sliding_period_ID = new_snowfall.cumsum()
137167
cumulative_sliding = slide_amt.groupby(sliding_period_ID).cumsum()
@@ -143,7 +173,6 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt,
143173
snow_coverage.ffill(inplace=True)
144174
snow_coverage -= cumulative_sliding
145175

146-
# clean up periods where row is completely uncovered
147176
return snow_coverage.clip(lower=0)
148177

149178

tests/test_snow.py

+37
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ def test_fully_covered_nrel():
1919
assert_series_equal(expected, fully_covered)
2020

2121

22+
def test_fully_covered_nrel_with_snow_depth():
23+
dt = pd.date_range(start="2019-1-1 12:00:00", end="2019-1-1 18:00:00",
24+
freq='1h')
25+
snowfall_data = pd.Series([1, 5, .6, 4, .23, -5, 19], index=dt)
26+
snow_depth = pd.Series([0., 1, 6, 6.6, 10.6, 10., -2], index=dt)
27+
expected = pd.Series([False, True, False, True, False, False, False],
28+
index=dt)
29+
fully_covered = snow.fully_covered_nrel(snowfall_data,
30+
snow_depth=snow_depth,
31+
threshold_depth=0.)
32+
assert_series_equal(expected, fully_covered)
33+
34+
2235
def test_coverage_nrel_hourly():
2336
surface_tilt = 45
2437
slide_amount_coefficient = 0.197
@@ -38,6 +51,30 @@ def test_coverage_nrel_hourly():
3851
assert_series_equal(expected, snow_coverage)
3952

4053

54+
def test_coverage_nrel_hourly_with_snow_depth():
55+
surface_tilt = 45
56+
slide_amount_coefficient = 0.197
57+
threshold_depth = 0.5
58+
dt = pd.date_range(start="2019-1-1 10:00:00", end="2019-1-1 18:00:00",
59+
freq='1h')
60+
poa_irradiance = pd.Series([400, 200, 100, 1234, 134, 982, 100, 100, 100],
61+
index=dt)
62+
temp_air = pd.Series([10, 2, 10, 1234, 34, 982, 10, 10, 10], index=dt)
63+
# restarts with new snow on 5th time step
64+
snowfall_data = pd.Series([1, .5, .6, .4, .23, 5., .1, .1, 0.], index=dt)
65+
snow_depth = pd.Series([1, 1, 1, 1, 0, 1, 1, 0, .1], index=dt)
66+
snow_coverage = snow.coverage_nrel(
67+
snowfall_data, poa_irradiance, temp_air, surface_tilt,
68+
snow_depth=snow_depth, threshold_snowfall=0.6,
69+
threshold_depth=threshold_depth)
70+
71+
slide_amt = slide_amount_coefficient * sind(surface_tilt)
72+
covered = 1.0 - slide_amt * np.array([0, 1, 2, 3, 0, 0, 1, 0, 0])
73+
expected = pd.Series(covered, index=dt)
74+
expected[snow_depth < threshold_depth] = 0
75+
assert_series_equal(expected, snow_coverage)
76+
77+
4178
def test_coverage_nrel_subhourly():
4279
surface_tilt = 45
4380
slide_amount_coefficient = 0.197

0 commit comments

Comments
 (0)