Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Lorenz et al. POA and GHI QC methods #167

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4175a4f
implemented gpoa limit lorenz check
abhisheksparikh Nov 12, 2022
0fd23e3
updated nomenclature and completed test functions
abhisheksparikh Nov 13, 2022
9399adf
edited description of a function
abhisheksparikh Nov 13, 2022
60861b2
updated documentation and api.rst
abhisheksparikh Nov 16, 2022
94d3430
style change
abhisheksparikh Nov 28, 2022
792f10d
simplifying initiation of `poa_global_limit_int_flag`
abhisheksparikh Nov 28, 2022
a2aa469
Simplifying the initiation of `poa_global_limit_bool_flag`
abhisheksparikh Nov 28, 2022
7097952
minor changes
abhisheksparikh Dec 4, 2022
97f3a9c
Added GHI check func and completed POA check func
abhisheksparikh Dec 12, 2022
8d2007b
Merge branch 'main' into poa_qa
abhisheksparikh Dec 12, 2022
a11d8d8
Update docs/whatsnew/0.1.3.rst
abhisheksparikh Dec 12, 2022
75dda23
Update pvanalytics/quality/irradiance.py
abhisheksparikh Dec 12, 2022
6c56859
Update pvanalytics/quality/irradiance.py
abhisheksparikh Dec 12, 2022
4da8913
Update pvanalytics/quality/irradiance.py
abhisheksparikh Dec 12, 2022
7420547
Update pvanalytics/quality/irradiance.py
abhisheksparikh Dec 13, 2022
e98d476
Update pvanalytics/tests/quality/test_irradiance.py
abhisheksparikh Dec 13, 2022
e0bfa2b
Update pvanalytics/tests/quality/test_irradiance.py
abhisheksparikh Dec 13, 2022
d0591cc
Update pvanalytics/tests/quality/test_irradiance.py
abhisheksparikh Dec 13, 2022
6d99699
Update pvanalytics/tests/quality/test_irradiance.py
abhisheksparikh Dec 13, 2022
e25674b
Update pvanalytics/tests/quality/test_irradiance.py
abhisheksparikh Dec 13, 2022
895bf72
Update pvanalytics/quality/irradiance.py
abhisheksparikh Dec 13, 2022
98f8445
Update pvanalytics/quality/irradiance.py
abhisheksparikh Dec 13, 2022
3ddfcc9
Update pvanalytics/quality/irradiance.py
abhisheksparikh Dec 13, 2022
f99c530
changed lower limit funcs and added -ve test data
abhisheksparikh Dec 13, 2022
ff5f019
Update pvanalytics/quality/irradiance.py
abhisheksparikh Dec 17, 2022
8524090
Update pvanalytics/quality/irradiance.py
abhisheksparikh Dec 17, 2022
6275efd
Update pvanalytics/quality/irradiance.py
abhisheksparikh Dec 17, 2022
f94efc3
cleaning test functions and some modifications in lorenz funcs
abhisheksparikh Dec 17, 2022
eef5429
Merge remote-tracking branch 'upstream/main' into poa_qa
abhisheksparikh Dec 18, 2022
c90a224
Corrected the upper limit equation of flag 3 for GHI
abhisheksparikh Dec 31, 2022
d144c68
minor modifications
abhisheksparikh Jan 9, 2023
165ce91
Merge branch 'main' into poa_qa
abhisheksparikh May 26, 2023
863ecde
Changed the function name from Lorenz to pvlive
abhisheksparikh May 26, 2023
57ae3bf
Update docs/api.rst
abhisheksparikh May 26, 2023
901e0a0
Update pvanalytics/quality/irradiance.py
abhisheksparikh May 26, 2023
9e05e4c
Update pvanalytics/quality/irradiance.py
abhisheksparikh May 26, 2023
771c721
Update pvanalytics/quality/irradiance.py
abhisheksparikh May 30, 2023
acb8a40
Update pvanalytics/quality/irradiance.py
abhisheksparikh May 30, 2023
368a6d0
Update pvanalytics/quality/irradiance.py
abhisheksparikh May 30, 2023
3e27f93
Update pvanalytics/quality/irradiance.py
abhisheksparikh Jun 4, 2023
8764c7b
Update pvanalytics/quality/irradiance.py
abhisheksparikh Jun 4, 2023
ee9f400
Update pvanalytics/quality/irradiance.py
abhisheksparikh Jun 4, 2023
74cac5a
Update pvanalytics/quality/irradiance.py
abhisheksparikh Jun 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,29 @@ There is function for calculating the component sum for GHI, DHI,
and DNI, and correcting for nighttime periods. Using this function, we can
estimate one irradiance field using the two other irradiance fields.
This can be useful for comparison, as well as to
calculate missing data fields.
calculate missing data fields.

