Skip to content

Commit

Permalink
Townsend snow (building on #1251) (#1468)
Browse files Browse the repository at this point in the history
* first experimental commit

* Added numpy array support

* changed var names

* changed townsend_Se to private

* removed snow.loss_townsend in api.rst

* added loss_townsend description in effects_on_pv_system_output.rst

* fixed stickler checks

* removed rounding of loss and changed to 0-1 range

Several other small changes - variable names change, comment change - in response to Kevin's review notes

* implementing changes suggested in PR #1251

* removing new line

* removing new line

* remove new line

* Se to effective_snow

* adding PR number to whatsnew

* address stickler line too long

* remove links and noqa E501, and fix long lines

* converting to metric system

* poa_global from kWh/m2 to Wh/m2

* changing returned loss from kWh to Wh

* fixing capacity loss calculation units to keep correct C1 value

* convert relative humidity from percent to fraction

* neatening docstrings and adjusting variable names

* changing eqn 3 percentage loss to fractional loss and adding comment explanation

Co-authored-by: Abhishek Parikh <abhishek.parikh2@dnv.com>
Co-authored-by: abhisheksparikh <abhi.parikh305@gmail.com>
  • Loading branch information
3 people authored Sep 12, 2022
1 parent ac2cb4b commit 875aa10
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 1 deletion.
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
5 changes: 5 additions & 0 deletions docs/sphinx/source/whatsnew/v0.9.3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Enhancements
* Low resolution altitude lookup map
:py:func:`~pvlib.location.lookup_altitude`
(:issue:`1516`, :pull:`1518`)
* Added Townsend-Powers monthly snow loss model:
:py:func:`pvlib.snow.loss_townsend`
(:issue:`1246`, :pull:`1251`, :pull:`1468`)

Bug fixes
~~~~~~~~~
Expand All @@ -37,3 +40,5 @@ Contributors
~~~~~~~~~~~~
* João Guilherme (:ghuser:`joaoguilhermeS`)
* Nicolas Martinez (:ghuser:`nicomt`)
* Abhishek Parikh (:ghuser:`abhisheksparikh`)
* Taos Transue (:ghuser:`reepoi`)
139 changes: 138 additions & 1 deletion pvlib/snow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import numpy as np
import pandas as pd
from pvlib.tools import sind
from pvlib.tools import sind, cosd, tand


def _time_delta_in_hours(times):
Expand Down Expand Up @@ -185,3 +185,140 @@ 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_effective_snow(snow_total, snow_events):
'''
Calculates effective snow using the total snowfall received each month and
the number of snowfall events each month.
Parameters
----------
snow_total : array-like
Snow received each month. Referred to as S in [1]_. [cm]
snow_events : array-like
Number of snowfall events each month. Referred to as N in [1]_. [-]
Returns
-------
effective_snowfall : array-like
Effective snowfall as defined in the Townsend model. [cm]
References
----------
.. [1] Townsend, Tim & Powers, Loren. (2011). Photovoltaics and snow: An
update from two winters of measurements in the SIERRA. 37th IEEE
Photovoltaic Specialists Conference, Seattle, WA, USA.
:doi:`10.1109/PVSC.2011.6186627`
'''
snow_events_no_zeros = np.maximum(snow_events, 1)
effective_snow = 0.5 * snow_total * (1 + 1 / snow_events_no_zeros)
return np.where(snow_events > 0, effective_snow, 0)


def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
temp_air, poa_global, slant_height, lower_edge_height,
angle_of_repose=40):
'''
Calculates monthly snow loss based on the Townsend monthly snow loss
model [1]_.
Parameters
----------
snow_total : array-like
Snow received each month. Referred to as S in [1]_. [cm]
snow_events : array-like
Number of snowfall events each month. Referred to as N in [1]_. [-]
surface_tilt : float
Tilt angle of the array. [deg]
relative_humidity : array-like
Monthly average relative humidity. [%]
temp_air : array-like
Monthly average ambient temperature. [C]
poa_global : array-like
Monthly plane of array insolation. [Wh/m2]
slant_height : float
Row length in the slanted plane of array dimension. [m]
lower_edge_height : float
Distance from array lower edge to the ground. [m]
angle_of_repose : float, default 40
Piled snow angle, assumed to stabilize at 40°, the midpoint of
25°-55° avalanching slope angles. [deg]
Returns
-------
loss : array-like
Monthly average DC capacity loss fraction due to snow coverage.
Notes
-----
This model has not been validated for tracking arrays; however, for
tracking arrays [1]_ 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. 37th IEEE
Photovoltaic Specialists Conference, Seattle, WA, USA.
:doi:`10.1109/PVSC.2011.6186627`
'''

