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

Agrivoltaics - PAR diffuse fraction model #2048

Merged
merged 37 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
90dd1e0
New PAR module with spitters_relationship
echedey-ls May 3, 2024
8cc7ffc
Merge branch 'main' into par-diffuse-fraction-of-global-par
echedey-ls May 6, 2024
ae63127
Update v0.11.0.rst
echedey-ls May 12, 2024
5880453
linter
echedey-ls May 12, 2024
5af860a
API update
echedey-ls May 12, 2024
bdc2303
Example rendering
echedey-ls May 12, 2024
aec7870
Update test_par.py
echedey-ls May 12, 2024
02ebe81
Apply suggestions from code review (Adam)
echedey-ls May 24, 2024
c5d133d
Update par.py
echedey-ls May 24, 2024
149961e
Move function to spectrum (mismatch.py)
echedey-ls May 24, 2024
eff3a25
Improve units formatting
echedey-ls May 24, 2024
d18827e
Split legends
echedey-ls May 24, 2024
18619a4
Remove api page, move to spectrum index
echedey-ls May 24, 2024
feded95
Update v0.11.0.rst
echedey-ls May 24, 2024
b9aa23e
Merge branch 'main' into par-diffuse-fraction-of-global-par
echedey-ls May 25, 2024
1c6d416
Move to ``irradiance.py``
echedey-ls May 30, 2024
899844c
Flake8 :knife:
echedey-ls May 30, 2024
9268a44
Fix trigonometry - double testing with a spreadsheet
echedey-ls Jun 14, 2024
bce50b6
Move section of PAR
echedey-ls Jun 14, 2024
2825e2c
Merge branch 'main' into par-diffuse-fraction-of-global-par
echedey-ls Jun 14, 2024
764bf8b
I should read more carefully
echedey-ls Jun 14, 2024
0e0f836
Update decomposition.rst
echedey-ls Jun 18, 2024
0d23fe0
Apply suggestions from code review (Cliff)
echedey-ls Jun 18, 2024
a7c84b5
Merge branch 'par-diffuse-fraction-of-global-par' of https://github.c…
echedey-ls Jun 18, 2024
d1bb64b
More docs refurbishment
echedey-ls Jun 18, 2024
eeef154
Rename to `diffuse_par_spitters`
echedey-ls Jun 18, 2024
6082fe7
`global` -> `broadband`
echedey-ls Jun 18, 2024
94df738
Merge branch 'main' into par-diffuse-fraction-of-global-par
echedey-ls Jun 18, 2024
ac9d45c
Code review from Adam, first batch
echedey-ls Jun 19, 2024
77f3ba8
Apply trigonometric property
echedey-ls Jun 19, 2024
a6c3c6b
Merge branch 'main' into par-diffuse-fraction-of-global-par
echedey-ls Jun 19, 2024
f5449d7
Fix merge - linter
echedey-ls Jun 19, 2024
bafe63b
Forgot to apply this comment
echedey-ls Jun 19, 2024
1a5d0d9
Remove model from eq
echedey-ls Jun 19, 2024
189efe5
Dailies, insolation instead of instant, irradiance values
echedey-ls Jun 20, 2024
7c28be6
More docs refurbishment
echedey-ls Jun 20, 2024
59faee0
Review from Cliff
AdamRJensen Jun 20, 2024
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
2 changes: 2 additions & 0 deletions docs/examples/agrivoltaics/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Agrivoltaic Systems Modelling
-----------------------------
145 changes: 145 additions & 0 deletions docs/examples/agrivoltaics/plot_diffuse_PAR_Spitters_relationship.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""
Calculating the diffuse PAR using Spitter's relationship
=========================================================