.. autosummary::
:toctree: generated/

quality.irradiance.calculate_component_sum_series

The ``check_poa_global_limits_lorenz`` function flags the global plane of array
irradiance measurements that are outside the limits described in [2]_.

.. autosummary::
:toctree: generated/

quality.irradiance.check_poa_global_limits_lorenz

The ``check_ghi_limits_lorenz`` function flags the global horizontal irradiance
measurements that are outside the limits described in [2]_.

.. autosummary::
:toctree: generated/

quality.irradiance.check_ghi_limits_lorenz

Gaps
----

Expand Down Expand Up @@ -197,6 +213,11 @@ the quality check.
Algorithm for Surface Radiation Measurements, The Open Atmospheric
Science Journal 2, pp. 23-37, 2008.

.. [2] Elke Lorenz et. al, High resolution measurement network of global
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved
horizontal and tilted solar irradiance in southern Germany with a new
quality control scheme, Solar Energy, Volume 231, 2022, Pages 593-606,
ISSN 0038-092X, https://doi.org/10.1016/j.solener.2021.11.023.

Features
========

Expand Down
264 changes: 264 additions & 0 deletions pvanalytics/quality/irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,3 +660,267 @@ def calculate_component_sum_series(solar_zenith,
return _fill_nighttime(component, component_sum_df,
fill_night_value,
solar_zenith, zenith_limit)


def _upper_poa_global_limit_lorenz(aoi, solar_zenith, dni_extra):
r"""Function to calculate the upper limit of poa_global
"""
# Changing aoi to 90 degrees when solar zenith is greater than 90 (sun
# below horizon) or aoi is greater than 90 (sun on the other side of
# the sensor/module's plane).
aoi = aoi.clip(lower=0, upper=90)

# Determining the upper limit
upper_limit = 0.9 * dni_extra * (cosd(aoi))**1.2 + 300

# Setting upper limit as 0 when solar zenith is > 90 (night time)
upper_limit[solar_zenith > 90] = 0

# Setting upper limit as undefined where solar_zenith is not available
upper_limit[solar_zenith.isna()] = np.nan
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved

# Setting upper limit as undefined where aoi is not available
upper_limit[aoi.isna()] = np.nan
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved

return upper_limit


def _lower_limit_lorenz(solar_zenith, dni_extra):
r"""Function to calculate the lower limit of poa_global and ghi
"""
# Setting the lower_limit at 0.
lower_limit = pd.Series(np.zeros(len(solar_zenith)),
index=solar_zenith.index)
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved

# Determining the lower limit when solar zenith is < 75
lower_limit = lower_limit.mask(solar_zenith < 75,
0.01 * dni_extra * cosd(solar_zenith))

# Setting lower limit as undefined where solar_zenith is not available
lower_limit[solar_zenith.isna()] = np.nan

return (lower_limit)
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved


def check_poa_global_limits_lorenz(poa_global, solar_zenith, aoi,
dni_extra=1367):
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved
r"""Test for limits on POA global using the equations described in
Section 6.1 of [1]_
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved

Criteria from [1]_ 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. Also, steps with
change in magnitude of more than 1000 W/m2 are flagged. Lower bounds are
constant for all tests. Upper bounds are calculated as

.. math::
upper\_limit = 0.9 * dni\_extra * cos(aoi)^{1.2} + 300

Parameters
----------
poa_global : Series
Global tilted irradiance [W/m^2]
solar_zenith : Series
Solar zenith angle [degrees]
aoi : Series
angle of incidence [degrees]
dni_extra : float, default 1367
normal irradiance at the top of atmosphere [W/m^2]

Returns
-------
poa_global_limit_bool_flag : Series
True for each value that is physically possible.
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved
poa_global_limit_int_flag : Series
Series of integers representing the flag numbers described in the
literature. [1]_
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved

Notes
-----
The upper limit for `poa_global` is set to 0 when `solar_zenith` is greater
than 90 degrees. Missing values of `poa_global`, `solar_zenith`
Comment on lines +668 to +669
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still in strong favor of not flagging values at nighttime. I recently had a chat with the main author of the paper who did not have a recommendation for night time. Lorenz pointed me toward one of the co-authors who has yet to respond to my request.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not having thought about the significance of nighttime data, I don't have a strong opinion on this. But if I were to think of irradiance as a part of operational analysis of a PV plant, I would like to have some distinction between daytime and nighttime data.

and/or `aoi` will result in a `False` flag.

References
----------
.. [1] Elke Lorenz et al., High resolution measurement network of global
horizontal and tilted solar irradiance in southern Germany with a
new quality control scheme, Solar Energy, Volume 231, 2022,
Pages 593-606, ISSN 0038-092X,
https://doi.org/10.1016/j.solener.2021.11.023.
"""
# Finding the upper and lower limit
upper_limit = _upper_poa_global_limit_lorenz(aoi, solar_zenith, dni_extra)
lower_limit = _lower_limit_lorenz(solar_zenith, dni_extra)

# Initiating a poa_global_limit_int_flag series
poa_global_limit_int_flag = pd.Series(0, index=solar_zenith.index)

# Initiating a poa_global_limit_bool_flag series
poa_global_limit_bool_flag = pd.Series(True, index=solar_zenith.index)
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved

# Changing the poa_global_flag to 3 when poa_global is above upper
# limit or below lower limit
poa_global_limit_int_flag = poa_global_limit_int_flag.mask(
((poa_global > upper_limit) |
(poa_global < lower_limit)),
3
)

# Changing the poa_global_flag to 3 when the step change in poa values is
# more than 1000 W/m2
poa_global_limit_int_flag = poa_global_limit_int_flag.mask(
poa_global.diff().abs() > 1000,
3
)
Comment on lines +696 to +701
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this deserves it's own function.

The step change limit is fairly common:

For 1-min data Shafer et al. (2000) recommended a step change limit of 800 W/m2, although Espinar (2011) suggested 1000 W/m2 for GHI. Lorenz et al. (2022) also suggested using the 1000 W/m2 threshold for tilted irradiance measurements.

Copy link
Member

@AdamRJensen AdamRJensen May 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be done in a separate pull request.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I think one good reason to have the step change limit in here is to stay true to the list of checks the paper suggests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could have a helper function that returns the step threshold, and pass it a kwarg 'pvlive' to return 1000.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I am completely fine with separating out the filter as a separate function ☺️.

As am I, let's do this and have a step_limit kwarg or something.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could have a helper function that returns the step threshold, and pass it a kwarg 'pvlive' to return 1000.

Can somebody help me with this? After some reading, I think I understand kwargs, but have never written a function with them. Some more guidance on how the functions' design would be helpful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abhisheksparikh let's do that in a follow-on pull request. I feel like we are good enough here and we can improve later. Agree @AdamRJensen ?


# Changing the poa_global_flag to 1 when poa_global is not available
poa_global_limit_int_flag = poa_global_limit_int_flag.mask(
((poa_global.isna()) |
(upper_limit.isna()) |
(lower_limit.isna())),
1
)

# Changing the poa_global_limit_bool_flag depending on
# poa_global_limit_int_flag
poa_global_limit_bool_flag = poa_global_limit_int_flag == 0

return (poa_global_limit_bool_flag, poa_global_limit_int_flag)
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved


def _upper_ghi_limit_lorenz_flag2(solar_zenith, dni_extra):
r"""Function to calculate the upper limit of ghi for Flag 2
"""
# Determining the upper limit
upper_limit_flag2 = 1.2 * dni_extra * cosd(solar_zenith) + 50

# Setting upper limit as 0 when solar zenith is > 90 (night time)
upper_limit_flag2[solar_zenith > 90] = 0

# Setting upper limit as undefined where solar_zenith is not available
upper_limit_flag2[solar_zenith.isna()] = np.nan

return upper_limit_flag2


def _upper_ghi_limit_lorenz_flag3(solar_zenith, dni_extra):
r"""Function to calculate the upper limit of ghi for Flag 3
"""
# Determining the upper limit
upper_limit_flag3 = np.minimum(
pd.Series(1.2 * dni_extra, index=solar_zenith.index),
1.5 * dni_extra * (cosd(solar_zenith))**1.2 + 100)
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved

# Setting upper limit as 0 when solar zenith is > 90 (night time)
upper_limit_flag3[solar_zenith > 90] = 0

# Setting upper limit as undefined where solar_zenith is not available
upper_limit_flag3[solar_zenith.isna()] = np.nan

return upper_limit_flag3


def check_ghi_limits_lorenz(ghi, solar_zenith, dni_extra=1367):
r"""Test for limits on global horizontal irradiance using the equations
described in Section 6.1 of [1]_

Criteria from [1]_ are used to determine physically plausible
lower, upper bounds and step change. Each value is tested and a value
passes if value > lower bound and value < upper bound. Also, steps with
change in magnitude of more than :math:`1000 W/m^{2}` are flagged. Lower
bounds are constant for all tests. As defined in the paper, there are
two values of upper bounds calculated:
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved
(1) Rare values - Flag 2
(2) Extreme values - Flag 3
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved

For Flag 2

.. math::
upper\_limit_{\mathbf{Flag\_2}} = 1.2 * dni\_extra * cos(solar\_zenith)
+ 50

For Flag 3

.. math::
upper\_limit_{\mathbf{Flag\_3}} = min(1.2 * dni\_extra,
1.5 * dni\_extra * cos(solar\_zenith)^{1.2} + 100)

Parameters
----------
ghi : Series
Global horizontal irradiance [W/m^2]
solar_zenith : Series
Solar zenith angle [degrees]
dni_extra : float, default 1367
normal irradiance at the top of atmosphere [W/m^2]

Returns
-------
ghi_limit_bool_flag : Series
True for each value that is physically possible.
ghi_limit_int_flag : Series
Series of integers representing the flag numbers described in the
literature. [1]_
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved

Notes
-----
The upper limit for `ghi` is set to 0 when `solar_zenith` is greater
than 90 degrees. Missing values of `ghi` and/or `solar_zenith` will result
in a `False` flag.

References
----------
.. [1] Elke Lorenz et al., High resolution measurement network of global
horizontal and tilted solar irradiance in southern Germany with a
new quality control scheme, Solar Energy, Volume 231, 2022,
Pages 593-606, ISSN 0038-092X,
https://doi.org/10.1016/j.solener.2021.11.023.
"""
# Finding the upper limit for flag 2 and flag 3
upper_limit_flag2 = _upper_ghi_limit_lorenz_flag2(solar_zenith, dni_extra)
upper_limit_flag3 = _upper_ghi_limit_lorenz_flag3(solar_zenith, dni_extra)

# Finding the lower limit for flag 3
lower_limit = _lower_limit_lorenz(solar_zenith, dni_extra)

# Initiating a ghi_limit_int_flag series
ghi_limit_int_flag = pd.Series(0, index=solar_zenith.index)

# Initiating a ghi_limit_bool_flag series
ghi_limit_bool_flag = pd.Series(True, index=solar_zenith.index)
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved

# Changing the ghi_limit_int_flag to 2 when ghi is above upper_limit_flag2
ghi_limit_int_flag = ghi_limit_int_flag.mask(
(ghi > upper_limit_flag2),
2
)

# Changing the ghi_limit_int_flag to 3 when ghi is above upper_limit_flag3
# or lower than the lower_limit
ghi_limit_int_flag = ghi_limit_int_flag.mask(
(ghi > upper_limit_flag3) |
(ghi < lower_limit),
3
)

# Changing the ghi_limit_int_flag to 3 when the step change in ghi values
# is more than 1000 W/m2
ghi_limit_int_flag = ghi_limit_int_flag.mask(
(abs(ghi - ghi.shift(1)) > 1000),
abhisheksparikh marked this conversation as resolved.
Show resolved Hide resolved
3
)

# Changing the ghi_limit_int_flag to 1 when ghi is not available
ghi_limit_int_flag = ghi_limit_int_flag.mask(
((ghi.isna()) |
(upper_limit_flag2.isna()) |
(upper_limit_flag3.isna()) |
(lower_limit.isna())),
1
)

# Changing the ghi_limit_bool_flag depending on ghi_limit_int_flag
ghi_limit_bool_flag = ghi_limit_int_flag == 0

return (ghi_limit_bool_flag, ghi_limit_int_flag)
Loading