C1 = 5.7e04
C2 = 0.51

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

effective_snow = _townsend_effective_snow(snow_total, snow_events)
effective_snow_prev = _townsend_effective_snow(
snow_total_prev,
snow_events_prev
)
effective_snow_weighted = (
1 / 3 * effective_snow_prev
+ 2 / 3 * effective_snow
)
effective_snow_weighted_m = effective_snow_weighted / 100

lower_edge_height_clipped = np.maximum(lower_edge_height, 0.01)
gamma = (
slant_height
* effective_snow_weighted_m
* cosd(surface_tilt)
/ (lower_edge_height_clipped**2 - effective_snow_weighted_m**2)
* 2
* tand(angle_of_repose)
)

ground_interference_term = 1 - C2 * np.exp(-gamma)
relative_humidity_fraction = relative_humidity / 100
temp_air_kelvin = temp_air + 273.15
effective_snow_weighted_in = effective_snow_weighted / 2.54
poa_global_kWh = poa_global / 1000

# Calculate Eqn. 3 in the reference.
# Although the reference says Eqn. 3 calculates percentage loss, the y-axis
# of Figure 7 indicates Eqn. 3 calculates fractional loss. Since the slope
# of the line in Figure 7 is the same as C1 in Eqn. 3, it is assumed that
# Eqn. 3 calculates fractional loss.
loss_fraction = (
C1
* effective_snow_weighted_in
* cosd(surface_tilt)**2
* ground_interference_term
* relative_humidity_fraction
/ temp_air_kelvin**2
/ poa_global_kWh**0.67
)

return np.clip(loss_fraction, 0, 1)
32 changes: 32 additions & 0 deletions pvlib/tests/test_snow.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,35 @@ 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_effective_snow():
snow_total = np.array([25.4, 25.4, 12.7, 2.54, 0, 0, 0, 0, 0, 0, 12.7,
25.4])
snow_events = np.array([2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3])
expected = np.array([19.05, 19.05, 12.7, 0, 0, 0, 0, 0, 0, 0, 9.525,
254 / 15])
actual = snow._townsend_effective_snow(snow_total, snow_events)
np.testing.assert_allclose(expected, actual, rtol=1e-07)


def test_loss_townsend():
snow_total = np.array([25.4, 25.4, 12.7, 2.54, 0, 0, 0, 0, 0, 0, 12.7,
25.4])
snow_events = np.array([2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3])
surface_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([350000, 350000, 350000, 350000, 350000, 350000,
350000, 350000, 350000, 350000, 350000, 350000])
angle_of_repose = 40
slant_height = 2.54
lower_edge_height = 0.254
expected = np.array([0.07696253, 0.07992262, 0.06216201, 0.01715392, 0, 0,
0, 0, 0, 0, 0.02643821, 0.06068194])
actual = snow.loss_townsend(snow_total, snow_events, surface_tilt,
relative_humidity, temp_air,
poa_global, slant_height,
lower_edge_height, angle_of_repose)
np.testing.assert_allclose(expected, actual, rtol=1e-05)

0 comments on commit 875aa10

Please sign in to comment.