Skip to content
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

Townsend snow (building on #1251) #1468

Merged
merged 29 commits into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0b736d4
first experimental commit
Jun 25, 2021
3f2c40d
Added numpy array support
Jul 2, 2021
974d516
changed var names
Jul 16, 2021
655fa79
changed townsend_Se to private
Jul 20, 2021
c0c8b4d
removed snow.loss_townsend in api.rst
abhisheksparikh Mar 17, 2022
f0e1f3a
Merge branch 'master' of https://github.com/abhisheksparikh/pvlib-pyt…
abhisheksparikh Mar 17, 2022
78716f7
added loss_townsend description in effects_on_pv_system_output.rst
abhisheksparikh Mar 17, 2022
5e06a40
fixed stickler checks
abhisheksparikh Mar 17, 2022
a5c5315
removed rounding of loss and changed to 0-1 range
abhisheksparikh Mar 18, 2022
f79e906
Merge branch 'master' into townsend_snow
abhisheksparikh Mar 18, 2022
f7ba1ad
Merge branch 'master' of https://github.com/pvlib/pvlib-python into t…
reepoi May 26, 2022
c2d6d4f
implementing changes suggested in PR #1251
reepoi May 26, 2022
68c85f7
removing new line
reepoi May 26, 2022
e5118ef
removing new line
reepoi May 26, 2022
c253f59
remove new line
reepoi Jun 7, 2022
e2c1f76
Se to effective_snow
reepoi Jun 7, 2022
4783556
adding PR number to whatsnew
reepoi Jun 7, 2022
588115e
address stickler line too long
reepoi Jun 7, 2022
36ae264
remove links and noqa E501, and fix long lines
reepoi Jun 13, 2022
d9dad34
converting to metric system
reepoi Jun 13, 2022
1d5eb88
poa_global from kWh/m2 to Wh/m2
reepoi Jun 13, 2022
bbe5bc3
changing returned loss from kWh to Wh
reepoi Jun 13, 2022
b3ab5aa
fixing capacity loss calculation units to keep correct C1 value
reepoi Jun 14, 2022
cff9345
Merge branch 'master' into townsend_snow_loss
reepoi Jun 16, 2022
b759bcf
convert relative humidity from percent to fraction
reepoi Jun 20, 2022
febe4a2
Merge branch 'townsend_snow_loss' of https://github.com/reepoi/pvlib-…
reepoi Jun 20, 2022
f46fc00
neatening docstrings and adjusting variable names
reepoi Jun 22, 2022
03e7dcf
Merge branch 'master' of github.com:reepoi/pvlib-python into townsend…
reepoi Sep 11, 2022
0d1542f
changing eqn 3 percentage loss to fractional loss and adding comment …
reepoi Sep 11, 2022
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
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.9.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Bug fixes
where passing localized timezones with large UTC offsets could return
rise/set/transit times for the wrong day in recent versions of ``ephem``
(:issue:`1449`, :pull:`1448`)
* Added Townsend-Powers monthly snow loss model: :py:func:`pvlib.snow.loss_townsend`
(:issue:`1246`, :pull:`1251`, :pull:`1468`)


Testing
Expand All @@ -40,3 +42,5 @@ Contributors
* Naman Priyadarshi (:ghuser:`Naman-Priyadarshi`)
* Chencheng Luo (:ghuser:`roger-lcc`)
* Prajwal Borkar (:ghuser:`PrajwalBorkar`)
* Abhishek Parikh (:ghuser:`abhisheksparikh`)
* Taos Transue (:ghuser:`reepoi`)
129 changes: 128 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,130 @@ 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_load, snow_events):
'''
Calculates effective snow using the total snowfall in inches received
each month and the number of snowfall events each month.

Parameters
----------
snow_load : array-like
Inches of snow received each month. Referred to as S in the paper [in]

snow_events : array-like
Number of snowfall events each month. Referred to as N in the paper [-]

Returns
-------
effective_snowfall : array-like
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: E501
snow_events_no_zeros = np.maximum(snow_events, 1)
effective_snow = 0.5 * snow_load * (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_drop_height,
angle_of_repose=40):
'''
Calculates monthly snow loss based on the Townsend monthly snow loss model [1]_.

Parameters
----------
snow_total : array-like
Inches of snow received each month. Referred to as S in the paper [in]

snow_events : array-like
Number of snowfall events each month. Referred to as N in the paper [-]

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 [kWh/m2/month]

slant_height : float
Row length in the slanted plane of array dimension [in]

lower_edge_drop_height : float
Drop height from array edge to ground [in]

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. 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: E501

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
)

drop_height_clipped = np.maximum(lower_edge_drop_height, 0.01)
gamma = (
slant_height
* effective_snow_weighted
* cosd(surface_tilt)
/ (drop_height_clipped**2 - effective_snow_weighted**2)
* 2
* tand(angle_of_repose)
)

ground_interference_term = 1 - C2 * np.exp(-gamma)
temp_air_kelvin = temp_air + 273.15
loss_percentage = (
C1
* effective_snow_weighted
* cosd(surface_tilt)**2
* ground_interference_term
* relative_humidity
/ temp_air_kelvin**2
/ poa_global**0.67
)

return loss_percentage / 100
29 changes: 29 additions & 0 deletions pvlib/tests/test_snow.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,32 @@ 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_load = 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])
expected = np.array([7.5, 7.5, 5, 0, 0, 0, 0, 0, 0, 0, 3.75, 6.66666667])
actual = snow._townsend_effective_snow(snow_load, snow_events)
np.testing.assert_allclose(expected, actual, rtol=1e-07)


def test_loss_townsend():
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])
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([350, 350, 350, 350, 350, 350, 350, 350, 350, 350,
350, 350])
angle_of_repose = 40
slant_height = 100
lower_edge_drop_height = 10
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_drop_height, angle_of_repose)
np.testing.assert_allclose(expected, actual, rtol=1e-05)