Skip to content

Commit 310bff1

Browse files
committed
Merge remote-tracking branch 'upstream/main' into remove_g_poa_effective
2 parents b1d1eff + e711e9e commit 310bff1

70 files changed

Lines changed: 92235 additions & 37227 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""
2+
Agrivoltaic system modeling
3+
===========================
4+
5+
Irradiance at crop level between rows
6+
"""
7+
8+
# %%
9+
# This example demonstrates how to calculate power output for a bifacial
10+
# agriPV plant as well as calculating the irradiance at crop level
11+
# using pvlib's infinite sheds model.
12+
# For an overview of agrivPV concepts and performance, the reader
13+
# is referred to :doi:`10.69766/XAEU5008`.
14+
#
15+
# This gallery example is based on an actual AgriPV plant, namely
16+
# European Energy's `Flakkebjerg AgriPV site
17+
# <https://europeanenergy.com/2023/12/20/using-the-same-land-twice-at-european-\
18+
# energys-flakkebjerg-solar-park/>`_.
19+
#
20+
# The first steps are to define the plant location and to calculate solar
21+
# position and clearsky irradiance for a single day as an example.
22+
#
23+
# .. figure:: ../../_images/agrivoltaics_system.jpg
24+
# :align: center
25+
# :width: 75%
26+
# :alt: Photo of an agriPV system
27+
#
28+
# Photo of an agriPV system.
29+
# *Source: Adam R. Jensen*
30+
31+
import pvlib
32+
import pandas as pd
33+
from pvlib.tools import cosd
34+
import matplotlib.pyplot as plt
35+
36+
# sphinx_gallery_thumbnail_path = '_images/agrivoltaics_system.jpg'
37+
38+
location = pvlib.location.Location(latitude=55, longitude=10)
39+
40+
times = pd.date_range('2020-06-28', periods=24*60, freq='1min', tz='UTC')
41+
42+
solpos = location.get_solarposition(times)
43+
44+
clearsky = location.get_clearsky(times, model='ineichen')
45+
46+
# %%
47+
# Next, we need to define the plant layout:
48+
49+
height = 2.6 # [m] height of torque above ground
50+
pitch = 12 # [m] row spacing
51+
row_width = 2 * 2.384 # [m] two modules in portrait, each 2 m long
52+
gcr = row_width / pitch # ground coverage ratio [unitless]
53+
axis_azimuth = 0 # [degrees] north-south tracking axis
54+
max_angle = 55 # [degrees] maximum rotation angle
55+
56+
# %%
57+
# Before running the infinite sheds model, we need to know the orientation
58+
# of the trackers. For a single-axis tracker, this can be calculated as:
59+
60+
tracking_orientations = pvlib.tracking.singleaxis(
61+
apparent_zenith=solpos['apparent_zenith'],
62+
apparent_azimuth=solpos['azimuth'],
63+
axis_azimuth=axis_azimuth,
64+
max_angle=max_angle,
65+
backtrack=True,
66+
gcr=gcr,
67+
)
68+
69+
# %%
70+
# For agrivPV systems, the local albedo is dependent on crop growth and thus
71+
# changes throughout the seasons. In this example, we only simulate one
72+
# day and thus use a constant value. Similarly, we will assume a constant
73+
# air temperature to avoid getting external data. Both albedo and air
74+
# temperature could be defined as Series with the same index as used for the
75+
# solar position calculations.
76+
77+
albedo = 0.20 # [unitless]
78+
temp_air = 18 # [degrees C]
79+
80+
# %%
81+
# Now, we are ready to calculate the front and rear-side irradiance using
82+
# the pvlib infinite sheds model.
83+
84+
dni_extra = pvlib.irradiance.get_extra_radiation(times)
85+
86+
irradiance = pvlib.bifacial.infinite_sheds.get_irradiance(
87+
surface_tilt=tracking_orientations['surface_tilt'],
88+
surface_azimuth=tracking_orientations['surface_azimuth'],
89+
solar_zenith=solpos['apparent_zenith'],
90+
solar_azimuth=solpos['azimuth'],
91+
gcr=gcr,
92+
height=height,
93+
pitch=pitch,
94+
ghi=clearsky['ghi'],
95+
dhi=clearsky['dhi'],
96+
dni=clearsky['dni'],
97+
albedo=albedo,
98+
model='haydavies',
99+
dni_extra=dni_extra,
100+
bifaciality=0.7, # [unitless] rear-side power relative to front-side
101+
)
102+
103+
# %%
104+
# Once the in-plane irradiance is known, we can estimate the PV array power.
105+
# For simplicity, we use the PVWatts model:
106+
107+
N_modules = 1512 # [unitless] Number of modules
108+
pdc0_per_module = 660 # [W] STC rating
109+
pdc0 = pdc0_per_module * N_modules
110+
111+
gamma_pdc = -0.004 # [1/degrees C]
112+
113+
temp_cell = pvlib.temperature.faiman(
114+
poa_global=irradiance['poa_global'],
115+
temp_air=temp_air,
116+
)
117+
118+
power_dc = pvlib.pvsystem.pvwatts_dc(
119+
g_poa_effective=irradiance['poa_global'],
120+
temp_cell=temp_cell,
121+
pdc0=pdc0,
122+
gamma_pdc=gamma_pdc)
123+
124+
power_dc.divide(1000).plot()
125+
plt.ylabel('DC power [kW]')
126+
plt.show()
127+
128+
# %%
129+
# In addition to the power output of the PV array, we are also interested
130+
# in how much irradiance reaches the crops under the array. In this case
131+
# we calculate the average irradiance on the ground between two rows, using
132+
# the infinite sheds utility functions.
133+
#
134+
# This consists of two parts. First we determine the diffuse irradiance on
135+
# ground and second we calculate the fraction of the ground that is unshaded
136+
# (i.e., receives DNI).
137+
138+
vf_ground_sky = pvlib.bifacial.utils.vf_ground_sky_2d_integ(
139+
surface_tilt=tracking_orientations['surface_tilt'],
140+
gcr=gcr,
141+
height=height,
142+
pitch=pitch,
143+
)
144+
145+
unshaded_ground_fraction = pvlib.bifacial.utils._unshaded_ground_fraction(
146+
surface_tilt=tracking_orientations['surface_tilt'],
147+
surface_azimuth=tracking_orientations['surface_azimuth'],
148+
solar_zenith=solpos['apparent_zenith'],
149+
solar_azimuth=solpos['azimuth'],
150+
gcr=gcr,
151+
)
152+
153+
crop_avg_irradiance = (unshaded_ground_fraction * clearsky['dni']
154+
* cosd(solpos['apparent_zenith'])
155+
+ vf_ground_sky * clearsky['dhi'])
156+
157+
fig, ax = plt.subplots()
158+
clearsky['ghi'].plot(ax=ax, label='Horizontal irradiance above panels')
159+
crop_avg_irradiance.plot(ax=ax, label='Horizontal irradiance at crop level')
160+
ax.legend(loc='upper center')
161+
ax.set_ylabel('Irradiance [W/m$^2$]')
162+
ax.set_ylim(-10, 1050)
163+
plt.show()
1.58 MB
Loading

docs/sphinx/source/conf.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
intersphinx_mapping = {
6767
'python': ('https://docs.python.org/3/', None),
6868
'numpy': ('https://numpy.org/doc/stable/', None),
69-
'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None),
69+
'scipy': ('https://docs.scipy.org/doc/scipy/', None),
7070
'pandas': ('https://pandas.pydata.org/pandas-docs/stable', None),
7171
'matplotlib': ('https://matplotlib.org/stable', None),
7272
}
@@ -412,6 +412,41 @@ def setup(app):
412412
# https://gist.github.com/flying-sheep/b65875c0ce965fbdd1d9e5d0b9851ef1
413413

414414

415+
# select correct base URL depending on the build system context
416+
def get_source_files_base_url():
417+
"""
418+
Get the base URL for the source code to generate links to GitHub source.
419+
If the build is on ReadTheDocs and it's a stable version, use the
420+
versioned link. If it's a latest version, use the main link.
421+
422+
For other builds (e.g. pull requests), use the main link.
423+
Local builds will also use the main link.
424+
425+
Resulting base URL should end with a trailing slash.
426+
427+
See https://docs.readthedocs.com/platform/stable/reference/environment-variables.html
428+
""" # noqa: E501
429+
repo_url = os.environ.get(
430+
"READTHEDOCS_GIT_CLONE_URL",
431+
default="https://github.com/pvlib/pvlib-python",
432+
)
433+
READTHEDOCS_ENV = os.environ.get("READTHEDOCS", None) == "True"
434+
READTHEDOCS_VERSION = os.environ.get("READTHEDOCS_VERSION", None)
435+
READTHEDOCS_GIT_IDENTIFIER = os.environ.get(
436+
"READTHEDOCS_GIT_IDENTIFIER", None
437+
)
438+
if READTHEDOCS_ENV: # Building docs on ReadTheDocs
439+
if READTHEDOCS_VERSION == "latest": # latest version, commited to main
440+
repo_url += "/blob/main/"
441+
elif READTHEDOCS_VERSION == "stable": # stable version, has a tag
442+
repo_url += f"/blob/{READTHEDOCS_GIT_IDENTIFIER}/"
443+
else: # pull request, user and branch are unknown so use main
444+
repo_url += "/blob/main/"
445+
else: # Local build
446+
repo_url += "/blob/main/" # can't tell where to point to
447+
return repo_url
448+
449+
415450
def get_obj_module(qualname):
416451
"""
417452
Get a module/class/attribute and its original module by qualname.
@@ -456,18 +491,18 @@ def get_linenos(obj):
456491
return start, start + len(lines) - 1
457492

458493

494+
URL_BASE = get_source_files_base_url() # Edit on GitHub source code links
495+
496+
459497
def make_github_url(file_name):
460498
"""
461499
Generate the appropriate GH link for a given docs page. This function
462500
is intended for use in sphinx template files.
463501
464502
The target URL is built differently based on the type of page. The pydata
465503
sphinx theme has a built-in `file_name` variable that looks like
466-
"/docs/sphinx/source/api.rst" or "generated/pvlib.atmosphere.alt2pres.rst"
504+
"docs/sphinx/source/api.rst" or "generated/pvlib.atmosphere.alt2pres.rst"
467505
"""
468-
469-
URL_BASE = "https://github.com/pvlib/pvlib-python/blob/main/"
470-
471506
# is it a gallery page?
472507
if any(d in file_name for d in sphinx_gallery_conf['gallery_dirs']):
473508
example_folder = file_name.split("/")[-2]

0 commit comments

Comments
 (0)