This example demonstrates how to calculate the diffuse photosynthetically
active radiation (PAR) using Spitter's relationship.
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved
"""

# %%
# The photosynthetically active radiation (PAR) is a key component in the
# photosynthesis process of plants. As in photovoltaic systems, the PAR is
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved
# divided into direct and diffuse components. The diffuse fraction of PAR
# with respect to the total PAR is important in agrivoltaic systems, where
# crops are grown under solar panels. The diffuse fraction of PAR can be
# calculated using the Spitter's relationship [1]_ implemented in
# :py:func:`~pvlib.par.spitters_relationship`.
# This model requires the solar zenith angle and the fraction of the global
# radiation that is diffuse as inputs.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# radiation that is diffuse as inputs.
# irradiance that is diffuse as inputs.

Copy link
Member

Choose a reason for hiding this comment

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

To (perhaps) head off a discussion of whether "radiation" or "irradiance" should be used here: radiation is usually used when referring to the source, and irradiance (or irradiation) when referring to the receiver. So the sun emits radiation, and a tilted plane on the earth's surface receives solar irradiation. Either perspective seems OK to me in the context of this PR. But mixing PAR (..."radiation") with "irradiance" may be confusing to some.

#
# .. note::
# Understanding the distinction between the global radiation and the PAR is
# a key concept. The global radiation is the total amount of solar radiation
Copy link
Member

Choose a reason for hiding this comment

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

Is global radiation limited to any particular bandwidth in [1]? If it is not, then I'd describe it as "broadband solar radiation" and not mention PV at all. Mentioning PV may imply to some that the global solar radiation means the radiation in the photovoltaic band, usually 400-1100nm.

Copy link
Contributor Author

@echedey-ls echedey-ls Jun 18, 2024

Choose a reason for hiding this comment

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

I only see:

While for PV systems the total global radiation is the key term, for crops, the analogous term is the photosynthetically active radiation (PAR).

but no mention of the bandwidth.

# that is usually accounted for in PV applications, while the PAR is a
# measurement of a narrower range of wavelengths that are used in
# photosynthesis. See section on *Photosynthetically Active Radiation* in
# pp. 222-223 of [1]_.
#
# The key function used in this example is
# :py:func:`pvlib.par.spitters_relationship` to calculate the diffuse PAR
# fraction, as a function of global diffuse fraction and solar zenith.
#
# References
# ----------
# .. [1] C. J. T. Spitters, H. A. J. M. Toussaint, and J. Goudriaan,
# 'Separating the diffuse and direct component of global radiation and its
# implications for modeling canopy photosynthesis Part I. Components of
# incoming radiation', Agricultural and Forest Meteorology, vol. 38,
# no. 1, pp. 217-229, Oct. 1986, :doi:`10.1016/0168-1923(86)90060-2`.
#
# Read some example data
# ^^^^^^^^^^^^^^^^^^^^^^

import pvlib
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter
from pathlib import Path

# Read some sample data
DATA_FILE = Path(pvlib.__path__[0]).joinpath("data", "723170TYA.CSV")

tmy, metadata = pvlib.iotools.read_tmy3(
DATA_FILE, coerce_year=1990, map_variables=True
)
tmy = tmy.filter(
["ghi", "dhi", "dni", "pressure", "temp_air"]
) # remaining data is not needed
tmy = tmy[
"1990-04-11T06":"1990-04-11T22"
] # select a single day for this example

solar_position = pvlib.solarposition.get_solarposition(
# TMY timestamp is at end of hour, so shift to center of interval
tmy.index.shift(freq="-30T"),
latitude=metadata["latitude"],
longitude=metadata["longitude"],
altitude=metadata["altitude"],
pressure=tmy["pressure"] * 100, # convert from millibar to Pa
temperature=tmy["temp_air"],
)
solar_position.index = tmy.index # reset index to end of the hour

# %%
# Calculate Photosynthetically Active Radiation
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# The total PAR can be approximated as 0.50 times the global horizontal
# irradiance (GHI) for solar elevation higher that :math:`10^\circ`.
# See section on *Photosynthetically Active Radiation* in pp. 222-223 of [1]_.

par = pd.DataFrame({"total": 0.50 * tmy["ghi"]}, index=tmy.index)

# Calculate global irradiance diffuse fraction, input of the Spitter's model
tmy["diffuse_fraction"] = tmy["dhi"] / tmy["ghi"]

# Calculate diffuse PAR fraction using Spitter's relationship
par["diffuse_fraction"] = pvlib.par.spitters_relationship(
solar_position["zenith"], tmy["diffuse_fraction"]
)

# Finally, calculate the diffuse PAR
par["diffuse"] = par["total"] * par["diffuse_fraction"]
par[solar_position["zenith"] > 80] = (
0 # set to zero for elevation < 10 degrees
)

# %%
# Plot the results
# ^^^^^^^^^^^^^^^^
# Irradiances on left axis, diffuse fractions on right axis

fig, ax_l = plt.subplots(figsize=(12, 6))
ax_l.set(
xlabel="Time",
ylabel="Irradiance $[W/m^2]$",
title="Diffuse PAR using Spitter's relationship",
)
ax_l.xaxis.set_major_formatter(DateFormatter("%H:%M", tz=tmy.index.tz))
ax_l.plot(tmy.index, tmy["ghi"], label="Global: total", color="deepskyblue")
ax_l.plot(
tmy.index,
tmy["dhi"],
label="Global: diffuse",
color="skyblue",
linestyle="-.",
)
ax_l.plot(tmy.index, par["total"], label="PAR: total", color="orangered")
ax_l.plot(
tmy.index,
par["diffuse"],
label="PAR: diffuse",
color="coral",
linestyle="-.",
)
ax_l.grid()

ax_r = ax_l.twinx()
ax_r.set(ylabel="Diffuse fraction")
ax_r.plot(
tmy.index,
tmy["diffuse_fraction"],
label="Global diffuse fraction",
color="plum",
linestyle=":",
)
ax_r.plot(
tmy.index,
par["diffuse_fraction"],
label="PAR diffuse fraction",
color="chocolate",
linestyle=":",
)

lines = ax_l.get_lines() + ax_r.get_lines()
plt.legend(lines, (line.get_label() for line in lines))
plt.show()
9 changes: 9 additions & 0 deletions docs/sphinx/source/reference/agrivoltaics.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.. currentmodule:: pvlib

Agrivoltaic Systems Modelling
=============================

.. autosummary::
:toctree: generated/

par.spitters_relationship
1 change: 1 addition & 0 deletions docs/sphinx/source/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ API reference
bifacial
scaling
location
agrivoltaics
6 changes: 6 additions & 0 deletions docs/sphinx/source/whatsnew/v0.11.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ Deprecations

Enhancements
~~~~~~~~~~~~
* Add module :py:mod:`pvlib.par` to facilitate the design of Agrivoltaic
systems.
- Adds function :py:func:`pvlib.par.spitters_relationship` to calculate the
diffuse fraction of Photosynthetically Active Radiation (PAR) from the
global diffuse fraction and the solar zenith.
(:issue:`2047`, :pull:`2048`)


Bug fixes
Expand Down
1 change: 1 addition & 0 deletions pvlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
ivtools,
location,
modelchain,
par,
pvarray,
pvsystem,
scaling,
Expand Down
72 changes: 72 additions & 0 deletions pvlib/par.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
Photosynthetically Active Radiation (PAR) module.
Utilities found here are specially interesting for agrivoltaic systems.
"""

