Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
18 changes: 17 additions & 1 deletion pvlib/snow.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,8 @@ def _townsend_effective_snow(snow_total, snow_events):

def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
temp_air, poa_global, slant_height, lower_edge_height,
string_factor=1.0, angle_of_repose=40):
string_factor=1.0, angle_of_repose=40,
front_side_fraction=None):
'''
Calculates monthly snow loss based on the Townsend monthly snow loss
model.
Expand Down Expand Up @@ -293,6 +294,13 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
Piled snow angle, assumed to stabilize at 40°, the midpoint of
25°-55° avalanching slope angles. [deg]

front_side_fraction : numeric or array-like, default None
Optional multiplier applied to the calculated loss fraction. For
bifacial systems, this can be used to scale the snow loss by the
front-side energy fraction from a no-soiling simulation. For example,
use 0.9 when 90% of monthly energy is from the front side and 10% is
from the rear side. If None, no bifacial adjustment is applied. [-]
Comment on lines +297 to +302
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
front_side_fraction : numeric or array-like, default None
Optional multiplier applied to the calculated loss fraction. For
bifacial systems, this can be used to scale the snow loss by the
front-side energy fraction from a no-soiling simulation. For example,
use 0.9 when 90% of monthly energy is from the front side and 10% is
from the rear side. If None, no bifacial adjustment is applied. [-]
front_side_fraction : numeric or array-like, default 1.0
Fraction of monthly energy from front-side insolation (unitless).
Multiplies the calculated loss fraction. For example,
use 0.9 when 90% of monthly energy is from the front side
of a bifacial system and 10% is from the rear side.

Here I think 1.0 is a suitable default.


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Deleting this blank space will resolve the flake8 error (same below)

Suggested change

Returns
-------
loss : array-like
Expand All @@ -310,6 +318,10 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
publication of [1]_, as described in [2]_.
The definition for snow events documented above is based on [3]_.

For bifacial systems, [2]_ recommends including both front-side and
rear-side insolation in ``poa_global``. The resulting loss may then be
scaled by the front-side energy fraction using ``front_side_fraction``.
Comment on lines +322 to +323
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
rear-side insolation in ``poa_global``. The resulting loss may then be
scaled by the front-side energy fraction using ``front_side_fraction``.
rear-side insolation in ``poa_global``. The resulting loss is
scaled by the front-side energy fraction ``front_side_fraction``.


References
----------
.. [1] Townsend, Tim & Powers, Loren. (2011). Photovoltaics and snow: An
Expand Down Expand Up @@ -384,4 +396,8 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity,
* string_factor
)

if front_side_fraction is not None:
front_side_fraction = np.asarray(front_side_fraction)
Comment on lines +399 to +400
Copy link
Copy Markdown
Member

@cwhanse cwhanse May 17, 2026

Choose a reason for hiding this comment

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

I think this if and np.asarray are unnecessary if the default is set to 1.0

loss_fraction = loss_fraction * front_side_fraction

return np.clip(loss_fraction, 0, 1)
24 changes: 24 additions & 0 deletions tests/test_snow.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,27 @@ def test_loss_townsend_cases(poa_global, surface_tilt, slant_height,
poa_global, slant_height, lower_edge_height, string_factor)
actual = np.around(actual * 100)
assert np.allclose(expected, actual)


Comment on lines +250 to +251
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Deleting this blank space will resolve the flake8 error

Suggested change

def test_loss_townsend_front_side_fraction():
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.full(12, 80)
temp_air = np.full(12, 0)
poa_global = np.full(12, 350000)
slant_height = 2.54
lower_edge_height = 0.254
front_side_fraction = 0.9

unadjusted = snow.loss_townsend(
snow_total, snow_events, surface_tilt, relative_humidity, temp_air,
poa_global, slant_height, lower_edge_height)

adjusted = snow.loss_townsend(
snow_total, snow_events, surface_tilt, relative_humidity, temp_air,
poa_global, slant_height, lower_edge_height,
front_side_fraction=front_side_fraction)

np.testing.assert_allclose(adjusted, unadjusted * front_side_fraction)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Adding a new line at the end of the file will resolve this flake8 error

Suggested change
np.testing.assert_allclose(adjusted, unadjusted * front_side_fraction)
np.testing.assert_allclose(adjusted, unadjusted * front_side_fraction)