Repository: pvlib/pvanalytics
Pull request #190 (2023-05-30 09:43:55)

Add extreme QCRAD QC limits
<!-- Thank you for your contribution! Please don't hesitate to ask for help if you are unsure of how to accomplish any of the items. 
You are free to strikethrough any checklist items that do not apply or to add additional items that are not on this list. -->

- ~~[ ] Closes #xxx~~
- ~~[ ] Added tests to cover all new or modified code.~~
- [x] Clearly documented all new API functions with [PEP257](https://www.python.org/dev/peps/pep-0257/) and [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) compliant docstrings.
- ~~[ ] Added new API functions to `docs/api.rst`.~~
- ~~[ ] Non-API functions clearly documented with docstrings or comments as necessary.~~
- [x] Adds description and name entries in the appropriate "what's new" file 
      in [`docs/whatsnew`](https://github.com/pvlib/pvanalytics/tree/main/docs/whatsnew) 
      for all changes. Includes link to the GitHub Issue with `` :issue:`num` `` 
      or this Pull Request with `` :pull:`num` ``. Includes contributor name 
      and/or GitHub username (link with `` :ghuser:`user` ``).
- [x] Pull request is nearly complete and ready for detailed review.
- [x] Maintainer: Appropriate GitHub Labels and Milestone are assigned to the Pull Request and linked Issue.

<!-- Please provide a brief description of the problem and the proposed solution or new feature (if not already fully described in a linked issue): -->

The QCRad software relies on the [BSRN irradiance limits](https://epic.awi.de/30083/1/BSRN_recommended_QC_tests_V2.pdf). Specifically, QCRad uses the "physical possible" BSRN limits for the upper limit but both the "physical possible" and the "extremely rare" BSRN limits for the lower limits. Currently, pvanalytics only support the "physical possible" limits.

However, the "extremely rare" limits are widely used, e.g., by [Espinar et al. (2011)](https://hal-mines-paristech.archivesouvertesfr/hal-01493608), [Geuder et al. (2015)](http://dx.doi.org/10.1016/j.egypro.2015.03.205), [Lorenz et al. (2022)](https://doi.org/10.1016/j.solener.2021.11.023), and [Forstinger et al. (2021)](https://doi.org/10.18086/swc.2021.38.02).

In this PR I propose adding the "extremely rare" BSRN limits to the QCRad functions. Ideally, I think the functions should be named "*_bsrn* and not "*_qcrad" as QCRad states that it relies on the BSRN limits. At the current stage, the PR isn't breaking anything, but changing the function names would of course be a braking change.

cwhanse: > I propose adding the "extremely rare" BSRN limits to the QCRad functions.

+1 to this. What do you think about extending the output to a tuple (boolean series, integer series) as is being done in #167?

> Ideally, I think the functions should be named "_bsrn and not "*_qcrad" as QCRad states that it relies on the BSRN limits.

On reading that paper again, I agree. We can deprecate the existing functions and use _bsrn for functions that introduce the option to evaluate extremely rare value limits. We can address #191 in the new functions also.
AdamRJensen: > +1 to this. What do you think about extending the output to a tuple (boolean series, integer series) as is being done in #167?

I'm not sure I see the advantage of having two outputs?

> On reading that paper again, I agree. We can deprecate the existing functions and use _bsrn for functions that introduce the option to evaluate extremely rare value limits. We can address #191 in the new functions also.

Yay! 😄 

AdamRJensen: @cwhanse this year and a half PR is ready for review 
AdamRJensen: @kandersolar ready for review!

Patch:
diff --git a/docs/whatsnew/0.2.2.rst b/docs/whatsnew/0.2.2.rst
index 71eb44c..4097791 100644
--- a/docs/whatsnew/0.2.2.rst
+++ b/docs/whatsnew/0.2.2.rst
@@ -6,11 +6,16 @@
 
 Enhancements
 ~~~~~~~~~~~~
+* Added extreme limits option to
+  :py:func:`~pvanalytics.quality.irradiance.check_ghi_limits_qcrad`,
+  :py:func:`~pvanalytics.quality.irradiance.check_dhi_limits_qcrad`,
+  :py:func:`~pvanalytics.quality.irradiance.check_dni_limits_qcrad`, and
+  :py:func:`~pvanalytics.quality.irradiance.check_irradiance_limits_qcrad`.
+  (:pull:`190`)
 * Added optional keyword `outside_domain` to
   :py:func:`~pvanalytics.quality.irradiance.check_irradiance_consistency_qcrad`.
   (:pull:`214`)
 
-
 Bug Fixes
 ~~~~~~~~~
 
diff --git a/pvanalytics/quality/irradiance.py b/pvanalytics/quality/irradiance.py
index 6967805..2c70ccc 100644
--- a/pvanalytics/quality/irradiance.py
+++ b/pvanalytics/quality/irradiance.py
@@ -10,10 +10,18 @@ from pvanalytics import quality
 from pvanalytics import util
 
 
-QCRAD_LIMITS = {'ghi_ub': {'mult': 1.5, 'exp': 1.2, 'min': 100},
-                'dhi_ub': {'mult': 0.95, 'exp': 1.2, 'min': 50},
-                'dni_ub': {'mult': 1.0, 'exp': 0.0, 'min': 0},
-                'ghi_lb': -4, 'dhi_lb': -4, 'dni_lb': -4}
+# QCRAD limits are often also referred to as BSRN limits
+QCRAD_LIMITS_PHYSICAL = {  # Physically Possible Limits
+    'ghi_ub': {'mult': 1.5, 'exp': 1.2, 'min': 100},
+    'dhi_ub': {'mult': 0.95, 'exp': 1.2, 'min': 50},
+    'dni_ub': {'mult': 1.0, 'exp': 0.0, 'min': 0},
+    'ghi_lb': -4, 'dhi_lb': -4, 'dni_lb': -4}
+
+QCRAD_LIMITS_EXTREME = {  # Extremely Rare Limits
+    'ghi_ub': {'mult': 1.2, 'exp': 1.2, 'min': 50},
+    'dhi_ub': {'mult': 0.75, 'exp': 1.2, 'min': 30},
+    'dni_ub': {'mult': 0.95, 'exp': 0.2, 'min': 10},
+    'ghi_lb': -2, 'dhi_lb': -2, 'dni_lb': -2}
 
 QCRAD_CONSISTENCY = {
     'ghi_ratio': {
@@ -42,8 +50,8 @@ def _qcrad_ub(dni_extra, sza, lim):
     return lim['mult'] * dni_extra * cosd_sza**lim['exp'] + lim['min']
 
 
-def check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra, limits=None):
-    r"""Test for physical limits on GHI using the QCRad criteria.
+def check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra, limits='physical'):
+    r"""Test for lower and upper limits on GHI using the QCRad criteria.
 
     Test is applied to each GHI value. A GHI value passes if value >
     lower bound and value < upper bound. Lower bounds are constant for
@@ -60,10 +68,11 @@ def check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra, limits=None):
         Solar zenith angle in degrees
     dni_extra : Series
         Extraterrestrial normal irradiance in :math:`W/m^2`
-    limits : dict, default QCRAD_LIMITS
-        Must have keys 'ghi_ub' and 'ghi_lb'. For 'ghi_ub' value is a
-        dict with keys {'mult', 'exp', 'min'} and float values. For
-        'ghi_lb' value is a float.
+    limits : {'physical', 'extreme'} or dict, default 'physical'
+        If string, must be either 'physical' or 'extreme', corresponding to the
+        QCRAD QC limits. If dict, must have keys 'ghi_ub' and 'ghi_lb'. For
+        'ghi_ub' value is a dict with keys {'mult', 'exp', 'min'} and float
+        values. For 'ghi_lb' value is a float.
 
     Returns
     -------
@@ -79,8 +88,11 @@ def check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra, limits=None):
     for more information.
 
     """
-    if not limits:
-        limits = QCRAD_LIMITS
+    if limits == 'physical':
+        limits = QCRAD_LIMITS_PHYSICAL
+    elif limits == 'extreme':
+        limits = QCRAD_LIMITS_EXTREME
+
     ghi_ub = _qcrad_ub(dni_extra, solar_zenith, limits['ghi_ub'])
 
     ghi_limit_flag = quality.util.check_limits(ghi, limits['ghi_lb'], ghi_ub)
@@ -88,8 +100,8 @@ def check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra, limits=None):
     return ghi_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.
+def check_dhi_limits_qcrad(dhi, solar_zenith, dni_extra, limits='physical'):
+    r"""Test for lower and upper limits on DHI using the QCRad criteria.
 
     Test is applied to each DHI value. A DHI value passes if value >
     lower bound and value < upper bound. Lower bounds are constant for
@@ -106,10 +118,11 @@ def check_dhi_limits_qcrad(dhi, solar_zenith, dni_extra, limits=None):
         Solar zenith angle in degrees
     dni_extra : Series
         Extraterrestrial normal irradiance in :math:`W/m^2`
-    limits : dict, default QCRAD_LIMITS
-        Must have keys 'dhi_ub' and 'dhi_lb'. For 'dhi_ub' value is a
-        dict with keys {'mult', 'exp', 'min'} and float values. For
-        'dhi_lb' value is a float.
+    limits : {'physical', 'extreme'} or dict, default 'physical'
+        If string, must be either 'physical' or 'extreme', corresponding to the
+        QCRAD QC limits. If dict, must have keys 'dhi_ub' and 'dhi_lb'. For
+        'dhi_ub' value is a dict with keys {'mult', 'exp', 'min'} and float
+        values. For 'dhi_lb' value is a float.
 
     Returns
     -------
@@ -125,8 +138,10 @@ def check_dhi_limits_qcrad(dhi, solar_zenith, dni_extra, limits=None):
     for more information.
 
     """
-    if not limits:
-        limits = QCRAD_LIMITS
+    if limits == 'physical':
+        limits = QCRAD_LIMITS_PHYSICAL
+    elif limits == 'extreme':
+        limits = QCRAD_LIMITS_EXTREME
 
     dhi_ub = _qcrad_ub(dni_extra, solar_zenith, limits['dhi_ub'])
 
@@ -135,8 +150,8 @@ def check_dhi_limits_qcrad(dhi, solar_zenith, dni_extra, limits=None):
     return dhi_limit_flag
 
 
-def check_dni_limits_qcrad(dni, solar_zenith, dni_extra, limits=None):
-    r"""Test for physical limits on DNI using the QCRad criteria.
+def check_dni_limits_qcrad(dni, solar_zenith, dni_extra, limits='physical'):
+    r"""Test for lower and upper limits on DNI using the QCRad criteria.
 
     Test is applied to each DNI value. A DNI value passes if value >
     lower bound and value < upper bound. Lower bounds are constant for
@@ -153,10 +168,11 @@ def check_dni_limits_qcrad(dni, solar_zenith, dni_extra, limits=None):
         Solar zenith angle in degrees
     dni_extra : Series
         Extraterrestrial normal irradiance in :math:`W/m^2`
-    limits : dict, default QCRAD_LIMITS
-        Must have keys 'dni_ub' and 'dni_lb'. For 'dni_ub' value is a
-        dict with keys {'mult', 'exp', 'min'} and float values. For
-        'dni_lb' value is a float.
+    limits : {'physical', 'extreme'} or dict, default 'physical'
+        If string, must be either 'physical' or 'extreme', corresponding to the
+        QCRAD QC limits. If dict, must have keys 'dni_ub' and 'dni_lb'. For
+        'dni_ub' value is a dict with keys {'mult', 'exp', 'min'} and float
+        values. For 'dni_lb' value is a float.
 
     Returns
     -------
@@ -172,8 +188,10 @@ def check_dni_limits_qcrad(dni, solar_zenith, dni_extra, limits=None):
     for more information.
 
     """
-    if not limits:
-        limits = QCRAD_LIMITS
+    if limits == 'physical':
+        limits = QCRAD_LIMITS_PHYSICAL
+    elif limits == 'extreme':
+        limits = QCRAD_LIMITS_EXTREME
 
     dni_ub = _qcrad_ub(dni_extra, solar_zenith, limits['dni_ub'])
 
@@ -183,10 +201,10 @@ def check_dni_limits_qcrad(dni, solar_zenith, dni_extra, limits=None):
 
 
 def check_irradiance_limits_qcrad(solar_zenith, dni_extra, ghi=None, dhi=None,
-                                  dni=None, limits=None):
+                                  dni=None, limits='physical'):
     r"""Test for physical limits on GHI, DHI or DNI using the QCRad criteria.
 
-    Criteria from [1]_ are used to determine physically plausible
+    Criteria from [1]_ and [2]_ are used to determine physically plausible
     lower and upper bounds. Each value is tested and a value passes if
     value > lower bound and value < upper bound. Lower bounds are
     constant for all tests. Upper bounds are calculated as
@@ -209,10 +227,13 @@ def check_irradiance_limits_qcrad(solar_zenith, dni_extra, ghi=None, dhi=None,
         Diffuse horizontal irradiance in :math:`W/m^2`
     dni : Series or None, default None
         Direct normal irradiance in :math:`W/m^2`
-    limits : dict, default QCRAD_LIMITS
-        for keys 'ghi_ub', 'dhi_ub', 'dni_ub', value is a dict with
-        keys {'mult', 'exp', 'min'} and float values. For keys
-        'ghi_lb', 'dhi_lb', 'dni_lb', value is a float.
+    limits : {'physical', 'extreme'} or dict, default 'physical'
+        If string, must be either 'physical' or 'extreme', corresponding to the
+        QCRAD QC limits. If dict, must have keys:
+
+            * 'ghi_ub', 'dhi_ub', 'dni_ub': dicts with keys
+              {'mult', 'exp', 'min'} and float values.
+            * 'ghi_lb', 'dhi_lb', 'dni_lb': float values.
 
     Returns
     -------
@@ -233,13 +254,19 @@ def check_irradiance_limits_qcrad(solar_zenith, dni_extra, ghi=None, dhi=None,
 
     References
     ----------
-    .. [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.
-
-    """
-    if not limits:
-        limits = QCRAD_LIMITS
+    .. [1] C. N. Long and Y. Shi, "An Automated Quality Assessment and Control
+       Algorithm for Surface Radiation Measurements," The Open Atmospheric
+       Science Journal, vol. 2, no. 1. Bentham Science Publishers Ltd.,
+       pp. 23–37, Apr. 18, 2008. :doi:`10.2174/1874282300802010023`.
+    .. [2]  C. N. Long and E. G. Dutton, "BSRN Global Network recommended QC
+       tests, V2.0," Baseline Surface Radiation Network (BSRN),
+       Accessed: Oct. 24, 2024. [Online.] Available:
+       `<https://bsrn.awi.de/fileadmin/user_upload/bsrn.awi.de/Publications/BSRN_recommended_QC_tests_V2.pdf>`_
+    """  # noqa: E501
+    if limits == 'physical':
+        limits = QCRAD_LIMITS_PHYSICAL
+    elif limits == 'extreme':
+        limits = QCRAD_LIMITS_EXTREME
 
     if ghi is not None:
         ghi_limit_flag = check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra,
@@ -289,8 +316,8 @@ def check_irradiance_consistency_qcrad(solar_zenith, ghi, dhi, dni,
                                        param=None, outside_domain=False):
     r"""Check consistency of GHI, DHI and DNI using QCRad criteria.
 
-    Uses criteria given in [1]_ to validate the ratio of irradiance
-    components.
+    Uses criteria given in [1]_ to validate the ratio of irradiance components.
+    These tests are equivalent to the BSRN comparison tests [2]_.
 
     .. warning:: Not valid for night time or low irradiance. When the input
        data fall outside the test domain, the returned value is set by the
@@ -342,11 +369,15 @@ def check_irradiance_consistency_qcrad(solar_zenith, ghi, dhi, dni,
 
     References
     ----------
-    .. [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.
-
-    """
+    .. [1] C. N. Long and Y. Shi, "An Automated Quality Assessment and Control
+       Algorithm for Surface Radiation Measurements," The Open Atmospheric
+       Science Journal, vol. 2, no. 1. Bentham Science Publishers Ltd.,
+       pp. 23–37, Apr. 18, 2008. :doi:`10.2174/1874282300802010023`.
+    .. [2]  C. N. Long and E. G. Dutton, "BSRN Global Network recommended QC
+       tests, V2.0," Baseline Surface Radiation Network (BSRN),
+       Accessed: Oct. 24, 2024. [Online.] Available:
+       https://bsrn.awi.de/fileadmin/user_upload/bsrn.awi.de/Publications/BSRN_recommended_QC_tests_V2.pdf
+    """  # noqa: E501
     if not param:
         param = QCRAD_CONSISTENCY
 


Test patch:
diff --git a/pvanalytics/tests/quality/test_irradiance.py b/pvanalytics/tests/quality/test_irradiance.py
index 2b28e1f..8d3ba21 100644
--- a/pvanalytics/tests/quality/test_irradiance.py
+++ b/pvanalytics/tests/quality/test_irradiance.py
@@ -42,28 +42,31 @@ def irradiance_qcrad():
     output = pd.DataFrame(
         columns=['ghi', 'dhi', 'dni', 'solar_zenith', 'dni_extra',
                  'ghi_limit_flag', 'dhi_limit_flag', 'dni_limit_flag',
+                 'ghi_extreme_limit_flag', 'dhi_extreme_limit_flag',
+                 'dni_extreme_limit_flag',
                  'consistent_components', 'diffuse_ratio_limit',
                  'consistent_components_outside_domain',
                  'diffuse_ratio_limit_outside_domain',
                  ],
-        data=np.array([[-100, 100, 100, 30, 1370, 0, 1, 1, 0, 0, 0, 1],
-                       [100, -100, 100, 30, 1370, 1, 0, 1, 0, 0, 1, 0],
-                       [100, 100, -100, 30, 1370, 1, 1, 0, 0, 1, 1, 1],
-                       [1000, 100, 900, 0, 1370, 1, 1, 1, 1, 1, 1, 1],
-                       [1000, 200, 800, 15, 1370, 1, 1, 1, 1, 1, 1, 1],
-                       [1000, 200, 800, 60, 1370, 0, 1, 1, 0, 1, 0, 1],
-                       [1000, 300, 850, 80, 1370, 0, 0, 1, 0, 1, 0, 1],
-                       [1000, 500, 800, 90, 1370, 0, 0, 1, 0, 1, 0, 1],
-                       [500, 100, 1100, 0, 1370, 1, 1, 1, 0, 1, 0, 1],
-                       [1000, 300, 1200, 0, 1370, 1, 1, 1, 0, 1, 0, 1],
-                       [500, 600, 100, 60, 1370, 1, 1, 1, 0, 0, 0, 0],
-                       [500, 600, 400, 80, 1370, 0, 0, 1, 0, 0, 0, 0],
-                       [500, 500, 300, 80, 1370, 0, 0, 1, 1, 1, 1, 1],
-                       [0, 0, 0, 93, 1370, 1, 1, 1, 0, 0, 1, 1],
-                       [100, 100, 0, 95, 1370, 0, 0, 1, 0, 0, 1, 1],
+        data=np.array([[-100, 100, 100, 30, 1370, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1],  # noqa: E501
+                       [100, -100, 100, 30, 1370, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0],  # noqa: E501
+                       [100, 100, -100, 30, 1370, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1],  # noqa: E501
+                       [1000, 100, 900, 0, 1370, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+                       [1000, 200, 800, 15, 1370, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],  # noqa: E501
+                       [1000, 200, 800, 60, 1370, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1],  # noqa: E501
+                       [1000, 300, 850, 80, 1370, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1],  # noqa: E501
+                       [1000, 500, 800, 90, 1370, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1],  # noqa: E501
+                       [500, 100, 1100, 0, 1370, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1],
+                       [1000, 300, 1200, 0, 1370, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1],  # noqa: E501
+                       [500, 600, 100, 60, 1370, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0],
+                       [500, 600, 400, 80, 1370, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0],
+                       [500, 500, 300, 80, 1370, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1],
+                       [0, 0, 0, 93, 1370, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1],
+                       [100, 100, 0, 95, 1370, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1]
                        ]))
     dtypes = ['float64', 'float64', 'float64', 'float64', 'float64',
-              'bool', 'bool', 'bool', 'bool', 'bool', 'bool', 'bool']
+              'bool', 'bool', 'bool', 'bool', 'bool', 'bool', 'bool', 'bool',
+              'bool', 'bool']
     for (col, typ) in zip(output.columns, dtypes):
         output[col] = output[col].astype(typ)
     return output
@@ -88,6 +91,13 @@ def test_check_ghi_limits_qcrad(irradiance_qcrad):
                                                 expected['dni_extra'])
     assert_series_equal(ghi_out, ghi_out_expected, check_names=False)
 
+    ghi_out_extreme = irradiance.check_ghi_limits_qcrad(
+        expected['ghi'], expected['solar_zenith'], expected['dni_extra'],
+        limits='extreme')
+    ghi_extreme_out_expected = expected['ghi_extreme_limit_flag']
+    assert_series_equal(
+        ghi_out_extreme, ghi_extreme_out_expected, check_names=False)
+
 
 def test_check_dhi_limits_qcrad(irradiance_qcrad):
     """Test that QCRad identifies out of bounds DHI values.
@@ -108,6 +118,13 @@ def test_check_dhi_limits_qcrad(irradiance_qcrad):
                                                 expected['dni_extra'])
     assert_series_equal(dhi_out, dhi_out_expected, check_names=False)
 
+    dhi_out_extreme = irradiance.check_dhi_limits_qcrad(
+        expected['dhi'], expected['solar_zenith'], expected['dni_extra'],
+        limits='extreme')
+    dhi_extreme_out_expected = expected['dhi_extreme_limit_flag']
+    assert_series_equal(
+        dhi_out_extreme, dhi_extreme_out_expected, check_names=False)
+
 
 def test_check_dni_limits_qcrad(irradiance_qcrad):
     """Test that QCRad identifies out of bounds DNI values.
@@ -128,6 +145,13 @@ def test_check_dni_limits_qcrad(irradiance_qcrad):
                                                 expected['dni_extra'])
     assert_series_equal(dni_out, dni_out_expected, check_names=False)
 
+    dni_out_extreme = irradiance.check_dni_limits_qcrad(
+        expected['dni'], expected['solar_zenith'], expected['dni_extra'],
+        limits='extreme')
+    dni_extreme_out_expected = expected['dni_extreme_limit_flag']
+    assert_series_equal(
+        dni_out_extreme, dni_extreme_out_expected, check_names=False)
+
 
 def test_check_irradiance_limits_qcrad(irradiance_qcrad):
     """Test different input combinations to check_irradiance_limits_qcrad.
@@ -162,6 +186,20 @@ def test_check_irradiance_limits_qcrad(irradiance_qcrad):
     assert_series_equal(dni_out, dni_out_expected, check_names=False)
 
 
+def test_check_irradiance_limits_qcrad_extreme(irradiance_qcrad):
+    """Test different input combinations to check_irradiance_limits_qcrad.
+    """
+    expected = irradiance_qcrad
+
+    ghi_out, dhi_out, dni_out = irradiance.check_irradiance_limits_qcrad(
+        expected['solar_zenith'], expected['dni_extra'], ghi=expected['ghi'],
+        dhi=expected['dhi'], dni=expected['dni'], limits='extreme')
+
+    assert_series_equal(ghi_out, expected['ghi_extreme_limit_flag'], check_names=False)  # noqa: E501
+    assert_series_equal(dhi_out, expected['dhi_extreme_limit_flag'], check_names=False)  # noqa: E501
+    assert_series_equal(dni_out, expected['dni_extreme_limit_flag'], check_names=False)  # noqa: E501
+
+
 def test_check_irradiance_consistency_qcrad(irradiance_qcrad):
     """Test that QCRad identifies consistent irradiance measurements.