from pvlib.tools import cosd, sind


def spitters_relationship(solar_zenith, global_diffuse_fraction):
r"""
Derive the diffuse fraction of photosynthetically active radiation (PAR)
respect to the global radiation diffuse fraction.

The relationship is based on the work of Spitters et al. (1986) [1]_.

Parameters
----------
solar_zenith : numeric
Solar zenith angle in degrees :math:`^{\circ}`.
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved

global_diffuse_fraction : numeric
Fraction of the global radiation that is diffuse. Unitless.

Returns
-------
par_diffuse_fraction : numeric
Photosynthetically active radiation in W/m^2.
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved

Notes
-----
The relationship is given by equations (9) & (10) in [1]_ and (1) in [2]_:

.. math::

k_{diffuse\_PAR}^{model} = \frac{PAR_{diffuse}}{PAR_{total}} =
\frac{\left[1 + 0.3 \left(1 - \left(k_d^{model}\right) ^2\right)\right]
k_d^{model}}
{1 + \left(1 - \left(k_d^{model}\right)^2\right) \cos ^2 (90 - \beta)
\cos ^3 \beta}

where :math:`k_d^{model}` is the diffuse fraction of the global radiation,
provided by some model.

A comparison of different models performance for the diffuse fraction of
the global irradiance can be found in [2]_ in the context of Sweden.

References
----------
.. [1] C. J. T. Spitters, H. A. J. M. Toussaint, and J. Goudriaan,
'Separating the diffuse and direct component of global radiation and its
implications for modeling canopy photosynthesis Part I. Components of
incoming radiation', Agricultural and Forest Meteorology, vol. 38,
no. 1, pp. 217-229, Oct. 1986, :doi:`10.1016/0168-1923(86)90060-2`.
.. [2] S. Ma Lu et al., 'Photosynthetically active radiation decomposition
models for agrivoltaic systems applications', Solar Energy, vol. 244,
pp. 536-549, Sep. 2022, :doi:`10.1016/j.solener.2022.05.046`.
"""
# notation change:
# cosd(90-x) = sind(x) and 90-solar_elevation = solar_zenith
sind_solar_zenith = sind(solar_zenith)
cosd_solar_elevation = cosd(90 - solar_zenith)
par_diffuse_fraction = (
(1 + 0.3 * (1 - global_diffuse_fraction**2))
* global_diffuse_fraction
/ (
1
+ (1 - global_diffuse_fraction**2)
* sind_solar_zenith**2
* cosd_solar_elevation**3
)
)
return par_diffuse_fraction
28 changes: 28 additions & 0 deletions pvlib/tests/test_par.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Test Photosynthetically Active Radiation (PAR) submodule.
"""

from pvlib import par

echedey-ls marked this conversation as resolved.
Show resolved Hide resolved
import numpy as np
from numpy.testing import assert_allclose


def test_spitters_relationship():
solar_zenith, global_diffuse_fraction = np.meshgrid(
[90, 85, 75, 60, 40, 30, 10, 0], [0.01, 0.1, 0.3, 0.6, 0.8, 0.99]
)
solar_zenith = solar_zenith.ravel()
global_diffuse_fraction = global_diffuse_fraction.ravel()
result = par.spitters_relationship(solar_zenith, global_diffuse_fraction)
expected = np.array([
0.00650018, 0.00656213, 0.00706211, 0.00874170, 0.01171437, 0.01260581,
0.01299765, 0.01299970, 0.06517588, 0.06579393, 0.07077986, 0.08750105,
0.11699064, 0.12580782, 0.12967973, 0.12970000, 0.19994764, 0.20176275,
0.21635259, 0.26460255, 0.34722693, 0.37134002, 0.38184514, 0.38190000,
0.43609756, 0.43933488, 0.46497584, 0.54521789, 0.66826809, 0.70117647,
0.71512774, 0.71520000, 0.65176471, 0.65503875, 0.68042968, 0.75414541,
0.85271445, 0.87653894, 0.88634962, 0.88640000, 0.97647838, 0.97683827,
0.97952006, 0.98634857, 0.99374028, 0.99529135, 0.99590717, 0.9959103
])
assert_allclose(result, expected, atol=1e-8)
Loading