From 936439c44cad5a5e9bd1622c8325b69b93f49ce6 Mon Sep 17 00:00:00 2001 From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com> Date: Wed, 30 Nov 2022 11:23:42 +0100 Subject: [PATCH 1/7] Create nighttime_offset_correction function --- pvanalytics/features/irradiance.py | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 pvanalytics/features/irradiance.py diff --git a/pvanalytics/features/irradiance.py b/pvanalytics/features/irradiance.py new file mode 100644 index 000000000..83c425b95 --- /dev/null +++ b/pvanalytics/features/irradiance.py @@ -0,0 +1,65 @@ +"""Quality control functions for irradiance data.""" + +import numpy as np +import datetime as dt + + +def nighttime_offset_correction(irradiance, zenith, sza_night_limit=100, + label='right', midnight_method='zenith', + aggregation_method='median'): + """ + Apply nighttime correction to irradiance time series. + + Parameters + ---------- + irradiance : pd.Series + Pandas Series of irradiance data. + zenith : pd.Series + Pandas Series of zenith angles corresponding to the irradiance time + series. + sza_night_limit : float, optional + Solar zenith angle boundary limit (periods with zenith angles greater + or equal are used to compute the nighttime offset). The default is 100. + label : {'right', 'left'}, optional + Whether the timestamps correspond to the start/left or end/right of the + interval. The default is 'right'. + midnight_method : {'zenith', 'time'}, optional + Method for determining midnight. The default is 'zenith', which + assumes midnight occurs when the zenith angle is at the maximum. + aggregation_method : {'median', 'mean'}, optional + Method for calculating nighttime offset. The default is 'median'. + + Returns + ------- + corrected_irradiance : pd.Series + Pandas Series of nighttime corrected irradiance. + """ + # Raise an error if arguments are incorrect + if label not in ['right', 'left']: + raise ValueError("label must be 'right' or 'left'.") + if aggregation_method not in ['median', 'mean']: + raise ValueError("aggregation_method must be 'mean' or 'median'.") + + # Create boolean series where nighttime is one (calculated based on the + # zenith angle) + midnight_zenith = (zenith.diff().apply(np.sign).diff() < 0) + # Assign unique number to each day + day_number_zenith = midnight_zenith.cumsum() + + # Choose grouping parameter based on the midnight_method + if midnight_method == 'zenith': + grouping_category = day_number_zenith + elif midnight_method == 'time': + grouping_category = irradiance.index.date + if label == 'right': + grouping_category[irradiance.index.time == dt.time(0)] += -dt.timedelta(days=1) + else: + raise ValueError("midnight_method must be 'zenith' or 'time'.") + + # Create Pandas Series only containing nighttime irradiance + nighttime_irradiance = irradiance[zenith >= sza_night_limit] + # Calculate nighttime offset + nighttime_offset = nighttime_irradiance.groupby(grouping_category).transform(aggregation_method) + # Calculate corrected irradiance time series + corrected_irradiance = irradiance - nighttime_offset + return corrected_irradiance From 5afbe222725ac653306367bb5c2f34519e2373ea Mon Sep 17 00:00:00 2001 From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com> Date: Wed, 30 Nov 2022 12:44:08 +0100 Subject: [PATCH 2/7] Add Nolas lower GHI limit function --- pvanalytics/quality/irradiance.py | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pvanalytics/quality/irradiance.py b/pvanalytics/quality/irradiance.py index dd3b329db..0b0ba0f09 100644 --- a/pvanalytics/quality/irradiance.py +++ b/pvanalytics/quality/irradiance.py @@ -88,6 +88,44 @@ def check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra, limits=None): return ghi_limit_flag +def chec_ghi_lower_limit_nollas(ghi, solar_zenith): + r"""Test for lower limit on GHI using empirical limit from Nollas (2023). + + Test is applied to each GHI value. A GHI value passes if value > + lower bound. The lower bound is from [1]_ and calculated as: + + .. math:: + lb = (6.5331 - 0.065502 * solar\_zenith + 0.00018312 * solar\_zenith^{2}) / + (1 + 0.01113*solar\_zenith) + + Parameters + ---------- + ghi : Series + Global horizontal irradiance in :math:`W/m^2` + solar_zenith : Series + Solar zenith angle in degrees + + Returns + ------- + Series + True where value passes limits test. + + References + ---------- + .. [1] `F. M. Nollas, G. A. Salazar, and C. A. Gueymard, Quality control + procedure for 1-minute pyranometric measurements of global and + shadowband-based diffuse solar irradiance, Renewable Energy, 202, + pp. 40-55, 2023. + `_ + """ + ghi_lb = ((6.5331-0.065502*solar_zenith + 0.00018312*solar_zenith**2) / + (1 + 0.01113*solar_zenith)) + + ghi_lower_limit_flag = quality.util.check_limits(value=ghi, lower_bound=ghi_lb) + + return ghi_lower_limit_flag + + def check_dhi_limits_qcrad(dhi, solar_zenith, dni_extra, limits=None): r"""Test for physical limits on DHI using the QCRad criteria. From 9fd466975dfe87d694416ab4694c379d5ee4279c Mon Sep 17 00:00:00 2001 From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:43:22 +0100 Subject: [PATCH 3/7] Fix typo --- pvanalytics/quality/irradiance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvanalytics/quality/irradiance.py b/pvanalytics/quality/irradiance.py index 0b0ba0f09..a86245e67 100644 --- a/pvanalytics/quality/irradiance.py +++ b/pvanalytics/quality/irradiance.py @@ -88,7 +88,7 @@ def check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra, limits=None): return ghi_limit_flag -def chec_ghi_lower_limit_nollas(ghi, solar_zenith): +def check_ghi_lower_limit_nollas(ghi, solar_zenith): r"""Test for lower limit on GHI using empirical limit from Nollas (2023). Test is applied to each GHI value. A GHI value passes if value > From b494155fdfae24aa17456826f8473a8e7e008855 Mon Sep 17 00:00:00 2001 From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com> Date: Wed, 30 Nov 2022 15:07:58 +0100 Subject: [PATCH 4/7] Remove nighttime offset function --- pvanalytics/features/irradiance.py | 65 ------------------------------ 1 file changed, 65 deletions(-) delete mode 100644 pvanalytics/features/irradiance.py diff --git a/pvanalytics/features/irradiance.py b/pvanalytics/features/irradiance.py deleted file mode 100644 index 83c425b95..000000000 --- a/pvanalytics/features/irradiance.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Quality control functions for irradiance data.""" - -import numpy as np -import datetime as dt - - -def nighttime_offset_correction(irradiance, zenith, sza_night_limit=100, - label='right', midnight_method='zenith', - aggregation_method='median'): - """ - Apply nighttime correction to irradiance time series. - - Parameters - ---------- - irradiance : pd.Series - Pandas Series of irradiance data. - zenith : pd.Series - Pandas Series of zenith angles corresponding to the irradiance time - series. - sza_night_limit : float, optional - Solar zenith angle boundary limit (periods with zenith angles greater - or equal are used to compute the nighttime offset). The default is 100. - label : {'right', 'left'}, optional - Whether the timestamps correspond to the start/left or end/right of the - interval. The default is 'right'. - midnight_method : {'zenith', 'time'}, optional - Method for determining midnight. The default is 'zenith', which - assumes midnight occurs when the zenith angle is at the maximum. - aggregation_method : {'median', 'mean'}, optional - Method for calculating nighttime offset. The default is 'median'. - - Returns - ------- - corrected_irradiance : pd.Series - Pandas Series of nighttime corrected irradiance. - """ - # Raise an error if arguments are incorrect - if label not in ['right', 'left']: - raise ValueError("label must be 'right' or 'left'.") - if aggregation_method not in ['median', 'mean']: - raise ValueError("aggregation_method must be 'mean' or 'median'.") - - # Create boolean series where nighttime is one (calculated based on the - # zenith angle) - midnight_zenith = (zenith.diff().apply(np.sign).diff() < 0) - # Assign unique number to each day - day_number_zenith = midnight_zenith.cumsum() - - # Choose grouping parameter based on the midnight_method - if midnight_method == 'zenith': - grouping_category = day_number_zenith - elif midnight_method == 'time': - grouping_category = irradiance.index.date - if label == 'right': - grouping_category[irradiance.index.time == dt.time(0)] += -dt.timedelta(days=1) - else: - raise ValueError("midnight_method must be 'zenith' or 'time'.") - - # Create Pandas Series only containing nighttime irradiance - nighttime_irradiance = irradiance[zenith >= sza_night_limit] - # Calculate nighttime offset - nighttime_offset = nighttime_irradiance.groupby(grouping_category).transform(aggregation_method) - # Calculate corrected irradiance time series - corrected_irradiance = irradiance - nighttime_offset - return corrected_irradiance From 53bf66b35ba9364d0642121d15e30ee2fd5ef50c Mon Sep 17 00:00:00 2001 From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com> Date: Thu, 1 Jun 2023 19:57:09 +0200 Subject: [PATCH 5/7] Add tests --- pvanalytics/quality/irradiance.py | 115 +++++++++++++------ pvanalytics/tests/quality/test_irradiance.py | 20 ++++ 2 files changed, 97 insertions(+), 38 deletions(-) diff --git a/pvanalytics/quality/irradiance.py b/pvanalytics/quality/irradiance.py index e81e6739e..07df7b2bb 100644 --- a/pvanalytics/quality/irradiance.py +++ b/pvanalytics/quality/irradiance.py @@ -88,44 +88,6 @@ def check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra, limits=None): return ghi_limit_flag -def check_ghi_lower_limit_nollas(ghi, solar_zenith): - r"""Test for lower limit on GHI using empirical limit from Nollas (2023). - - Test is applied to each GHI value. A GHI value passes if value > - lower bound. The lower bound is from [1]_ and calculated as: - - .. math:: - lb = (6.5331 - 0.065502 * solar\_zenith + 0.00018312 * solar\_zenith^{2}) / - (1 + 0.01113*solar\_zenith) - - Parameters - ---------- - ghi : Series - Global horizontal irradiance in :math:`W/m^2` - solar_zenith : Series - Solar zenith angle in degrees - - Returns - ------- - Series - True where value passes limits test. - - References - ---------- - .. [1] `F. M. Nollas, G. A. Salazar, and C. A. Gueymard, Quality control - procedure for 1-minute pyranometric measurements of global and - shadowband-based diffuse solar irradiance, Renewable Energy, 202, - pp. 40-55, 2023. - `_ - """ - ghi_lb = ((6.5331-0.065502*solar_zenith + 0.00018312*solar_zenith**2) / - (1 + 0.01113*solar_zenith)) - - ghi_lower_limit_flag = quality.util.check_limits(value=ghi, lower_bound=ghi_lb) - - return ghi_lower_limit_flag - - def check_dhi_limits_qcrad(dhi, solar_zenith, dni_extra, limits=None): r"""Test for physical limits on DHI using the QCRad criteria. @@ -395,6 +357,83 @@ def check_irradiance_consistency_qcrad(solar_zenith, ghi, dhi, dni, return consistent_components, diffuse_ratio_limit +def _ghi_lower_limit_nollas(solar_zenith): + r"""Calculate lower limit for GHI according to Nollas et al. + + See [1]_ for further information. + + .. math:: + ghi_min = (6.5331 - 0.065502 * solar\_zenith + 0.00018312 * solar\_zenith^{2}) / + (1 + 0.01113*solar\_zenith) + + Parameters + ---------- + solar_zenith : Series + Solar zenith angle in degrees + + Returns + ------- + Series + Minimum possible GHI in :math:`W/m^2` + + References + ---------- + .. [1] F. M. Nollas, G. A. Salazar, and C. A. Gueymard, Quality control + procedure for 1-minute pyranometric measurements of global and + shadowband-based diffuse solar irradiance, Renewable Energy, 202, + pp. 40-55, 2023. + :doi:`10.1016/j.renene.2022.11.056` + """ # noqa: E501 + ghi_min = ((6.5331-0.065502*solar_zenith + 0.00018312*solar_zenith**2) / + (1 + 0.01113*solar_zenith)) + + # Set limit to nan when the sun is below the horizon + ghi_min[solar_zenith >= 90] = np.nan + + return ghi_min + + +def check_ghi_lower_limit_nollas(ghi, solar_zenith): + r"""Test for lower limit on GHI using empirical limit from Nollas (2023). + + Test is applied to each GHI value. A GHI value passes if value > + lower bound. The lower bound is from [1]_ and calculated as: + + .. math:: + lb = (6.5331 - 0.065502 * solar\_zenith + 0.00018312 * solar\_zenith^{2}) / + (1 + 0.01113*solar\_zenith) + + Parameters + ---------- + ghi : Series + Global horizontal irradiance in :math:`W/m^2` + solar_zenith : Series + Solar zenith angle in degrees + + Returns + ------- + Series + False where valuez are below the minimum limit. + + References + ---------- + .. [1] F. M. Nollas, G. A. Salazar, and C. A. Gueymard, Quality control + procedure for 1-minute pyranometric measurements of global and + shadowband-based diffuse solar irradiance, Renewable Energy, 202, + pp. 40-55, 2023. + :doi:`10.1016/j.renene.2022.11.056` + """ # noqa: E501 + ghi_lb = _ghi_lower_limit_nollas(solar_zenith) + + ghi_lower_limit_flag = quality.util.check_limits( + ghi, lower_bound=ghi_lb, inclusive_lower=False) + + # Set flags to True when ghi_lb is nan (e.g., when solar_zenith>=90) + ghi_lower_limit_flag[np.isnan(ghi_lb)] = True + + return ghi_lower_limit_flag + + def clearsky_limits(measured, clearsky, csi_max=1.1): """Identify irradiance values which do not exceed clearsky values. diff --git a/pvanalytics/tests/quality/test_irradiance.py b/pvanalytics/tests/quality/test_irradiance.py index d5f36295b..2bbb7746b 100644 --- a/pvanalytics/tests/quality/test_irradiance.py +++ b/pvanalytics/tests/quality/test_irradiance.py @@ -179,6 +179,26 @@ def test_check_irradiance_consistency_qcrad(irradiance_qcrad): check_names=False) +def test_ghi_lower_limit_nollas(irradiance_qcrad): + """Test thet minimum GHI is correctly calculated using Nollas equation.""" + data = irradiance_qcrad + actual_ghi = irradiance._ghi_lower_limit_nollas(data['solar_zenith']) + expected_ghi = pd.Series([3.548128, 3.548128, 3.548128, 6.5331, 4.7917837, + 1.9559971, 1.3039082, np.nan, 6.5331, 6.5331, + 1.9559971, 1.3039082, 1.3039082, np.nan]) + assert_series_equal(actual_ghi, expected_ghi, check_names=False) + + +def test_check_ghi_lower_limit_nollas(irradiance_qcrad): + """Test that min GHI is checked correctly.""" + data = irradiance_qcrad + actual_flags = irradiance.check_ghi_lower_limit_nollas( + data['ghi'], data['solar_zenith']) + expected_flags = pd.Series([0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + dtype=bool) + assert_series_equal(actual_flags, expected_flags, check_names=False) + + @pytest.fixture def times(): """One hour of times at 10 minute frequency. From 5f7d9091c3f888936021764bc7ccc5fc75740ab5 Mon Sep 17 00:00:00 2001 From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com> Date: Thu, 1 Jun 2023 19:57:17 +0200 Subject: [PATCH 6/7] Update api.rst --- docs/api.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 94e149b1d..2c3a800b1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -47,6 +47,14 @@ Irradiance measurements can also be checked for consistency. quality.irradiance.check_irradiance_consistency_qcrad +The validity of daytime GHI and DHI measurements can also be checked using +a more restrictive lower limit provided in [2]_. + +.. autosummary:: + :toctree: generated/ + + quality.irradiance.check_ghi_lower_limit_nollas + GHI and POA irradiance can be validated against clearsky values to eliminate data that is unrealistically high. @@ -196,6 +204,12 @@ the quality check. .. [1] C. N. Long and Y. Shi, An Automated Quality Assessment and Control Algorithm for Surface Radiation Measurements, The Open Atmospheric Science Journal 2, pp. 23-37, 2008. + :doi:`10.2174/1874282300802010023` +.. [2] F. M. Nollas, G. A. Salazar, and C. A. Gueymard, Quality control + procedure for 1-minute pyranometric measurements of global and + shadowband-based diffuse solar irradiance, Renewable Energy, 202, + pp. 40-55, 2023. + :doi:`10.1016/j.renene.2022.11.056` Features ======== From 82c4534fcf8e5d922d55f23d1baf60883beb226c Mon Sep 17 00:00:00 2001 From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com> Date: Thu, 1 Jun 2023 20:06:42 +0200 Subject: [PATCH 7/7] Update whatsnew --- docs/whatsnew/0.2.0.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/whatsnew/0.2.0.rst b/docs/whatsnew/0.2.0.rst index d93f9d83f..1cc58f1b0 100644 --- a/docs/whatsnew/0.2.0.rst +++ b/docs/whatsnew/0.2.0.rst @@ -15,7 +15,8 @@ Breaking Changes Enhancements ~~~~~~~~~~~~ - +* Add QC function for checking GHI lower limit according to Nollas et al. (2023). + :py:func:`~pvanalytics.quality.irradiance.check_ghi_lower_limit_nollas` (:pull:`174`) Bug Fixes ~~~~~~~~~ @@ -44,3 +45,4 @@ Contributors * Kevin Anderson (:ghuser:`kanderso-nrel`) * Cliff Hansen (:ghuser:`cwhanse`) * Abhishek Parikh (:ghuser:`abhisheksparikh`) +* Adam R. Jensen (:ghuser:`AdamRJensen`)