From d4a6abdd3c4d19cfdfdc54372b986b0246779da4 Mon Sep 17 00:00:00 2001 From: shethkajal7 Date: Sat, 16 May 2026 23:03:55 -0500 Subject: [PATCH 1/6] Add bifacial scaling to Townsend snow loss Add optional front_side_fraction argument to loss_townsend for bifacial snow loss adjustment. Includes test coverage for the scaling behavior. --- pvlib/snow.py | 18 +++++++++++++++++- tests/test_snow.py | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index ec1fa0dc51..40ad2b74f7 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -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. @@ -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. [-] + Returns ------- loss : array-like @@ -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``. + References ---------- .. [1] Townsend, Tim & Powers, Loren. (2011). Photovoltaics and snow: An @@ -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) + loss_fraction = loss_fraction * front_side_fraction + return np.clip(loss_fraction, 0, 1) diff --git a/tests/test_snow.py b/tests/test_snow.py index 6d8954011e..34e2ecd134 100644 --- a/tests/test_snow.py +++ b/tests/test_snow.py @@ -247,3 +247,29 @@ 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) + + +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.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]) + 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) \ No newline at end of file From 88246d0092539794d38cdd28e8c8c585183df9b9 Mon Sep 17 00:00:00 2001 From: shethkajal7 Date: Mon, 18 May 2026 16:00:15 -0500 Subject: [PATCH 2/6] Update tests/test_snow.py rewrite the test function to avoid repetition in numpy array. Co-authored-by: Rajiv Daxini <143435106+RDaxini@users.noreply.github.com> --- tests/test_snow.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_snow.py b/tests/test_snow.py index 34e2ecd134..e546e1100b 100644 --- a/tests/test_snow.py +++ b/tests/test_snow.py @@ -254,11 +254,9 @@ def test_loss_townsend_front_side_fraction(): 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]) + 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 From 393399a0e0eb7203b09a6af444649c465e6e5cc1 Mon Sep 17 00:00:00 2001 From: shethkajal7 Date: Mon, 18 May 2026 18:32:26 -0500 Subject: [PATCH 3/6] Address review comments Set front_side_fraction default to 1.0, simplify the scaling logic, clean up tests, and add a whatsnew entry. --- docs/sphinx/source/whatsnew/v0.15.2.rst | 6 +++++- pvlib/snow.py | 23 ++++++++++------------- tests/test_snow.py | 4 +--- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.15.2.rst b/docs/sphinx/source/whatsnew/v0.15.2.rst index 37f8692280..7cad308c58 100644 --- a/docs/sphinx/source/whatsnew/v0.15.2.rst +++ b/docs/sphinx/source/whatsnew/v0.15.2.rst @@ -21,7 +21,10 @@ Bug fixes Enhancements ~~~~~~~~~~~~ - +* Added the ``front_side_fraction`` parameter to + :py:func:`pvlib.snow.loss_townsend` to support bifacial Townsend snow-loss + workflows where the calculated loss is scaled by the front-side energy + fraction. (:pull:`2756`) (:ghuser:`shethkajal7`) Documentation ~~~~~~~~~~~~~ @@ -53,3 +56,4 @@ Contributors ~~~~~~~~~~~~ * :ghuser:`Omesh37` * Cliff Hansen (:ghuser:`cwhanse`) +* :ghuser:`shethkajal7` diff --git a/pvlib/snow.py b/pvlib/snow.py index 40ad2b74f7..a21a9e45ad 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -249,7 +249,7 @@ 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, - front_side_fraction=None): + front_side_fraction=1.0): ''' Calculates monthly snow loss based on the Townsend monthly snow loss model. @@ -294,13 +294,12 @@ 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. [-] - + 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. + Returns ------- loss : array-like @@ -319,8 +318,8 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity, 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``. + rear-side insolation in ``poa_global``. The resulting loss is + scaled by the front-side energy fraction ``front_side_fraction``. References ---------- @@ -396,8 +395,6 @@ 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) - loss_fraction = loss_fraction * front_side_fraction + loss_fraction = loss_fraction * front_side_fraction return np.clip(loss_fraction, 0, 1) diff --git a/tests/test_snow.py b/tests/test_snow.py index e546e1100b..0a9e88277f 100644 --- a/tests/test_snow.py +++ b/tests/test_snow.py @@ -247,8 +247,6 @@ 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) - - 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]) @@ -270,4 +268,4 @@ def test_loss_townsend_front_side_fraction(): poa_global, slant_height, lower_edge_height, front_side_fraction=front_side_fraction) - np.testing.assert_allclose(adjusted, unadjusted * front_side_fraction) \ No newline at end of file + np.testing.assert_allclose(adjusted, unadjusted * front_side_fraction) From 19d9486a1ee695fa88377ae239d49bc35139de44 Mon Sep 17 00:00:00 2001 From: shethkajal7 Date: Mon, 18 May 2026 19:37:03 -0500 Subject: [PATCH 4/6] Updated test_snow.py Added two newlines before the test function --- tests/test_snow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_snow.py b/tests/test_snow.py index 0a9e88277f..46dc0410ce 100644 --- a/tests/test_snow.py +++ b/tests/test_snow.py @@ -247,6 +247,8 @@ 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) + + 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]) From 31cc04b92e939c9fc970fc3e0d0ad770f50536f1 Mon Sep 17 00:00:00 2001 From: shethkajal7 Date: Tue, 19 May 2026 21:15:37 -0500 Subject: [PATCH 5/6] Update docs/sphinx/source/whatsnew/v0.15.2.rst Remove the username information following format of other entries Co-authored-by: Rajiv Daxini <143435106+RDaxini@users.noreply.github.com> --- docs/sphinx/source/whatsnew/v0.15.2.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.15.2.rst b/docs/sphinx/source/whatsnew/v0.15.2.rst index 7cad308c58..72d58671b1 100644 --- a/docs/sphinx/source/whatsnew/v0.15.2.rst +++ b/docs/sphinx/source/whatsnew/v0.15.2.rst @@ -21,10 +21,11 @@ Bug fixes Enhancements ~~~~~~~~~~~~ -* Added the ``front_side_fraction`` parameter to +* Add the ``front_side_fraction`` parameter to :py:func:`pvlib.snow.loss_townsend` to support bifacial Townsend snow-loss workflows where the calculated loss is scaled by the front-side energy - fraction. (:pull:`2756`) (:ghuser:`shethkajal7`) + fraction. (:issue:`2755`, :pull:`2756`) + Documentation ~~~~~~~~~~~~~ From 08d0ebf39615a367842d13275049325e629b3bb9 Mon Sep 17 00:00:00 2001 From: shethkajal7 Date: Tue, 19 May 2026 21:27:49 -0500 Subject: [PATCH 6/6] Update pvlib/snow.py to change parentheses and period position Parentheses and period positions are updated to follow style guide Co-authored-by: Rajiv Daxini <143435106+RDaxini@users.noreply.github.com> --- pvlib/snow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index a21a9e45ad..b78a93a2ec 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -295,7 +295,7 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity, 25°-55° avalanching slope angles. [deg] front_side_fraction : numeric or array-like, default 1.0 - Fraction of monthly energy from front-side insolation (unitless). + 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.