Skip to content

Commit 9d667ff

Browse files
committed
Merge remote-tracking branch 'upstream/main' into irradiance_updates
2 parents b318e6e + dfafe6d commit 9d667ff

46 files changed

Lines changed: 1191 additions & 9415 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/asv_check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Install Python
2424
uses: actions/setup-python@v5
2525
with:
26-
python-version: '3.9'
26+
python-version: '3.12'
2727

2828
- name: Install asv
2929
run: pip install asv==0.6.4

.github/workflows/flake8.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ jobs:
77
steps:
88
- name: Checkout source
99
uses: actions/checkout@v4
10-
- name: Install Python 3.11
10+
- name: Install Python 3.12
1111
uses: actions/setup-python@v5
1212
with:
13-
python-version: '3.11'
13+
python-version: '3.12'
1414
- name: Install Flake8 5.0.4 linter
1515
run: pip install flake8==5.0.4 # use this version for --diff option
1616
- name: Setup Flake8 output matcher for PR annotations

.github/workflows/publish.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ jobs:
2222
- name: Set up Python
2323
uses: actions/setup-python@v5
2424
with:
25-
python-version: 3.9
25+
# Python version should be the minimum supported version
26+
python-version: "3.9"
2627

2728
- name: Install build tools
2829
run: |

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Documentation
6868
=============
6969

7070
Full documentation can be found at [readthedocs](http://pvlib-python.readthedocs.io/en/stable/),
71-
including an [FAQ](http://pvlib-python.readthedocs.io/en/stable/user_guide/faq.html) page.
71+
including an [FAQ](https://pvlib-python.readthedocs.io/en/stable/user_guide/extras/faq.html) page.
7272

7373
Installation
7474
============
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+
effective_irradiance=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()

docs/examples/system-models/plot_oedi_9068.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@
165165
# module fraction and returns the average irradiance over the total module
166166
# surface.
167167

168-
solar_position = location.get_solarposition(psm3.index, latitude, longitude)
168+
solar_position = location.get_solarposition(psm3.index)
169169
tracker_angles = mount.get_orientation(
170170
solar_position['apparent_zenith'],
171171
solar_position['azimuth']
1.58 MB
Loading

docs/sphinx/source/conf.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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)