Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Snow
snow.coverage_nrel
snow.fully_covered_nrel
snow.dc_loss_nrel
snow.loss_townsend

Soiling
-------
Expand Down
104 changes: 104 additions & 0 deletions pvlib/snow.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,107 @@ def dc_loss_nrel(snow_coverage, num_strings):
Available at https://www.nrel.gov/docs/fy18osti/67399.pdf
'''
return np.ceil(snow_coverage * num_strings) / num_strings


def _townsend_Se(S, N):
'''
Calculates effective snow for a given month based upon the total snowfall
received in a month in inches and the number of events where snowfall is
greater than 1 inch

Parameters
----------
S : numeric
Snowfall in inches received in a month

N: numeric
Number of snowfall events with snowfall > 1"

Returns
-------
effective_snowfall : numeric
Effective snowfall as defined in the townsend model

References
----------
.. [1] Townsend, Tim & Powers, Loren. (2011). Photovoltaics and snow: An
update from two winters of measurements in the SIERRA. Conference
Record of the IEEE Photovoltaic Specialists Conference.
003231-003236. :doi:`10.1109/PVSC.2011.6186627`
Available at https://www.researchgate.net/publication/261042016_Photovoltaics_and_snow_An_update_from_two_winters_of_measurements_in_the_SIERRA

'''# noqa
return(np.where(N > 0, 0.5 * S * (1 + 1/N), 0))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return(np.where(N > 0, 0.5 * S * (1 + 1/N), 0))
return np.where(N > 0, 0.5 * S * (1 + 1/N), 0)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised stickler doesn't complain about this.



def loss_townsend(snow_total, snow_events, tilt, relative_humidity, temp_air,
poa_global, row_len, H, P=40):
'''
Calculates monthly snow loss based on a generalized monthly snow loss model
discussed in [1]_.
Comment on lines +225 to +226
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Calculates monthly snow loss based on the Townsend monthly snow loss model [1]_." or Townsend-Powers.


Parameters
----------
snow_total : numeric
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our current definition of numeric includes scalars (e.g. float), which I don't think makes sense here. Would array-like make sense? See https://pvlib-python.readthedocs.io/en/stable/contributing.html#documentation

Inches of snow received in the current month. Referred as S in the
paper

snow_events : numeric
Number of snowfall events with snowfall > 1". Referred as N in the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this > 1" requirement in the reference? I'm not seeing it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope - it isn't, hence removing it. I guess it is better to leave that up to the user (to define what a snow event is).

paper

tilt : numeric
Array tilt in degrees

relative_humidity : numeric
Relative humidity in percentage

temp_air : numeric
Ambient temperature [C]

poa_global : numeric
Plane of array insolation in kWh/m2/month

row_len : float
Row length in the slanted plane of array dimension in inches

H : float
Drop height from array edge to ground in inches

P : float
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This P slipped through the cracks :)

piled snow angle, assumed to stabilize at 40° , the midpoint of
25°-55° avalanching slope angles

Returns
-------
loss : numeric
Average monthly DC capacity loss in percentage due to snow coverage

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a Notes section here pointing out that this model has not been validated for tracking arrays, but the reference suggests using the maximum rotation angle in place of surface_tilt.

References
----------
.. [1] Townsend, Tim & Powers, Loren. (2011). Photovoltaics and snow: An
update from two winters of measurements in the SIERRA. Conference
Record of the IEEE Photovoltaic Specialists Conference.
003231-003236. 10.1109/PVSC.2011.6186627.
Available at https://www.researchgate.net/publication/261042016_Photovoltaics_and_snow_An_update_from_two_winters_of_measurements_in_the_SIERRA
'''# noqa

C1 = 5.7e04
C2 = 0.51

snow_total_prev = np.roll(snow_total, 1)
snow_events_prev = np.roll(snow_events, 1)

Se = _townsend_Se(snow_total, snow_events)
Se_prev = _townsend_Se(snow_total_prev, snow_events_prev)

Se_weighted = 1/3 * Se_prev + 2/3 * Se
Comment on lines +280 to +283
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Se = _townsend_Se(snow_total, snow_events)
Se_prev = _townsend_Se(snow_total_prev, snow_events_prev)
Se_weighted = 1/3 * Se_prev + 2/3 * Se
effective_snow = _townsend_Se(snow_total, snow_events)
effective_snow_prev = _townsend_Se(snow_total_prev, snow_events_prev)
effective_snow_weighted = 1/3 * effective_snow_prev + 2/3 * effective_snow

gamma = (row_len * Se_weighted * np.cos(np.deg2rad(tilt))) / \
(np.clip((H**2 - Se_weighted**2), a_min=0.01, a_max=None) / 2 /
np.tan(np.deg2rad(P)))

GIT = 1 - C2 * np.exp(-gamma)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also change this to ground_interference_term and use more lines in the equation below.

loss = C1 * Se_weighted * (np.cos(np.deg2rad(tilt)))**2 * GIT * \
relative_humidity / (temp_air+273.15)**2 / poa_global**0.67

return (np.round(loss, 2))
27 changes: 27 additions & 0 deletions pvlib/tests/test_snow.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,30 @@ def test_dc_loss_nrel():
expected = pd.Series([1, 1, .5, .625, .25, .5, 0])
actual = snow.dc_loss_nrel(snow_coverage, num_strings)
assert_series_equal(expected, actual)


def test__townsend_Se():
S = np.array([10, 10, 5, 1, 0, 0, 0, 0, 0, 0, 5, 10])
N = np.array([2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3])
expected = np.array([7.5, 7.5, 5, 0, 0, 0, 0, 0, 0, 0, 3.75, 6.66666667])
actual = snow._townsend_Se(S, N)
np.testing.assert_allclose(expected, actual, rtol=1e-07)


def test_loss_townsend():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question for @cwhanse: referring to #1393 (comment), should we have a policy of always testing both array and Series for array-like parameters?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we have a policy of always testing both array and Series for array-like parameters?

I think so

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say it's good practice but importance and extent depends on the model and implementation. I'm usually more concerned that we test for compatibility with scalars in "float, array, series" situations because I think it's easier for regressions to slip in with scalars (e.g. by introducing masking).

snow_total = np.array([10, 10, 5, 1, 0, 0, 0, 0, 0, 0, 5, 10])
snow_events = np.array([2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3])
tilt = 20
relative_humidity = np.array([80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
80, 80])
temp_air = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
poa_global = np.array([350, 350, 350, 350, 350, 350, 350, 350, 350, 350,
350, 350])
P = 40
row_len = 100
H = 10
expected = np.array([7.7, 7.99, 6.22, 1.72, 0, 0, 0, 0, 0, 0, 2.64, 6.07])
actual = snow.loss_townsend(snow_total, snow_events, tilt,
relative_humidity, temp_air,
poa_global, row_len, H, P)
np.testing.assert_allclose(expected, actual, rtol=1e-07)