Skip to content

Commit 12f16d6

Browse files
AdamRJensencwhansekandersolar
authored
Add extreme QCRAD QC limits (#190)
* Add extreme BSRN limits * Add test * Change BSRN to QCRAD * Update tests * Update citations * Add whatsnew * Fix linter * Fix typo * Update docs/whatsnew/0.2.2.rst * Fix typo Co-authored-by: Cliff Hansen <[email protected]> * Update limits docstring from review Co-authored-by: Kevin Anderson <[email protected]> * Implement review feedback * Remove <>_ from link * Add bsrn comment to consistency tests --------- Co-authored-by: Cliff Hansen <[email protected]> Co-authored-by: Kevin Anderson <[email protected]>
1 parent bf9eff6 commit 12f16d6

File tree

3 files changed

+139
-65
lines changed

3 files changed

+139
-65
lines changed

docs/whatsnew/0.2.2.rst

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66

77
Enhancements
88
~~~~~~~~~~~~
9+
* Added extreme limits option to
10+
:py:func:`~pvanalytics.quality.irradiance.check_ghi_limits_qcrad`,
11+
:py:func:`~pvanalytics.quality.irradiance.check_dhi_limits_qcrad`,
12+
:py:func:`~pvanalytics.quality.irradiance.check_dni_limits_qcrad`, and
13+
:py:func:`~pvanalytics.quality.irradiance.check_irradiance_limits_qcrad`.
14+
(:pull:`190`)
915
* Added optional keyword `outside_domain` to
1016
:py:func:`~pvanalytics.quality.irradiance.check_irradiance_consistency_qcrad`.
1117
(:pull:`214`)
1218

13-
1419
Bug Fixes
1520
~~~~~~~~~
1621

pvanalytics/quality/irradiance.py

+79-48
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@
1010
from pvanalytics import util
1111

1212

13-
QCRAD_LIMITS = {'ghi_ub': {'mult': 1.5, 'exp': 1.2, 'min': 100},
14-
'dhi_ub': {'mult': 0.95, 'exp': 1.2, 'min': 50},
15-
'dni_ub': {'mult': 1.0, 'exp': 0.0, 'min': 0},
16-
'ghi_lb': -4, 'dhi_lb': -4, 'dni_lb': -4}
13+
# QCRAD limits are often also referred to as BSRN limits
14+
QCRAD_LIMITS_PHYSICAL = { # Physically Possible Limits
15+
'ghi_ub': {'mult': 1.5, 'exp': 1.2, 'min': 100},
16+
'dhi_ub': {'mult': 0.95, 'exp': 1.2, 'min': 50},
17+
'dni_ub': {'mult': 1.0, 'exp': 0.0, 'min': 0},
18+
'ghi_lb': -4, 'dhi_lb': -4, 'dni_lb': -4}
19+
20+
QCRAD_LIMITS_EXTREME = { # Extremely Rare Limits
21+
'ghi_ub': {'mult': 1.2, 'exp': 1.2, 'min': 50},
22+
'dhi_ub': {'mult': 0.75, 'exp': 1.2, 'min': 30},
23+
'dni_ub': {'mult': 0.95, 'exp': 0.2, 'min': 10},
24+
'ghi_lb': -2, 'dhi_lb': -2, 'dni_lb': -2}
1725

1826
QCRAD_CONSISTENCY = {
1927
'ghi_ratio': {
@@ -42,8 +50,8 @@ def _qcrad_ub(dni_extra, sza, lim):
4250
return lim['mult'] * dni_extra * cosd_sza**lim['exp'] + lim['min']
4351

4452

45-
def check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra, limits=None):
46-
r"""Test for physical limits on GHI using the QCRad criteria.
53+
def check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra, limits='physical'):
54+
r"""Test for lower and upper limits on GHI using the QCRad criteria.
4755
4856
Test is applied to each GHI value. A GHI value passes if value >
4957
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):
6068
Solar zenith angle in degrees
6169
dni_extra : Series
6270
Extraterrestrial normal irradiance in :math:`W/m^2`
63-
limits : dict, default QCRAD_LIMITS
64-
Must have keys 'ghi_ub' and 'ghi_lb'. For 'ghi_ub' value is a
65-
dict with keys {'mult', 'exp', 'min'} and float values. For
66-
'ghi_lb' value is a float.
71+
limits : {'physical', 'extreme'} or dict, default 'physical'
72+
If string, must be either 'physical' or 'extreme', corresponding to the
73+
QCRAD QC limits. If dict, must have keys 'ghi_ub' and 'ghi_lb'. For
74+
'ghi_ub' value is a dict with keys {'mult', 'exp', 'min'} and float
75+
values. For 'ghi_lb' value is a float.
6776
6877
Returns
6978
-------
@@ -79,17 +88,20 @@ def check_ghi_limits_qcrad(ghi, solar_zenith, dni_extra, limits=None):
7988
for more information.
8089
8190
"""
82-
if not limits:
83-
limits = QCRAD_LIMITS
91+
if limits == 'physical':
92+
limits = QCRAD_LIMITS_PHYSICAL
93+
elif limits == 'extreme':
94+
limits = QCRAD_LIMITS_EXTREME
95+
8496
ghi_ub = _qcrad_ub(dni_extra, solar_zenith, limits['ghi_ub'])
8597

8698
ghi_limit_flag = quality.util.check_limits(ghi, limits['ghi_lb'], ghi_ub)
8799

88100
return ghi_limit_flag
89101

90102

91-
def check_dhi_limits_qcrad(dhi, solar_zenith, dni_extra, limits=None):
92-
r"""Test for physical limits on DHI using the QCRad criteria.
103+
def check_dhi_limits_qcrad(dhi, solar_zenith, dni_extra, limits='physical'):
104+
r"""Test for lower and upper limits on DHI using the QCRad criteria.
93105
94106
Test is applied to each DHI value. A DHI value passes if value >
95107
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):
106118
Solar zenith angle in degrees
107119
dni_extra : Series
108120
Extraterrestrial normal irradiance in :math:`W/m^2`
109-
limits : dict, default QCRAD_LIMITS
110-
Must have keys 'dhi_ub' and 'dhi_lb'. For 'dhi_ub' value is a
111-
dict with keys {'mult', 'exp', 'min'} and float values. For
112-
'dhi_lb' value is a float.
121+
limits : {'physical', 'extreme'} or dict, default 'physical'
122+
If string, must be either 'physical' or 'extreme', corresponding to the
123+
QCRAD QC limits. If dict, must have keys 'dhi_ub' and 'dhi_lb'. For
124+
'dhi_ub' value is a dict with keys {'mult', 'exp', 'min'} and float
125+
values. For 'dhi_lb' value is a float.
113126
114127
Returns
115128
-------
@@ -125,8 +138,10 @@ def check_dhi_limits_qcrad(dhi, solar_zenith, dni_extra, limits=None):
125138
for more information.
126139
127140
"""
128-
if not limits:
129-
limits = QCRAD_LIMITS
141+
if limits == 'physical':
142+
limits = QCRAD_LIMITS_PHYSICAL
143+
elif limits == 'extreme':
144+
limits = QCRAD_LIMITS_EXTREME
130145

131146
dhi_ub = _qcrad_ub(dni_extra, solar_zenith, limits['dhi_ub'])
132147

@@ -135,8 +150,8 @@ def check_dhi_limits_qcrad(dhi, solar_zenith, dni_extra, limits=None):
135150
return dhi_limit_flag
136151

137152

138-
def check_dni_limits_qcrad(dni, solar_zenith, dni_extra, limits=None):
139-
r"""Test for physical limits on DNI using the QCRad criteria.
153+
def check_dni_limits_qcrad(dni, solar_zenith, dni_extra, limits='physical'):
154+
r"""Test for lower and upper limits on DNI using the QCRad criteria.
140155
141156
Test is applied to each DNI value. A DNI value passes if value >
142157
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):
153168
Solar zenith angle in degrees
154169
dni_extra : Series
155170
Extraterrestrial normal irradiance in :math:`W/m^2`
156-
limits : dict, default QCRAD_LIMITS
157-
Must have keys 'dni_ub' and 'dni_lb'. For 'dni_ub' value is a
158-
dict with keys {'mult', 'exp', 'min'} and float values. For
159-
'dni_lb' value is a float.
171+
limits : {'physical', 'extreme'} or dict, default 'physical'
172+
If string, must be either 'physical' or 'extreme', corresponding to the
173+
QCRAD QC limits. If dict, must have keys 'dni_ub' and 'dni_lb'. For
174+
'dni_ub' value is a dict with keys {'mult', 'exp', 'min'} and float
175+
values. For 'dni_lb' value is a float.
160176
161177
Returns
162178
-------
@@ -172,8 +188,10 @@ def check_dni_limits_qcrad(dni, solar_zenith, dni_extra, limits=None):
172188
for more information.
173189
174190
"""
175-
if not limits:
176-
limits = QCRAD_LIMITS
191+
if limits == 'physical':
192+
limits = QCRAD_LIMITS_PHYSICAL
193+
elif limits == 'extreme':
194+
limits = QCRAD_LIMITS_EXTREME
177195

178196
dni_ub = _qcrad_ub(dni_extra, solar_zenith, limits['dni_ub'])
179197

@@ -183,10 +201,10 @@ def check_dni_limits_qcrad(dni, solar_zenith, dni_extra, limits=None):
183201

184202

185203
def check_irradiance_limits_qcrad(solar_zenith, dni_extra, ghi=None, dhi=None,
186-
dni=None, limits=None):
204+
dni=None, limits='physical'):
187205
r"""Test for physical limits on GHI, DHI or DNI using the QCRad criteria.
188206
189-
Criteria from [1]_ are used to determine physically plausible
207+
Criteria from [1]_ and [2]_ are used to determine physically plausible
190208
lower and upper bounds. Each value is tested and a value passes if
191209
value > lower bound and value < upper bound. Lower bounds are
192210
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,
209227
Diffuse horizontal irradiance in :math:`W/m^2`
210228
dni : Series or None, default None
211229
Direct normal irradiance in :math:`W/m^2`
212-
limits : dict, default QCRAD_LIMITS
213-
for keys 'ghi_ub', 'dhi_ub', 'dni_ub', value is a dict with
214-
keys {'mult', 'exp', 'min'} and float values. For keys
215-
'ghi_lb', 'dhi_lb', 'dni_lb', value is a float.
230+
limits : {'physical', 'extreme'} or dict, default 'physical'
231+
If string, must be either 'physical' or 'extreme', corresponding to the
232+
QCRAD QC limits. If dict, must have keys:
233+
234+
* 'ghi_ub', 'dhi_ub', 'dni_ub': dicts with keys
235+
{'mult', 'exp', 'min'} and float values.
236+
* 'ghi_lb', 'dhi_lb', 'dni_lb': float values.
216237
217238
Returns
218239
-------
@@ -233,13 +254,19 @@ def check_irradiance_limits_qcrad(solar_zenith, dni_extra, ghi=None, dhi=None,
233254
234255
References
235256
----------
236-
.. [1] C. N. Long and Y. Shi, An Automated Quality Assessment and Control
237-
Algorithm for Surface Radiation Measurements, The Open Atmospheric
238-
Science Journal 2, pp. 23-37, 2008.
239-
240-
"""
241-
if not limits:
242-
limits = QCRAD_LIMITS
257+
.. [1] C. N. Long and Y. Shi, "An Automated Quality Assessment and Control
258+
Algorithm for Surface Radiation Measurements," The Open Atmospheric
259+
Science Journal, vol. 2, no. 1. Bentham Science Publishers Ltd.,
260+
pp. 23–37, Apr. 18, 2008. :doi:`10.2174/1874282300802010023`.
261+
.. [2] C. N. Long and E. G. Dutton, "BSRN Global Network recommended QC
262+
tests, V2.0," Baseline Surface Radiation Network (BSRN),
263+
Accessed: Oct. 24, 2024. [Online.] Available:
264+
`<https://bsrn.awi.de/fileadmin/user_upload/bsrn.awi.de/Publications/BSRN_recommended_QC_tests_V2.pdf>`_
265+
""" # noqa: E501
266+
if limits == 'physical':
267+
limits = QCRAD_LIMITS_PHYSICAL
268+
elif limits == 'extreme':
269+
limits = QCRAD_LIMITS_EXTREME
243270

244271
if ghi is not None:
245272
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,
289316
param=None, outside_domain=False):
290317
r"""Check consistency of GHI, DHI and DNI using QCRad criteria.
291318
292-
Uses criteria given in [1]_ to validate the ratio of irradiance
293-
components.
319+
Uses criteria given in [1]_ to validate the ratio of irradiance components.
320+
These tests are equivalent to the BSRN comparison tests [2]_.
294321
295322
.. warning:: Not valid for night time or low irradiance. When the input
296323
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,
342369
343370
References
344371
----------
345-
.. [1] C. N. Long and Y. Shi, An Automated Quality Assessment and Control
346-
Algorithm for Surface Radiation Measurements, The Open Atmospheric
347-
Science Journal 2, pp. 23-37, 2008.
348-
349-
"""
372+
.. [1] C. N. Long and Y. Shi, "An Automated Quality Assessment and Control
373+
Algorithm for Surface Radiation Measurements," The Open Atmospheric
374+
Science Journal, vol. 2, no. 1. Bentham Science Publishers Ltd.,
375+
pp. 23–37, Apr. 18, 2008. :doi:`10.2174/1874282300802010023`.
376+
.. [2] C. N. Long and E. G. Dutton, "BSRN Global Network recommended QC
377+
tests, V2.0," Baseline Surface Radiation Network (BSRN),
378+
Accessed: Oct. 24, 2024. [Online.] Available:
379+
https://bsrn.awi.de/fileadmin/user_upload/bsrn.awi.de/Publications/BSRN_recommended_QC_tests_V2.pdf
380+
""" # noqa: E501
350381
if not param:
351382
param = QCRAD_CONSISTENCY
352383

pvanalytics/tests/quality/test_irradiance.py

+54-16
Original file line numberDiff line numberDiff line change
@@ -42,28 +42,31 @@ def irradiance_qcrad():
4242
output = pd.DataFrame(
4343
columns=['ghi', 'dhi', 'dni', 'solar_zenith', 'dni_extra',
4444
'ghi_limit_flag', 'dhi_limit_flag', 'dni_limit_flag',
45+
'ghi_extreme_limit_flag', 'dhi_extreme_limit_flag',
46+
'dni_extreme_limit_flag',
4547
'consistent_components', 'diffuse_ratio_limit',
4648
'consistent_components_outside_domain',
4749
'diffuse_ratio_limit_outside_domain',
4850
],
49-
data=np.array([[-100, 100, 100, 30, 1370, 0, 1, 1, 0, 0, 0, 1],
50-
[100, -100, 100, 30, 1370, 1, 0, 1, 0, 0, 1, 0],
51-
[100, 100, -100, 30, 1370, 1, 1, 0, 0, 1, 1, 1],
52-
[1000, 100, 900, 0, 1370, 1, 1, 1, 1, 1, 1, 1],
53-
[1000, 200, 800, 15, 1370, 1, 1, 1, 1, 1, 1, 1],
54-
[1000, 200, 800, 60, 1370, 0, 1, 1, 0, 1, 0, 1],
55-
[1000, 300, 850, 80, 1370, 0, 0, 1, 0, 1, 0, 1],
56-
[1000, 500, 800, 90, 1370, 0, 0, 1, 0, 1, 0, 1],
57-
[500, 100, 1100, 0, 1370, 1, 1, 1, 0, 1, 0, 1],
58-
[1000, 300, 1200, 0, 1370, 1, 1, 1, 0, 1, 0, 1],
59-
[500, 600, 100, 60, 1370, 1, 1, 1, 0, 0, 0, 0],
60-
[500, 600, 400, 80, 1370, 0, 0, 1, 0, 0, 0, 0],
61-
[500, 500, 300, 80, 1370, 0, 0, 1, 1, 1, 1, 1],
62-
[0, 0, 0, 93, 1370, 1, 1, 1, 0, 0, 1, 1],
63-
[100, 100, 0, 95, 1370, 0, 0, 1, 0, 0, 1, 1],
51+
data=np.array([[-100, 100, 100, 30, 1370, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1], # noqa: E501
52+
[100, -100, 100, 30, 1370, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0], # noqa: E501
53+
[100, 100, -100, 30, 1370, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1], # noqa: E501
54+
[1000, 100, 900, 0, 1370, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
55+
[1000, 200, 800, 15, 1370, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # noqa: E501
56+
[1000, 200, 800, 60, 1370, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1], # noqa: E501
57+
[1000, 300, 850, 80, 1370, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1], # noqa: E501
58+
[1000, 500, 800, 90, 1370, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1], # noqa: E501
59+
[500, 100, 1100, 0, 1370, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1],
60+
[1000, 300, 1200, 0, 1370, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1], # noqa: E501
61+
[500, 600, 100, 60, 1370, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0],
62+
[500, 600, 400, 80, 1370, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0],
63+
[500, 500, 300, 80, 1370, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1],
64+
[0, 0, 0, 93, 1370, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1],
65+
[100, 100, 0, 95, 1370, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1]
6466
]))
6567
dtypes = ['float64', 'float64', 'float64', 'float64', 'float64',
66-
'bool', 'bool', 'bool', 'bool', 'bool', 'bool', 'bool']
68+
'bool', 'bool', 'bool', 'bool', 'bool', 'bool', 'bool', 'bool',
69+
'bool', 'bool']
6770
for (col, typ) in zip(output.columns, dtypes):
6871
output[col] = output[col].astype(typ)
6972
return output
@@ -88,6 +91,13 @@ def test_check_ghi_limits_qcrad(irradiance_qcrad):
8891
expected['dni_extra'])
8992
assert_series_equal(ghi_out, ghi_out_expected, check_names=False)
9093

94+
ghi_out_extreme = irradiance.check_ghi_limits_qcrad(
95+
expected['ghi'], expected['solar_zenith'], expected['dni_extra'],
96+
limits='extreme')
97+
ghi_extreme_out_expected = expected['ghi_extreme_limit_flag']
98+
assert_series_equal(
99+
ghi_out_extreme, ghi_extreme_out_expected, check_names=False)
100+
91101

92102
def test_check_dhi_limits_qcrad(irradiance_qcrad):
93103
"""Test that QCRad identifies out of bounds DHI values.
@@ -108,6 +118,13 @@ def test_check_dhi_limits_qcrad(irradiance_qcrad):
108118
expected['dni_extra'])
109119
assert_series_equal(dhi_out, dhi_out_expected, check_names=False)
110120

