From 3594450987570e7cf524a67cf0aba4b93ff8dee7 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 6 Nov 2024 13:10:42 -0800 Subject: [PATCH 01/13] add snow_depth inputs --- pvlib/snow.py | 39 ++++++++++++++++++++++++++++++--------- pvlib/tests/test_snow.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index 190fe7baae..2c51dc8256 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -13,23 +13,25 @@ def _time_delta_in_hours(times): return delta.dt.total_seconds().div(3600) -def fully_covered_nrel(snowfall, threshold_snowfall=1.): +def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1.): ''' Calculates the timesteps when the row's slant height is fully covered by snow. Parameters ---------- - snowfall : Series - Accumulated snowfall in each time period [cm] - - threshold_snowfall : float, default 1.0 + snowfall: Series + Accumulated snowfall in each time period. [cm] + snow_depth: Series, optional + Snow depth on the ground at the beginning of each time period. + Must have the same index as `snowfall`. [cm] + threshold_snowfall: float, default 1.0 Hourly snowfall above which snow coverage is set to the row's slant height. [cm/hr] Returns ---------- - boolean: Series + Series True where the snowfall exceeds the defined threshold to fully cover the panel. @@ -37,6 +39,10 @@ def fully_covered_nrel(snowfall, threshold_snowfall=1.): ----- Implements the model described in [1]_ with minor improvements in [2]_. + `snow_depth` is used to set coverage=0 when no snow is present on the + ground. This check is described in [2]_ as needed for systems with + low tilt angle. + References ---------- .. [1] Marion, B.; Schaefer, R.; Caine, H.; Sanchez, G. (2013). @@ -56,11 +62,15 @@ def fully_covered_nrel(snowfall, threshold_snowfall=1.): hourly_snow_rate.iloc[0] = snowfall.iloc[0] / timedelta else: # can't infer frequency from index hourly_snow_rate.iloc[0] = 0 # replaces NaN - return hourly_snow_rate > threshold_snowfall + covered = (hourly_snow_rate > threshold_snowfall) + # no coverage when no snow on the ground + if snow_depth is not None: + covered = covered & (snow_depth > 0.) + return covered def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, - initial_coverage=0, threshold_snowfall=1., + snow_depth=None, initial_coverage=0, threshold_snowfall=1., can_slide_coefficient=-80., slide_amount_coefficient=0.197): ''' Calculates the fraction of the slant height of a row of modules covered by @@ -82,6 +92,9 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, surface_tilt : numeric Tilt of module's from horizontal, e.g. surface facing up = 0, surface facing horizon = 90. [degrees] + snow_depth : Series, optional + Snow depth on the ground at the beginning of each time period. + Must have the same index as `snowfall`. [cm] initial_coverage : float, default 0 Fraction of row's slant height that is covered with snow at the beginning of the simulation. [unitless] @@ -106,6 +119,10 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, In [1]_, `can_slide_coefficient` is termed `m`, and the value of `slide_amount_coefficient` is given in tenths of a module's slant height. + `snow_depth` is used to set coverage=0 when no snow is present on the + ground. This check is described in [2]_ as needed for systems with + low tilt angle. + References ---------- .. [1] Marion, B.; Schaefer, R.; Caine, H.; Sanchez, G. (2013). @@ -117,7 +134,7 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, ''' # find times with new snowfall - new_snowfall = fully_covered_nrel(snowfall, threshold_snowfall) + new_snowfall = fully_covered_nrel(snowfall, snow_depth, threshold_snowfall) # set up output Series snow_coverage = pd.Series(np.nan, index=poa_irradiance.index) @@ -143,6 +160,10 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, snow_coverage.ffill(inplace=True) snow_coverage -= cumulative_sliding + if snow_depth is not None: + # no coverage when there's no snow on the ground + # described in [2] to avoid non-sliding snow for low-tilt systems. + snow_coverage[snow_depth<=0] = 0. # clean up periods where row is completely uncovered return snow_coverage.clip(lower=0) diff --git a/pvlib/tests/test_snow.py b/pvlib/tests/test_snow.py index 19e79b5179..042350f0f6 100644 --- a/pvlib/tests/test_snow.py +++ b/pvlib/tests/test_snow.py @@ -19,6 +19,18 @@ def test_fully_covered_nrel(): assert_series_equal(expected, fully_covered) +def test_fully_covered_nrel_with_snow_depth(): + dt = pd.date_range(start="2019-1-1 12:00:00", end="2019-1-1 18:00:00", + freq='1h') + snowfall_data = pd.Series([1, 5, .6, 4, .23, -5, 19], index=dt) + snow_depth = pd.Series([0., 1, 6, 6.6, 10.6, 10., -2], index=dt) + expected = pd.Series([False, True, False, True, False, False, False], + index=dt) + fully_covered = snow.fully_covered_nrel(snowfall_data, + snow_depth=snow_depth) + assert_series_equal(expected, fully_covered) + + def test_coverage_nrel_hourly(): surface_tilt = 45 slide_amount_coefficient = 0.197 @@ -38,6 +50,27 @@ def test_coverage_nrel_hourly(): assert_series_equal(expected, snow_coverage) +def test_coverage_nrel_hourly_with_snow_depth(): + surface_tilt = 45 + slide_amount_coefficient = 0.197 + dt = pd.date_range(start="2019-1-1 10:00:00", end="2019-1-1 17:00:00", + freq='1h') + poa_irradiance = pd.Series([400, 200, 100, 1234, 134, 982, 100, 100], + index=dt) + temp_air = pd.Series([10, 2, 10, 1234, 34, 982, 10, 10], index=dt) + snowfall_data = pd.Series([1, .5, .6, .4, .23, -5, .1, .1], index=dt) + snow_depth = pd.Series([1, 1, 1, 1, 0, 1, 0, .1], index=dt) + snow_coverage = snow.coverage_nrel( + snowfall_data, poa_irradiance, temp_air, surface_tilt, + snow_depth=snow_depth, threshold_snowfall=0.6) + + slide_amt = slide_amount_coefficient * sind(surface_tilt) + covered = 1.0 - slide_amt * np.array([0, 1, 2, 3, 4, 5, 6, 7]) + expected = pd.Series(covered, index=dt) + expected[snow_depth<=0] = 0 + assert_series_equal(expected, snow_coverage) + + def test_coverage_nrel_subhourly(): surface_tilt = 45 slide_amount_coefficient = 0.197 From 9716bc8b36ed446f50cc11514590584a96414872 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 6 Nov 2024 13:37:57 -0800 Subject: [PATCH 02/13] linter --- pvlib/snow.py | 2 +- pvlib/tests/test_snow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index 2c51dc8256..c98739712d 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -163,7 +163,7 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, if snow_depth is not None: # no coverage when there's no snow on the ground # described in [2] to avoid non-sliding snow for low-tilt systems. - snow_coverage[snow_depth<=0] = 0. + snow_coverage[snow_depth <= 0] = 0. # clean up periods where row is completely uncovered return snow_coverage.clip(lower=0) diff --git a/pvlib/tests/test_snow.py b/pvlib/tests/test_snow.py index 042350f0f6..da1e1f1f6c 100644 --- a/pvlib/tests/test_snow.py +++ b/pvlib/tests/test_snow.py @@ -67,7 +67,7 @@ def test_coverage_nrel_hourly_with_snow_depth(): slide_amt = slide_amount_coefficient * sind(surface_tilt) covered = 1.0 - slide_amt * np.array([0, 1, 2, 3, 4, 5, 6, 7]) expected = pd.Series(covered, index=dt) - expected[snow_depth<=0] = 0 + expected[snow_depth <= 0] = 0 assert_series_equal(expected, snow_coverage) From cd579f2f432f7ef880a1790da09d9c25aa74d54a Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 6 Nov 2024 13:40:01 -0800 Subject: [PATCH 03/13] double ticks --- pvlib/snow.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index c98739712d..0fef53de6d 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -24,7 +24,7 @@ def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1.): Accumulated snowfall in each time period. [cm] snow_depth: Series, optional Snow depth on the ground at the beginning of each time period. - Must have the same index as `snowfall`. [cm] + Must have the same index as ``snowfall``. [cm] threshold_snowfall: float, default 1.0 Hourly snowfall above which snow coverage is set to the row's slant height. [cm/hr] @@ -39,7 +39,7 @@ def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1.): ----- Implements the model described in [1]_ with minor improvements in [2]_. - `snow_depth` is used to set coverage=0 when no snow is present on the + ``snow_depth`` is used to set coverage=0 when no snow is present on the ground. This check is described in [2]_ as needed for systems with low tilt angle. @@ -94,7 +94,7 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, surface facing horizon = 90. [degrees] snow_depth : Series, optional Snow depth on the ground at the beginning of each time period. - Must have the same index as `snowfall`. [cm] + Must have the same index as ``snowfall``. [cm] initial_coverage : float, default 0 Fraction of row's slant height that is covered with snow at the beginning of the simulation. [unitless] @@ -116,10 +116,10 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, Notes ----- - In [1]_, `can_slide_coefficient` is termed `m`, and the value of - `slide_amount_coefficient` is given in tenths of a module's slant height. + In [1]_, ``can_slide_coefficient`` is termed `m`, and the value of + ``slide_amount_coefficient`` is given in tenths of a module's slant height. - `snow_depth` is used to set coverage=0 when no snow is present on the + ``snow_depth`` is used to set coverage=0 when no snow is present on the ground. This check is described in [2]_ as needed for systems with low tilt angle. From 7194e3cf5e59a132ee13c504a1d93cc2185f28ae Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 11 Dec 2024 09:20:05 -0700 Subject: [PATCH 04/13] review comments, improve some descriptions --- pvlib/snow.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index 0fef53de6d..75d6e33535 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -15,8 +15,7 @@ def _time_delta_in_hours(times): def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1.): ''' - Calculates the timesteps when the row's slant height is fully covered - by snow. + Calculates the timesteps when modules are fully covered by snow. Parameters ---------- @@ -26,22 +25,22 @@ def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1.): Snow depth on the ground at the beginning of each time period. Must have the same index as ``snowfall``. [cm] threshold_snowfall: float, default 1.0 - Hourly snowfall above which snow coverage is set to the row's slant - height. [cm/hr] + Hourly snowfall above which the row is fully covered for that hour. + [cm/hr] Returns ---------- - Series - True where the snowfall exceeds the defined threshold to fully cover - the panel. + covered: Series + A Series of boolean, True where the snowfall exceeds the defined + threshold to fully cover the panel. Notes ----- Implements the model described in [1]_ with minor improvements in [2]_. - ``snow_depth`` is used to set coverage=0 when no snow is present on the - ground. This check is described in [2]_ as needed for systems with - low tilt angle. + ``snow_depth`` is used to return `False` (not fully covered) when no snow + is present on the ground. This check is described in [2]_ as needed for + systems with low tilt angle. References ---------- @@ -73,8 +72,8 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, snow_depth=None, initial_coverage=0, threshold_snowfall=1., can_slide_coefficient=-80., slide_amount_coefficient=0.197): ''' - Calculates the fraction of the slant height of a row of modules covered by - snow at every time step. + Calculates the fraction of the slant height of a row of modules that is + covered by snow at every time step. Implements the model described in [1]_ with minor improvements in [2]_, with the change that the output is in fraction of the row's slant height @@ -119,9 +118,9 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, In [1]_, ``can_slide_coefficient`` is termed `m`, and the value of ``slide_amount_coefficient`` is given in tenths of a module's slant height. - ``snow_depth`` is used to set coverage=0 when no snow is present on the - ground. This check is described in [2]_ as needed for systems with - low tilt angle. + ``snow_depth`` is used to set ``snow_coverage`` to 0 when no snow is + present on the ground. This check is described in [2]_ as needed for + systems with low tilt angle. References ---------- From 11cf12e8f8469ad3cfe91a8c62d6dd00b2acda36 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 11 Dec 2024 09:27:34 -0700 Subject: [PATCH 05/13] whatsnew --- docs/sphinx/source/whatsnew/v0.11.2.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.11.2.rst b/docs/sphinx/source/whatsnew/v0.11.2.rst index 087697f778..beb5bafaff 100644 --- a/docs/sphinx/source/whatsnew/v0.11.2.rst +++ b/docs/sphinx/source/whatsnew/v0.11.2.rst @@ -22,6 +22,9 @@ Bug Fixes (:issue:`1338`, :pull:`2227`) * Handle DST transitions that happen at midnight in :py:func:`pvlib.solarposition.hour_angle` (:issue:`2132` :pull:`2133`) +* Add a check to :py:func:`~pvlib.snow.fully_covered_nrel` and + :py:func:`~pvlib.snow.coverage_nrel`. The check uses snow depth on the ground + to improve modeling for systems with shallow tilt angles. (:issue:`1171`, :pull:`2292`) Bug fixes ~~~~~~~~~ From b6424e3e462d7fd6335a18124aff1aa6a01351a9 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 20 Dec 2024 15:24:09 -0700 Subject: [PATCH 06/13] clarify threshold_depth --- pvlib/snow.py | 33 ++++++++++++++++++++------------- pvlib/tests/test_snow.py | 9 ++++++--- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index 75d6e33535..9f4234caf0 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -13,20 +13,23 @@ def _time_delta_in_hours(times): return delta.dt.total_seconds().div(3600) -def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1.): +def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1., + threshold_depth=1.): ''' Calculates the timesteps when modules are fully covered by snow. Parameters ---------- snowfall: Series - Accumulated snowfall in each time period. [cm] + Snowfall in each time period. [cm] snow_depth: Series, optional Snow depth on the ground at the beginning of each time period. Must have the same index as ``snowfall``. [cm] threshold_snowfall: float, default 1.0 Hourly snowfall above which the row is fully covered for that hour. [cm/hr] + threshold_depth: float, default 1.0 + Snow depth on the ground, above which snow can affect the modules. [cm] Returns ---------- @@ -38,9 +41,9 @@ def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1.): ----- Implements the model described in [1]_ with minor improvements in [2]_. - ``snow_depth`` is used to return `False` (not fully covered) when no snow - is present on the ground. This check is described in [2]_ as needed for - systems with low tilt angle. + ``snow_depth`` is used to return `False` (not fully covered) when snow + is less than ``threshold_depth. This check is described in [2]_ as needed + for systems with low tilt angle. References ---------- @@ -64,13 +67,14 @@ def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1.): covered = (hourly_snow_rate > threshold_snowfall) # no coverage when no snow on the ground if snow_depth is not None: - covered = covered & (snow_depth > 0.) + covered = covered & (snow_depth >= threshold_depth) return covered def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, snow_depth=None, initial_coverage=0, threshold_snowfall=1., - can_slide_coefficient=-80., slide_amount_coefficient=0.197): + threshold_depth=1., can_slide_coefficient=-80., + slide_amount_coefficient=0.197): ''' Calculates the fraction of the slant height of a row of modules that is covered by snow at every time step. @@ -83,7 +87,7 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, Parameters ---------- snowfall : Series - Accumulated snowfall within each time period. [cm] + Snowfall within each time period. [cm] poa_irradiance : Series Total in-plane irradiance [W/m^2] temp_air : Series @@ -100,6 +104,8 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, threshold_snowfall : float, default 1.0 Hourly snowfall above which snow coverage is set to the row's slant height. [cm/hr] + threshold_depth: float, default 1.0 + Snow depth on the ground, above which snow can affect the modules. [cm] can_slide_coefficient : float, default -80. Coefficient to determine if snow can slide given irradiance and air temperature. [W/(m^2 C)] @@ -118,9 +124,9 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, In [1]_, ``can_slide_coefficient`` is termed `m`, and the value of ``slide_amount_coefficient`` is given in tenths of a module's slant height. - ``snow_depth`` is used to set ``snow_coverage`` to 0 when no snow is - present on the ground. This check is described in [2]_ as needed for - systems with low tilt angle. + ``snow_depth`` is used to set ``snow_coverage`` to 0 (not fully covered) + when snow is less than ``threshold_depth. . This check is described in + [2]_ as needed for systems with low tilt angle. References ---------- @@ -133,7 +139,8 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, ''' # find times with new snowfall - new_snowfall = fully_covered_nrel(snowfall, snow_depth, threshold_snowfall) + new_snowfall = fully_covered_nrel(snowfall, snow_depth, threshold_snowfall, + threshold_depth) # set up output Series snow_coverage = pd.Series(np.nan, index=poa_irradiance.index) @@ -162,7 +169,7 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, if snow_depth is not None: # no coverage when there's no snow on the ground # described in [2] to avoid non-sliding snow for low-tilt systems. - snow_coverage[snow_depth <= 0] = 0. + snow_coverage[snow_depth < threshold_depth] = 0. # clean up periods where row is completely uncovered return snow_coverage.clip(lower=0) diff --git a/pvlib/tests/test_snow.py b/pvlib/tests/test_snow.py index da1e1f1f6c..65687f3f0b 100644 --- a/pvlib/tests/test_snow.py +++ b/pvlib/tests/test_snow.py @@ -27,7 +27,8 @@ def test_fully_covered_nrel_with_snow_depth(): expected = pd.Series([False, True, False, True, False, False, False], index=dt) fully_covered = snow.fully_covered_nrel(snowfall_data, - snow_depth=snow_depth) + snow_depth=snow_depth, + threshold_depth=0.) assert_series_equal(expected, fully_covered) @@ -53,6 +54,7 @@ def test_coverage_nrel_hourly(): def test_coverage_nrel_hourly_with_snow_depth(): surface_tilt = 45 slide_amount_coefficient = 0.197 + threshold_depth = 0.5 dt = pd.date_range(start="2019-1-1 10:00:00", end="2019-1-1 17:00:00", freq='1h') poa_irradiance = pd.Series([400, 200, 100, 1234, 134, 982, 100, 100], @@ -62,12 +64,13 @@ def test_coverage_nrel_hourly_with_snow_depth(): snow_depth = pd.Series([1, 1, 1, 1, 0, 1, 0, .1], index=dt) snow_coverage = snow.coverage_nrel( snowfall_data, poa_irradiance, temp_air, surface_tilt, - snow_depth=snow_depth, threshold_snowfall=0.6) + snow_depth=snow_depth, threshold_snowfall=0.6, + threshold_depth=threshold_depth) slide_amt = slide_amount_coefficient * sind(surface_tilt) covered = 1.0 - slide_amt * np.array([0, 1, 2, 3, 4, 5, 6, 7]) expected = pd.Series(covered, index=dt) - expected[snow_depth <= 0] = 0 + expected[snow_depth < threshold_depth] = 0 assert_series_equal(expected, snow_coverage) From d46837cabfd862f74baa8c26ba9e3727e88f65b9 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 20 Dec 2024 15:26:21 -0700 Subject: [PATCH 07/13] add note about default value --- pvlib/snow.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pvlib/snow.py b/pvlib/snow.py index 9f4234caf0..4927b326d0 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -169,6 +169,9 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, if snow_depth is not None: # no coverage when there's no snow on the ground # described in [2] to avoid non-sliding snow for low-tilt systems. + # default threshold_depth of 1cm is from SAM's implementation and + # is different than the value of 0cm implied in [2]. + # https://github.com.mcas-gov.ms/NREL/ssc/issues/1265 snow_coverage[snow_depth < threshold_depth] = 0. # clean up periods where row is completely uncovered return snow_coverage.clip(lower=0) From 57f5eefa968866c6a455182b2b31ef8ad141213f Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Sun, 22 Dec 2024 11:15:29 -0700 Subject: [PATCH 08/13] move check for snow depth --- pvlib/snow.py | 22 +++++++++++++++------- pvlib/tests/test_snow.py | 13 +++++++------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index 4927b326d0..aab63fdf62 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -155,6 +155,14 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, # don't slide in the interval preceding the snowfall data slide_amt.iloc[0] = 0 + if snow_depth is not None: + # all slides off if there is no snow on the ground + # described in [2] to avoid non-sliding snow for low-tilt systems. + # default threshold_depth of 1cm is from SAM's implementation and + # is different than the value of 0cm implied in [2]. + # https://github.com.mcas-gov.ms/NREL/ssc/issues/1265 + slide_amt[snow_depth < threshold_depth] = 1. + # build time series of cumulative slide amounts sliding_period_ID = new_snowfall.cumsum() cumulative_sliding = slide_amt.groupby(sliding_period_ID).cumsum() @@ -166,13 +174,13 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, snow_coverage.ffill(inplace=True) snow_coverage -= cumulative_sliding - if snow_depth is not None: - # no coverage when there's no snow on the ground - # described in [2] to avoid non-sliding snow for low-tilt systems. - # default threshold_depth of 1cm is from SAM's implementation and - # is different than the value of 0cm implied in [2]. - # https://github.com.mcas-gov.ms/NREL/ssc/issues/1265 - snow_coverage[snow_depth < threshold_depth] = 0. + # if snow_depth is not None: + # # no coverage when there's no snow on the ground + # # described in [2] to avoid non-sliding snow for low-tilt systems. + # # default threshold_depth of 1cm is from SAM's implementation and + # # is different than the value of 0cm implied in [2]. + # # https://github.com.mcas-gov.ms/NREL/ssc/issues/1265 + # snow_coverage[snow_depth < threshold_depth] = 0. # clean up periods where row is completely uncovered return snow_coverage.clip(lower=0) diff --git a/pvlib/tests/test_snow.py b/pvlib/tests/test_snow.py index 65687f3f0b..6d8954011e 100644 --- a/pvlib/tests/test_snow.py +++ b/pvlib/tests/test_snow.py @@ -55,20 +55,21 @@ def test_coverage_nrel_hourly_with_snow_depth(): surface_tilt = 45 slide_amount_coefficient = 0.197 threshold_depth = 0.5 - dt = pd.date_range(start="2019-1-1 10:00:00", end="2019-1-1 17:00:00", + dt = pd.date_range(start="2019-1-1 10:00:00", end="2019-1-1 18:00:00", freq='1h') - poa_irradiance = pd.Series([400, 200, 100, 1234, 134, 982, 100, 100], + poa_irradiance = pd.Series([400, 200, 100, 1234, 134, 982, 100, 100, 100], index=dt) - temp_air = pd.Series([10, 2, 10, 1234, 34, 982, 10, 10], index=dt) - snowfall_data = pd.Series([1, .5, .6, .4, .23, -5, .1, .1], index=dt) - snow_depth = pd.Series([1, 1, 1, 1, 0, 1, 0, .1], index=dt) + temp_air = pd.Series([10, 2, 10, 1234, 34, 982, 10, 10, 10], index=dt) + # restarts with new snow on 5th time step + snowfall_data = pd.Series([1, .5, .6, .4, .23, 5., .1, .1, 0.], index=dt) + snow_depth = pd.Series([1, 1, 1, 1, 0, 1, 1, 0, .1], index=dt) snow_coverage = snow.coverage_nrel( snowfall_data, poa_irradiance, temp_air, surface_tilt, snow_depth=snow_depth, threshold_snowfall=0.6, threshold_depth=threshold_depth) slide_amt = slide_amount_coefficient * sind(surface_tilt) - covered = 1.0 - slide_amt * np.array([0, 1, 2, 3, 4, 5, 6, 7]) + covered = 1.0 - slide_amt * np.array([0, 1, 2, 3, 0, 0, 1, 0, 0]) expected = pd.Series(covered, index=dt) expected[snow_depth < threshold_depth] = 0 assert_series_equal(expected, snow_coverage) From 81c44d70a45c7e3ece5c091af482fd9baa337dfe Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Sun, 22 Dec 2024 11:45:54 -0700 Subject: [PATCH 09/13] comments --- pvlib/snow.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index aab63fdf62..580489c575 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -174,14 +174,6 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, snow_coverage.ffill(inplace=True) snow_coverage -= cumulative_sliding - # if snow_depth is not None: - # # no coverage when there's no snow on the ground - # # described in [2] to avoid non-sliding snow for low-tilt systems. - # # default threshold_depth of 1cm is from SAM's implementation and - # # is different than the value of 0cm implied in [2]. - # # https://github.com.mcas-gov.ms/NREL/ssc/issues/1265 - # snow_coverage[snow_depth < threshold_depth] = 0. - # clean up periods where row is completely uncovered return snow_coverage.clip(lower=0) From fb97cd99c837e252e877f8078c2ce65f6a18ecdd Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 7 Mar 2025 09:20:29 -0700 Subject: [PATCH 10/13] update whatsnew --- docs/sphinx/source/whatsnew/v0.11.2.rst | 6 ------ docs/sphinx/source/whatsnew/v0.11.3.rst | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.2.rst b/docs/sphinx/source/whatsnew/v0.11.2.rst index 8010b9389d..ab63431e0d 100644 --- a/docs/sphinx/source/whatsnew/v0.11.2.rst +++ b/docs/sphinx/source/whatsnew/v0.11.2.rst @@ -23,12 +23,6 @@ Bug Fixes (:issue:`1338`, :pull:`2227`) * Handle DST transitions that happen at midnight in :py:func:`pvlib.solarposition.hour_angle` (:issue:`2132` :pull:`2133`) -* Add a check to :py:func:`~pvlib.snow.fully_covered_nrel` and - :py:func:`~pvlib.snow.coverage_nrel`. The check uses snow depth on the ground - to improve modeling for systems with shallow tilt angles. (:issue:`1171`, :pull:`2292`) - -Bug fixes -~~~~~~~~~ * Change ``dni_extra`` to a required parameter in :py:func:`pvlib.irradiance.ghi_from_poa_driesse_2023` (:issue:`2279` :pull:`2331`) * :py:func:`~pvlib.spa.julian_day_dt` now accounts for the 10 day difference diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index 4f2637f948..5f7934f4bb 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -4,6 +4,12 @@ v0.11.3 (Anticipated March, 2025) --------------------------------- +Bug fixes +~~~~~~~~~ +* Add a check to :py:func:`~pvlib.snow.fully_covered_nrel` and + :py:func:`~pvlib.snow.coverage_nrel`. The check uses snow depth on the ground + to improve modeling for systems with shallow tilt angles. (:issue:`1171`, :pull:`2292`) + Deprecations ~~~~~~~~~~~~ From e3d97e5ee3b585fa54dafbd796980ef196f4c74e Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 13 Mar 2025 08:49:27 -0600 Subject: [PATCH 11/13] edit whatsnew --- docs/sphinx/source/whatsnew/v0.11.3.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index 5f7934f4bb..2d6abe14a7 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -8,7 +8,8 @@ Bug fixes ~~~~~~~~~ * Add a check to :py:func:`~pvlib.snow.fully_covered_nrel` and :py:func:`~pvlib.snow.coverage_nrel`. The check uses snow depth on the ground - to improve modeling for systems with shallow tilt angles. (:issue:`1171`, :pull:`2292`) + to improve modeling for systems with shallow tilt angles. The check + adds a new, optional parameter snow_depth. (:issue:`1171`, :pull:`2292`) Deprecations ~~~~~~~~~~~~ From 4d0110aa01555ffbaf64aa209f351eaf0b53badb Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 14 Mar 2025 08:24:51 -0600 Subject: [PATCH 12/13] Apply suggestions from code review Co-authored-by: Kevin Anderson --- pvlib/snow.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index c4c35c3171..cdd142ba68 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -42,7 +42,7 @@ def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1., Implements the model described in [1]_ with minor improvements in [2]_. ``snow_depth`` is used to return `False` (not fully covered) when snow - is less than ``threshold_depth. This check is described in [2]_ as needed + is less than ``threshold_depth``. This check is described in [2]_ as needed for systems with low tilt angle. References @@ -101,9 +101,9 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, initial_coverage : float, default 0 Fraction of row's slant height that is covered with snow at the beginning of the simulation. [unitless] - threshold_snowfall : float, default 1.0 - Hourly snowfall above which snow coverage is set to the row's slant - height. [cm/hr] + threshold_snowfall: float, default 1.0 + Hourly snowfall above which the row is fully covered for that hour. + [cm/hr] threshold_depth: float, default 1.0 Snow depth on the ground, above which snow can affect the modules. [cm] can_slide_coefficient : float, default -80. @@ -125,7 +125,7 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, ``slide_amount_coefficient`` is given in tenths of a module's slant height. ``snow_depth`` is used to set ``snow_coverage`` to 0 (not fully covered) - when snow is less than ``threshold_depth. . This check is described in + when snow is less than ``threshold_depth``. This check is described in [2]_ as needed for systems with low tilt angle. References From b1ff01a81e7da4305a3478e602b921e9b5532add Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 14 Mar 2025 08:35:21 -0600 Subject: [PATCH 13/13] edits --- pvlib/snow.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index cdd142ba68..f2c8fca148 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -156,11 +156,10 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, slide_amt.iloc[0] = 0 if snow_depth is not None: - # all slides off if there is no snow on the ground - # described in [2] to avoid non-sliding snow for low-tilt systems. - # default threshold_depth of 1cm is from SAM's implementation and - # is different than the value of 0cm implied in [2]. - # https://github.com.mcas-gov.ms/NREL/ssc/issues/1265 + # All slides off if snow on the ground is less than threshold_depth. + # Described in [2] to avoid non-sliding snow for low-tilt systems. + # Default threshold_depth of 1cm is from [2[ and SAM's implementation. + # https://github.com/NREL/ssc/issues/1265 slide_amt[snow_depth < threshold_depth] = 1. # build time series of cumulative slide amounts