Skip to content

Commit 875aa10

Browse files
reepoiAbhishek Parikhabhisheksparikh
authored
Townsend snow (building on #1251) (#1468)
* 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 <[email protected]> Co-authored-by: abhisheksparikh <[email protected]>
1 parent ac2cb4b commit 875aa10

File tree

4 files changed

+176
-1
lines changed

4 files changed

+176
-1
lines changed

docs/sphinx/source/reference/effects_on_pv_system_output.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Snow
2121
snow.coverage_nrel
2222
snow.fully_covered_nrel
2323
snow.dc_loss_nrel
24+
snow.loss_townsend
2425

2526
Soiling
2627
-------

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ Enhancements
1212
* Low resolution altitude lookup map
1313
:py:func:`~pvlib.location.lookup_altitude`
1414
(:issue:`1516`, :pull:`1518`)
15+
* Added Townsend-Powers monthly snow loss model:
16+
:py:func:`pvlib.snow.loss_townsend`
17+
(:issue:`1246`, :pull:`1251`, :pull:`1468`)
1518

1619
Bug fixes
1720
~~~~~~~~~
@@ -37,3 +40,5 @@ Contributors
3740
~~~~~~~~~~~~
3841
* João Guilherme (:ghuser:`joaoguilhermeS`)
3942
* Nicolas Martinez (:ghuser:`nicomt`)
43+
* Abhishek Parikh (:ghuser:`abhisheksparikh`)
44+
* Taos Transue (:ghuser:`reepoi`)

pvlib/snow.py

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import numpy as np
77
import pandas as pd
8-
from pvlib.tools import sind
8+
from pvlib.tools import sind, cosd, tand
99

1010

1111
def _time_delta_in_hours(times):
@@ -185,3 +185,140 @@ def dc_loss_nrel(snow_coverage, num_strings):
185185
Available at https://www.nrel.gov/docs/fy18osti/67399.pdf
186186
'''
187187
return np.ceil(snow_coverage * num_strings) / num_strings
188+
189+
190+
def _townsend_effective_snow(snow_total, snow_events):
191+
'''
192+
Calculates effective snow using the total snowfall received each month and
193+
the number of snowfall events each month.
194+
195+
Parameters
196+
----------
197+
snow_total : array-like
198+
Snow received each month. Referred to as S in [1]_. [cm]
199+
200+
snow_events : array-like
201+
Number of snowfall events each month. Referred to as N in [1]_. [-]
202+
203+
Returns
204+
-------
205+
effective_snowfall : array-like
206+
Effective snowfall as defined in the Townsend model. [cm]
207+
208+
References
209+
----------
210+
.. [1] Townsend, Tim & Powers, Loren. (2011). Photovoltaics and snow: An
211+
update from two winters of measurements in the SIERRA. 37th IEEE
212+
Photovoltaic Specialists Conference, Seattle, WA, USA.
213+
:doi:`10.1109/PVSC.2011.6186627`
214+
'''
215+
snow_events_no_zeros = np.maximum(snow_events, 1)
216+
effective_snow = 0.5 * snow_total * (1 + 1 / snow_events_no_zeros)
217+
return np.where(snow_events > 0, effective_snow, 0)
218+
219+
220+
def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
221+
temp_air, poa_global, slant_height, lower_edge_height,
222+
angle_of_repose=40):
223+
'''
224+
Calculates monthly snow loss based on the Townsend monthly snow loss
225+
model [1]_.
226+
227+
Parameters
228+
----------
229+
snow_total : array-like
230+
Snow received each month. Referred to as S in [1]_. [cm]
231+
232+
snow_events : array-like
233+
Number of snowfall events each month. Referred to as N in [1]_. [-]
234+
235+
surface_tilt : float
236+
Tilt angle of the array. [deg]
237+
238+
relative_humidity : array-like
239+
Monthly average relative humidity. [%]
240+
241+
temp_air : array-like
242+
Monthly average ambient temperature. [C]
243+
244+
poa_global : array-like
245+
Monthly plane of array insolation. [Wh/m2]
246+
247+
slant_height : float
248+
Row length in the slanted plane of array dimension. [m]
249+
250+
lower_edge_height : float
251+
Distance from array lower edge to the ground. [m]
252+
253+
angle_of_repose : float, default 40
254+
Piled snow angle, assumed to stabilize at 40°, the midpoint of
255+
25°-55° avalanching slope angles. [deg]
256+
257+
Returns
258+
-------
259+
loss : array-like
260+
Monthly average DC capacity loss fraction due to snow coverage.
261+
262+
Notes
263+
-----
264+
This model has not been validated for tracking arrays; however, for
265+
tracking arrays [1]_ suggests using the maximum rotation angle in place
266+
of ``surface_tilt``.
267+
268+
References
269+
----------
270+
.. [1] Townsend, Tim & Powers, Loren. (2011). Photovoltaics and snow: An
271+
update from two winters of measurements in the SIERRA. 37th IEEE
272+
Photovoltaic Specialists Conference, Seattle, WA, USA.
273+
:doi:`10.1109/PVSC.2011.6186627`
274+
'''
275+
276+
C1 = 5.7e04
277+
C2 = 0.51
278+
279+
snow_total_prev = np.roll(snow_total, 1)
280+
snow_events_prev = np.roll(snow_events, 1)
281+
282+
effective_snow = _townsend_effective_snow(snow_total, snow_events)
283+
effective_snow_prev = _townsend_effective_snow(
284+
snow_total_prev,
285+
snow_events_prev
286+
)
287+
effective_snow_weighted = (
288+
1 / 3 * effective_snow_prev
289+
+ 2 / 3 * effective_snow
290+
)
291+
effective_snow_weighted_m = effective_snow_weighted / 100
292+
293+
lower_edge_height_clipped = np.maximum(lower_edge_height, 0.01)
294+
gamma = (
295+
slant_height
296+
* effective_snow_weighted_m
297+
* cosd(surface_tilt)
298+
/ (lower_edge_height_clipped**2 - effective_snow_weighted_m**2)
299+
* 2
300+
* tand(angle_of_repose)
301+
)
302+
303+
ground_interference_term = 1 - C2 * np.exp(-gamma)
304+
relative_humidity_fraction = relative_humidity / 100
305+
temp_air_kelvin = temp_air + 273.15
306+
effective_snow_weighted_in = effective_snow_weighted / 2.54
307+
poa_global_kWh = poa_global / 1000
308+
309+
# Calculate Eqn. 3 in the reference.
310+
# Although the reference says Eqn. 3 calculates percentage loss, the y-axis
311+
# of Figure 7 indicates Eqn. 3 calculates fractional loss. Since the slope
312+
# of the line in Figure 7 is the same as C1 in Eqn. 3, it is assumed that
313+
# Eqn. 3 calculates fractional loss.
314+
loss_fraction = (
315+
C1
316+
* effective_snow_weighted_in
317+
* cosd(surface_tilt)**2
318+
* ground_interference_term
319+
* relative_humidity_fraction
320+
/ temp_air_kelvin**2
321+
/ poa_global_kWh**0.67
322+
)
323+
324+
return np.clip(loss_fraction, 0, 1)

pvlib/tests/test_snow.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,35 @@ def test_dc_loss_nrel():
9595
expected = pd.Series([1, 1, .5, .625, .25, .5, 0])
9696
actual = snow.dc_loss_nrel(snow_coverage, num_strings)
9797
assert_series_equal(expected, actual)
98+
99+
100+
def test__townsend_effective_snow():
101+
snow_total = np.array([25.4, 25.4, 12.7, 2.54, 0, 0, 0, 0, 0, 0, 12.7,
102+
25.4])
103+
snow_events = np.array([2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3])
104+
expected = np.array([19.05, 19.05, 12.7, 0, 0, 0, 0, 0, 0, 0, 9.525,
105+
254 / 15])
106+
actual = snow._townsend_effective_snow(snow_total, snow_events)
107+
np.testing.assert_allclose(expected, actual, rtol=1e-07)
108+
109+
110+
def test_loss_townsend():
111+
snow_total = np.array([25.4, 25.4, 12.7, 2.54, 0, 0, 0, 0, 0, 0, 12.7,
112+
25.4])
113+
snow_events = np.array([2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3])
114+
surface_tilt = 20
115+
relative_humidity = np.array([80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
116+
80, 80])
117+
temp_air = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
118+
poa_global = np.array([350000, 350000, 350000, 350000, 350000, 350000,
119+
350000, 350000, 350000, 350000, 350000, 350000])
120+
angle_of_repose = 40
121+
slant_height = 2.54
122+
lower_edge_height = 0.254
123+
expected = np.array([0.07696253, 0.07992262, 0.06216201, 0.01715392, 0, 0,
124+
0, 0, 0, 0, 0.02643821, 0.06068194])
125+
actual = snow.loss_townsend(snow_total, snow_events, surface_tilt,
126+
relative_humidity, temp_air,
127+
poa_global, slant_height,
128+
lower_edge_height, angle_of_repose)
129+
np.testing.assert_allclose(expected, actual, rtol=1e-05)

0 commit comments

Comments
 (0)