121+
dhi_out_extreme = irradiance.check_dhi_limits_qcrad(
122+
expected['dhi'], expected['solar_zenith'], expected['dni_extra'],
123+
limits='extreme')
124+
dhi_extreme_out_expected = expected['dhi_extreme_limit_flag']
125+
assert_series_equal(
126+
dhi_out_extreme, dhi_extreme_out_expected, check_names=False)
127+
111128

112129
def test_check_dni_limits_qcrad(irradiance_qcrad):
113130
"""Test that QCRad identifies out of bounds DNI values.
@@ -128,6 +145,13 @@ def test_check_dni_limits_qcrad(irradiance_qcrad):
128145
expected['dni_extra'])
129146
assert_series_equal(dni_out, dni_out_expected, check_names=False)
130147

148+
dni_out_extreme = irradiance.check_dni_limits_qcrad(
149+
expected['dni'], expected['solar_zenith'], expected['dni_extra'],
150+
limits='extreme')
151+
dni_extreme_out_expected = expected['dni_extreme_limit_flag']
152+
assert_series_equal(
153+
dni_out_extreme, dni_extreme_out_expected, check_names=False)
154+
131155

132156
def test_check_irradiance_limits_qcrad(irradiance_qcrad):
133157
"""Test different input combinations to check_irradiance_limits_qcrad.
@@ -162,6 +186,20 @@ def test_check_irradiance_limits_qcrad(irradiance_qcrad):
162186
assert_series_equal(dni_out, dni_out_expected, check_names=False)
163187

164188

189+
def test_check_irradiance_limits_qcrad_extreme(irradiance_qcrad):
190+
"""Test different input combinations to check_irradiance_limits_qcrad.
191+
"""
192+
expected = irradiance_qcrad
193+
194+
ghi_out, dhi_out, dni_out = irradiance.check_irradiance_limits_qcrad(
195+
expected['solar_zenith'], expected['dni_extra'], ghi=expected['ghi'],
196+
dhi=expected['dhi'], dni=expected['dni'], limits='extreme')
197+
198+
assert_series_equal(ghi_out, expected['ghi_extreme_limit_flag'], check_names=False) # noqa: E501
199+
assert_series_equal(dhi_out, expected['dhi_extreme_limit_flag'], check_names=False) # noqa: E501
200+
assert_series_equal(dni_out, expected['dni_extreme_limit_flag'], check_names=False) # noqa: E501
201+
202+
165203
def test_check_irradiance_consistency_qcrad(irradiance_qcrad):
166204
"""Test that QCRad identifies consistent irradiance measurements.
167205

0 commit comments

Comments
 (0)