-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Snow coverage model from SAM #764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
bea249f
a931096
ca285d7
f7acc74
9a7964b
67cff8b
97565bf
78ac66a
a2ccd92
4d65294
bcd8c37
8edfffe
cf71e2a
43d21cd
1b5dcb1
7b7edfe
c5ac03b
b9f1988
04a0b15
109fa7d
436c8d1
92d693c
b5a9fb9
7116cd2
54f7a5e
63c2f58
c766e9a
4c25f2a
6c6c663
7689ef6
47c26da
ff2ca66
3a04ccd
10cd8f2
733e489
2f6376e
16bdde9
c4091cc
1ac666d
0b6236a
ad48714
edb1b1e
8f59d8b
0352814
d970d20
6fa5c95
81f2c85
2e13c1b
62f5df6
2858799
539402c
04083bf
3052442
bf79511
f5f776a
81f78d5
f1b0c5a
b75909c
724ac68
cabc6c8
fc179a4
468e42d
ee7ef7a
53d4654
743a4fb
9d7683e
bf29887
6bfc18b
029cf20
dac54de
1b6e62d
e6c33dd
2305603
c0fda4f
4250d71
4f6584a
f6b0e41
bcbd29f
bef5866
8792b60
69e340f
6a48f94
54d535b
b5bdcce
ad12c56
35daed1
673490c
c52bc77
dd72dcd
06de0aa
2be01f0
3b6b7db
7b9f922
92adcfc
cb131d6
02a604d
ae18625
0b0f097
dd504f6
c20d443
618fe26
b5ab200
e981839
e124175
8da7354
18c475f
13500d2
08f5f19
1200266
578a70f
1c12459
6c2372f
551331a
0c9f848
8343212
3661d86
f3a2eec
e80763c
b92efee
da22506
238acc6
7ee7cc6
76309cc
ea286cb
4104062
1441d51
23e9ef7
02926a5
f3d286c
f70377b
5788223
a341fd2
891158a
67f6537
00bda7a
7524ba8
52ff419
649a6a5
d183fae
06a3ae7
380a576
29e1772
1c2ad4f
87288cf
9eb70a9
bf42c8f
82e94a4
b6919b2
14e1390
21e1086
04ded05
4aaa6f1
fc68b91
af1a2ac
aa6bbba
4989c02
a41561b
31ed477
e37eda0
86599f4
21c6813
bf66c37
ae476ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,146 @@ | ||||||
""" | ||||||
The ``snow`` module contains functions that model the effect of snow on | ||||||
solar modules. | ||||||
""" | ||||||
|
||||||
import numpy as np | ||||||
import pandas as pd | ||||||
from pvlib.tools import sind | ||||||
|
||||||
|
||||||
def _time_delta_in_hours(times): | ||||||
delta = times.to_series().diff() | ||||||
return delta.dt.seconds.div(3600) | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
|
||||||
def fully_covered(snowfall, threshold=1.): | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
''' | ||||||
Calculates the timesteps when the row's slant height is fully covered | ||||||
by snow. | ||||||
|
||||||
Parameters | ||||||
---------- | ||||||
snowfall : Series | ||||||
Accumulated snowfall in each time period [cm] | ||||||
|
||||||
threshold : float, default 1.0 | ||||||
Minimum hourly snowfall to cover a row's slant height. [cm/hr] | ||||||
|
||||||
Returns | ||||||
---------- | ||||||
boolean: Series | ||||||
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]_. | ||||||
|
||||||
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 | ||||||
''' | ||||||
delta = snowfall.index.to_series().diff() # [0] will be NaT | ||||||
timestep = delta.dt.seconds.div(3600) # convert to hours | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
time_adjusted = snowfall / timestep | ||||||
time_adjusted.iloc[0] = 0 # replace NaN from NaT / timestep | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
return time_adjusted >= threshold | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
|
||||||
def snow_coverage_nrel(snowfall, poa_irradiance, temperature, surface_tilt, | ||||||
threshold_snowfall=1., m=-80, | ||||||
sliding_coefficient=0.197): | ||||||
''' | ||||||
Calculates the fraction of the slant height of a row of modules covered by | ||||||
snow at every time step. | ||||||
|
||||||
Parameters | ||||||
---------- | ||||||
snowfall : Series | ||||||
Accumulated snowfall at the end of each time period. [cm] | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
poa_irradiance : Series | ||||||
Total in-plane irradiance [W/m^2] | ||||||
temperature : Series | ||||||
Ambient air temperature at the surface [C] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think "at the surface" will cause more confusion that clarity. I don't think we use this phrase anywhere else in the library. |
||||||
surface_tilt : numeric | ||||||
Tilt of module's from horizontal, e.g. surface facing up = 0, | ||||||
surface facing horizon = 90. Must be between 0 and 180. [degrees] | ||||||
threshold_snowfall : float, default 1.0 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same thing as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, both |
||||||
Minimum hourly snowfall to cover a row's slant height. [cm/hr] | ||||||
m : numeric | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
A coefficient used in the model described in [1]_. [W/(m^2 C)] | ||||||
sliding coefficient : numeric | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
Empirically determined coefficient used in [1]_ to determine how much | ||||||
snow slides off in each time period. [unitless] | ||||||
|
||||||
Returns | ||||||
------- | ||||||
snow_coverage : numeric | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
The fraction of a the slant height of a row of modules that is covered | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
by snow at each time step. | ||||||
|
||||||
Notes | ||||||
----- | ||||||
Initial snow coverage is assumed to be zero. Implements the model described | ||||||
in [1]_ with minor improvements in [2]_. Validated for fixed tilt systems. | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
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. | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
.. [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 | ||||||
''' | ||||||
|
||||||
# set up output Series | ||||||
snow_coverage = pd.Series(index=poa_irradiance.index, data=np.nan) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
snow_events = snowfall[fully_covered(snowfall, threshold_snowfall)] | ||||||
|
||||||
can_slide = temperature > poa_irradiance / m | ||||||
slide_amt = sliding_coefficient * sind(surface_tilt) * \ | ||||||
_time_delta_in_hours(poa_irradiance.index) | ||||||
|
||||||
uncovered = pd.Series(0.0, index=poa_irradiance.index) | ||||||
uncovered[can_slide] = slide_amt[can_slide] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not immediately obvious to me what's happening here and why the variable is called |
||||||
|
||||||
windows = [(ev, ne) for (ev, ne) in | ||||||
zip(snow_events.index[:-1], snow_events.index[1:])] | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
# add last time window | ||||||
windows.append((snow_events.index[-1], snowfall.index[-1])) | ||||||
|
||||||
for (ev, ne) in windows: | ||||||
filt = (snow_coverage.index > ev) & (snow_coverage.index <= ne) | ||||||
snow_coverage[ev] = 1.0 | ||||||
snow_coverage[filt] = 1.0 - uncovered[filt].cumsum() | ||||||
|
||||||
# clean up periods where row is completely uncovered | ||||||
snow_coverage[snow_coverage < 0] = 0 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
snow_coverage = snow_coverage.fillna(value=0.) | ||||||
return snow_coverage | ||||||
|
||||||
|
||||||
def snow_loss_factor(snow_coverage, num_strings): | ||||||
''' | ||||||
Calculates the DC loss due to snow coverage. Assumes that if a string is | ||||||
partially covered by snow, it produces 0W. | ||||||
|
||||||
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. | ||||||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
''' | ||||||
return np.ceil(snow_coverage * num_strings) / num_strings |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import numpy as np | ||
import pandas as pd | ||
|
||
from pandas.util.testing import assert_series_equal | ||
|
||
from pvlib import snowcoverage | ||
from pvlib.tools import sind | ||
|
||
|
||
def test_fully_covered(): | ||
dt = pd.date_range(start="2019-1-1 12:00:00", end="2019-1-1 18:00:00", | ||
freq='1h') | ||
snowfall_data = pd.Series([1, 5, .6, 4, .23, -5, 19], index=dt) | ||
expected = pd.Series([False, True, False, True, False, False, True], | ||
index=dt) | ||
actual_snowfall = snowcoverage.fully_covered(snowfall_data) | ||
assert_series_equal(actual_snowfall, expected) | ||
|
||
|
||
def test_snow_coverage_nrel(): | ||
surface_tilt = 45 | ||
sliding_coefficient = 0.197 | ||
dt = pd.date_range(start="2019-1-1 10:00:00", end="2019-1-1 17:00:00", | ||
freq='1h') | ||
poa_irradiance = pd.Series([400, 200, 100, 1234, 134, 982, 100, 100], index=dt) | ||
temperature = pd.Series([10, 2, 10, 1234, 34, 982, 10, 10], index=dt) | ||
slide_amt = sliding_coefficient * sind(surface_tilt) | ||
snowfall_data = pd.Series([1, .5, .6, .4, .23, -5, .1, .1], index=dt) | ||
snow_coverage = snowcoverage.snow_coverage_nrel( | ||
snowfall_data, poa_irradiance, temperature, surface_tilt, | ||
threshold_snowfall=0.0) | ||
# covered every hour except when snowfall = -5 | ||
expected = pd.Series([1., 1., 1., 1., 1., 1. - slide_amt, 1., 1.], index=dt) | ||
assert_series_equal(snow_coverage, expected) | ||
# snowfall_threshold = 0.6 | ||
snow_coverage = snowcoverage.snow_coverage_nrel( | ||
snowfall_data, poa_irradiance, temperature, surface_tilt, | ||
threshold_snowfall=0.6) | ||
# covered every hour except when snowfall = -5 | ||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
covered = np.append(np.array([0., 0.]), | ||
1.0 - slide_amt * np.array([0, 1, 2, 3, 4, 5])) | ||
expected = pd.Series(covered, index=dt) | ||
assert_series_equal(snow_coverage, expected) | ||
|
||
|
||
def test_snow_loss_factor(): | ||
num_strings = 8 | ||
snow_coverage = pd.Series([1, 1, .5, .6, .2, .4, 0]) | ||
expected = pd.Series([1, 1, .5, .625, .25, .5, 0]) | ||
actual = snowcoverage.snow_loss_factor(snow_coverage, num_strings) | ||
assert_series_equal(expected, actual) |
Uh oh!
There was an error while loading. Please reload this page.