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 28 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) from total 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, PAR can be
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.irradiance.diffuse_par_spitters`.
# This model requires the solar zenith angle and the fraction of the broadband
# 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 broadband radiation and the PAR
# is a key concept. The broadband radiation is the total amount of solar
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved
# radiation that is usually accounted for in PV applications, while the PAR
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved
# 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.irradiance.diffuse_par_spitters` to calculate the diffuse
# PAR fraction, as a function of broadband diffuse fraction and solar zenith.
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved
#
# 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 broadband horizontal
# irradiance (GHI) for solar elevation higher that 10°.
Copy link
Member

Choose a reason for hiding this comment

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

Is PAR simply 0 at solar elevation angles less than 10 degrees? Seems odd

# See section on *Photosynthetically Active Radiation* in pp. 222-223 of [1]_.

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

# Calculate broadband 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.irradiance.diffuse_par_spitters(
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="Broadband: total", color="deepskyblue")
ax_l.plot(
tmy.index,
tmy["dhi"],
label="Broadband: 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_l.legend(loc="upper left")

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

plt.show()
1 change: 1 addition & 0 deletions docs/sphinx/source/reference/irradiance/components.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ Decomposing and combining irradiance
irradiance.get_ground_diffuse
irradiance.dni
irradiance.complete_irradiance
irradiance.diffuse_par_spitters
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.11.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ Enhancements
* Added extraterrestrial and direct spectra of the ASTM G173-03 standard with
the new function :py:func:`pvlib.spectrum.get_reference_spectra`.
(:issue:`1963`, :pull:`2039`)
* Add function :py:func:`pvlib.irradiance.diffuse_par_spitters` 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
67 changes: 67 additions & 0 deletions pvlib/irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -3766,3 +3766,70 @@ def louche(ghi, solar_zenith, datetime_or_doy, max_zenith=90):
data = pd.DataFrame(data, index=datetime_or_doy)

return data


def diffuse_par_spitters(solar_zenith, global_diffuse_fraction):
r"""
Derive the diffuse fraction of photosynthetically active radiation (PAR).

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

Parameters
----------
solar_zenith : numeric
Solar zenith angle. Degrees.

global_diffuse_fraction : numeric
Fraction of the global radiation that is diffuse. Unitless [0, 1].
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
par_diffuse_fraction : numeric
Fraction of photosynthetically active radiation (PAR) that is diffuse.
Unitless [0, 1].

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, and :math:`\beta` is the solar elevation angle
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved
(in degrees [°]).
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved

A comparison using different models 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
echedey-ls marked this conversation as resolved.
Show resolved Hide resolved
cosd_solar_zenith = tools.cosd(solar_zenith)
cosd_solar_elevation = tools.cosd(90 - solar_zenith)
par_diffuse_fraction = (
(1 + 0.3 * (1 - global_diffuse_fraction**2))
* global_diffuse_fraction
/ (
1
+ (1 - global_diffuse_fraction**2)
* cosd_solar_zenith**2
* cosd_solar_elevation**3
)
)
return par_diffuse_fraction
20 changes: 20 additions & 0 deletions pvlib/tests/test_irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -1406,3 +1406,23 @@ def test_louche():
out = irradiance.louche(ghi, zenith, index)

assert_frame_equal(out, expected)


def test_diffuse_par_spitters():
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 = irradiance.diffuse_par_spitters(
solar_zenith, global_diffuse_fraction
)
expected = np.array([
0.01300, 0.01290, 0.01226, 0.01118, 0.01125, 0.01189, 0.01293, 0.01300,
0.12970, 0.12874, 0.12239, 0.11174, 0.11236, 0.11868, 0.12905, 0.12970,
0.38190, 0.37931, 0.36201, 0.33273, 0.33446, 0.35188, 0.38014, 0.38190,
0.71520, 0.71178, 0.68859, 0.64787, 0.65033, 0.67472, 0.71288, 0.71520,
0.88640, 0.88401, 0.86755, 0.83745, 0.83931, 0.85746, 0.88478, 0.88640,
0.99591, 0.99576, 0.99472, 0.99270, 0.99283, 0.99406, 0.99581, 0.99591,
]) # fmt: skip
assert_allclose(result, expected, atol=1e-5)
Loading