Skip to content

Commit 0486c3b

Browse files
anomamwholmgren
authored andcommitted
pvfactors limited implementation for bifacial calculations (#635)
* Set up import of pvfactors package * Add test inputs from github repo README.md TLDR section * Function implementation and testing of outputs * Update docstrings * Update pvfactors version to latest v0.1.5 * Move bifacial calculation and test to new specific modules * Reverting autopep8 changes in irradiance.py and test_irradiance.py * Docstrings section name change * Implement decorator to allow use of pandas objects vs numpy arrays * I used a decorator because I wish to seperate the logic of handling the function inputs and the logic related to using pvfactors * Update function docstrings since now allowing pandas objects * via function decoration * Small docstrings fixes * Fix decorator for dataframe case and add test for decorator * Update docs for API changes and whatsnew file * Use better naming * Remove overkill test requirements for test_pvfactors_timeseries * Remove pvfactors version requirement * Add pvfactors to ci requirements * Fix import error * Silence stickler and add pvfactors to ci as pip install * Fix ci typos + docstrings. And use DatetimeIndex and lists in test * pandas>=0.23.3 not available for py34, so removing pvfactors install * Could not find a version that satisfies the requirement pandas>=0.23.3 * Remove decorator implementation + check for pd series in function * add identical test but using pandas series * Remove unused import * Simplify and clarify pvfactors function * Fix docstrings for pvlib consistency + link to pvfactors TLDR * Always return timestamped series and dataframe and test for that * Update whatsnew and remove old part
1 parent 52eeae7 commit 0486c3b

11 files changed

+310
-1
lines changed

ci/requirements-py27.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ dependencies:
1919
- pip:
2020
- coveralls
2121
- pytest-timeout
22+
- pvfactors==0.1.5

ci/requirements-py35.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ dependencies:
1919
- pip:
2020
- coveralls
2121
- pytest-timeout
22+
- pvfactors==0.1.5

ci/requirements-py36.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ dependencies:
1818
- nose
1919
- pip:
2020
- coveralls
21+
- pvfactors==0.1.5

ci/requirements-py37.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ dependencies:
1818
- nose
1919
- pip:
2020
- coveralls
21+
- pvfactors==0.1.5

docs/sphinx/source/api.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,3 +521,15 @@ Functions for power modeling.
521521

522522
modelchain.basic_chain
523523
modelchain.get_orientation
524+
525+
526+
Bifacial
527+
========
528+
529+
Methods for calculating back surface irradiance
530+
-----------------------------------------------
531+
532+
.. autosummary::
533+
:toctree: generated/
534+
535+
bifacial.pvfactors_timeseries

docs/sphinx/source/whatsnew/v0.6.1.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ API Changes
3535
(deprecated) to :py:func:`pvlib.solarposition.sun_rise_set_transit_spa.
3636
`sun_rise_set_transit_spa` requires time input to be localized to the
3737
specified latitude/longitude. (:issue:`316`)
38+
* Created new bifacial section for `pvfactors` limited implementation (:issue:`421`)
3839

3940

4041
Enhancements
@@ -60,6 +61,7 @@ Enhancements
6061
* Add option for :py:func:`pvlib.irradiance.disc` to use relative airmass
6162
by supplying `pressure=None`. (:issue:`449`)
6263
* Created :py:func:`pvlib.pvsystem.pvsyst_celltemp` to implement PVsyst's cell temperature model. (:issue:`552`)
64+
* Created :py:func:`pvlib.bifacial.pvfactors_timeseries` to use open-source `pvfactors` package to calculate back surface irradiance (:issue:`421`)
6365
* Add `PVSystem` class method :py:func:`~pvlib.pvsystem.PVSystem.pvsyst_celltemp` (:issue:`633`)
6466

6567

@@ -84,6 +86,7 @@ Testing
8486
~~~~~~~
8587
* Add test for :func:`~pvlib.solarposition.hour_angle` (:issue:`597`)
8688
* Update tests to be compatible with pytest 4.0. (:issue:`623`)
89+
* Add tests for :py:func:`pvlib.bifacial.pvfactors_timeseries` implementation (:issue:`421`)
8790

8891

8992
Contributors
@@ -97,3 +100,4 @@ Contributors
97100
* Anton Driesse (:ghuser:`adriesse`)
98101
* Cameron Stark (:ghuser:`camerontstark`)
99102
* Jonathan Gaffiot (:ghuser:`jgaffiot`)
103+
* Marc Anoma (:ghuser:`anomam`)

pvlib/bifacial.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""
2+
The ``bifacial`` module contains functions for modeling back surface
3+
plane-of-array irradiance under various conditions.
4+
"""
5+
6+
import pandas as pd
7+
8+
9+
def pvfactors_timeseries(
10+
solar_azimuth, solar_zenith, surface_azimuth, surface_tilt,
11+
timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, albedo,
12+
n_pvrows=3, index_observed_pvrow=1,
13+
rho_front_pvrow=0.03, rho_back_pvrow=0.05,
14+
horizon_band_angle=15.,
15+
run_parallel_calculations=True, n_workers_for_parallel_calcs=None):
16+
"""
17+
Calculate front and back surface plane-of-array irradiance on
18+
a fixed tilt or single-axis tracker PV array configuration, and using
19+
the open-source "pvfactors" package.
20+
Please refer to pvfactors online documentation for more details:
21+
https://sunpower.github.io/pvfactors/
22+
23+
Inputs
24+
------
25+
solar_azimuth: numeric
26+
Sun's azimuth angles using pvlib's azimuth convention (deg)
27+
solar_zenith: numeric
28+
Sun's zenith angles (deg)
29+
surface_azimuth: numeric
30+
Azimuth angle of the front surface of the PV modules, using pvlib's
31+
convention (deg)
32+
surface_tilt: numeric
33+
Tilt angle of the PV modules, going from 0 to 180 (deg)
34+
timestamps: datetime or DatetimeIndex
35+
List of simulation timestamps
36+
dni: numeric
37+
Direct normal irradiance (W/m2)
38+
dhi: numeric
39+
Diffuse horizontal irradiance (W/m2)
40+
gcr: float
41+
Ground coverage ratio of the pv array
42+
pvrow_height: float
43+
Height of the pv rows, measured at their center (m)
44+
pvrow_width: float
45+
Width of the pv rows in the considered 2D plane (m)
46+
albedo: float
47+
Ground albedo
48+
n_pvrows: int, default 3
49+
Number of PV rows to consider in the PV array
50+
index_observed_pvrow: int, default 1
51+
Index of the PV row whose incident irradiance will be returned. Indices
52+
of PV rows go from 0 to n_pvrows-1.
53+
rho_front_pvrow: float, default 0.03
54+
Front surface reflectivity of PV rows
55+
rho_back_pvrow: float, default 0.05
56+
Back surface reflectivity of PV rows
57+
horizon_band_angle: float, default 15
58+
Elevation angle of the sky dome's diffuse horizon band (deg)
59+
run_parallel_calculations: bool, default True
60+
pvfactors is capable of using multiprocessing. Use this flag to decide
61+
to run calculations in parallel (recommended) or not.
62+
n_workers_for_parallel_calcs: int, default None
63+
Number of workers to use in the case of parallel calculations. The
64+
default value of 'None' will lead to using a value equal to the number
65+
of CPU's on the machine running the model.
66+
67+
Returns
68+
-------
69+
front_poa_irradiance: numeric
70+
Calculated incident irradiance on the front surface of the PV modules
71+
(W/m2)
72+
back_poa_irradiance: numeric
73+
Calculated incident irradiance on the back surface of the PV modules
74+
(W/m2)
75+
df_registries: pandas DataFrame
76+
DataFrame containing detailed outputs of the simulation; for
77+
instance the shapely geometries, the irradiance components incident on
78+
all surfaces of the PV array (for all timestamps), etc.
79+
In the pvfactors documentation, this is refered to as the "surface
80+
registry".
81+
82+
References
83+
----------
84+
.. [1] Anoma, Marc Abou, et al. "View Factor Model and Validation for
85+
Bifacial PV and Diffuse Shade on Single-Axis Trackers." 44th IEEE
86+
Photovoltaic Specialist Conference. 2017.
87+
"""
88+
89+
# Convert pandas Series inputs to numpy arrays
90+
if isinstance(solar_azimuth, pd.Series):
91+
solar_azimuth = solar_azimuth.values
92+
if isinstance(solar_zenith, pd.Series):
93+
solar_zenith = solar_zenith.values
94+
if isinstance(surface_azimuth, pd.Series):
95+
surface_azimuth = surface_azimuth.values
96+
if isinstance(surface_tilt, pd.Series):
97+
surface_tilt = surface_tilt.values
98+
if isinstance(dni, pd.Series):
99+
dni = dni.values
100+
if isinstance(dhi, pd.Series):
101+
dhi = dhi.values
102+
103+
# Import pvfactors functions for timeseries calculations.
104+
from pvfactors.timeseries import (calculate_radiosities_parallel_perez,
105+
calculate_radiosities_serially_perez,
106+
get_average_pvrow_outputs)
107+
idx_slice = pd.IndexSlice
108+
109+
# Build up pv array configuration parameters
110+
pvarray_parameters = {
111+
'n_pvrows': n_pvrows,
112+
'pvrow_height': pvrow_height,
113+
'pvrow_width': pvrow_width,
114+
'gcr': gcr,
115+
'rho_ground': albedo,
116+
'rho_front_pvrow': rho_front_pvrow,
117+
'rho_back_pvrow': rho_back_pvrow,
118+
'horizon_band_angle': horizon_band_angle
119+
}
120+
121+
# Run pvfactors calculations: either in parallel or serially
122+
if run_parallel_calculations:
123+
df_registries, df_custom_perez = calculate_radiosities_parallel_perez(
124+
pvarray_parameters, timestamps, solar_zenith, solar_azimuth,
125+
surface_tilt, surface_azimuth, dni, dhi,
126+
n_processes=n_workers_for_parallel_calcs)
127+
else:
128+
inputs = (pvarray_parameters, timestamps, solar_zenith, solar_azimuth,
129+
surface_tilt, surface_azimuth, dni, dhi)
130+
df_registries, df_custom_perez = calculate_radiosities_serially_perez(
131+
inputs)
132+
133+
# Get the average surface outputs
134+
df_outputs = get_average_pvrow_outputs(df_registries,
135+
values=['qinc'],
136+
include_shading=True)
137+
138+
# Select the calculated outputs from the pvrow to observe
139+
ipoa_front = df_outputs.loc[:, idx_slice[index_observed_pvrow,
140+
'front', 'qinc']]
141+
142+
ipoa_back = df_outputs.loc[:, idx_slice[index_observed_pvrow,
143+
'back', 'qinc']]
144+
145+
# Set timestamps as index of df_registries for consistency of outputs
146+
df_registries = df_registries.set_index('timestamps')
147+
148+
return ipoa_front, ipoa_back, df_registries

pvlib/test/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,15 @@ def inner():
6969
def pandas_0_17():
7070
return parse_version(pd.__version__) >= parse_version('0.17.0')
7171

72+
7273
needs_pandas_0_17 = pytest.mark.skipif(
7374
not pandas_0_17(), reason='requires pandas 0.17 or greater')
7475

7576

7677
def numpy_1_10():
7778
return parse_version(np.__version__) >= parse_version('1.10.0')
7879

80+
7981
needs_numpy_1_10 = pytest.mark.skipif(
8082
not numpy_1_10(), reason='requires numpy 1.10 or greater')
8183

@@ -92,8 +94,10 @@ def has_spa_c():
9294
else:
9395
return True
9496

97+
9598
requires_spa_c = pytest.mark.skipif(not has_spa_c(), reason="requires spa_c")
9699

100+
97101
def has_numba():
98102
try:
99103
import numba
@@ -106,6 +110,7 @@ def has_numba():
106110
else:
107111
return True
108112

113+
109114
requires_numba = pytest.mark.skipif(not has_numba(), reason="requires numba")
110115

111116
try:
@@ -125,3 +130,12 @@ def has_numba():
125130

126131
requires_netCDF4 = pytest.mark.skipif(not has_netCDF4,
127132
reason='requires netCDF4')
133+
134+
try:
135+
import pvfactors # noqa: F401
136+
has_pvfactors = True
137+
except ImportError:
138+
has_pvfactors = False
139+
140+
requires_pvfactors = pytest.mark.skipif(not has_pvfactors,
141+
reason='requires pvfactors')

pvlib/test/test_bifacial.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import pandas as pd
2+
from datetime import datetime
3+
from pvlib.bifacial import pvfactors_timeseries
4+
from conftest import requires_pvfactors
5+
6+
7+
@requires_pvfactors
8+
def test_pvfactors_timeseries():
9+
""" Test that pvfactors is functional, using the TLDR section inputs of the
10+
package github repo README.md file:
11+
https://github.com/SunPower/pvfactors/blob/master/README.md#tldr---quick-start"""
12+
13+
# Create some inputs
14+
timestamps = pd.DatetimeIndex([datetime(2017, 8, 31, 11),
15+
datetime(2017, 8, 31, 12)]
16+
).set_names('timestamps')
17+
solar_zenith = [20., 10.]
18+
solar_azimuth = [110., 140.]
19+
surface_tilt = [10., 0.]
20+
surface_azimuth = [90., 90.]
21+
dni = [1000., 300.]
22+
dhi = [50., 500.]
23+
gcr = 0.4
24+
pvrow_height = 1.75
25+
pvrow_width = 2.44
26+
albedo = 0.2
27+
n_pvrows = 3
28+
index_observed_pvrow = 1
29+
rho_front_pvrow = 0.03
30+
rho_back_pvrow = 0.05
31+
horizon_band_angle = 15.
32+
33+
# Expected values
34+
expected_ipoa_front = pd.Series([1034.96216923, 795.4423259],
35+
index=timestamps,
36+
name=(1, 'front', 'qinc'))
37+
expected_ipoa_back = pd.Series([92.11871485, 70.39404124],
38+
index=timestamps,
39+
name=(1, 'back', 'qinc'))
40+
41+
# Test serial calculations
42+
ipoa_front, ipoa_back, df_registries = pvfactors_timeseries(
43+
solar_azimuth, solar_zenith, surface_azimuth, surface_tilt,
44+
timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, albedo,
45+
n_pvrows=n_pvrows, index_observed_pvrow=index_observed_pvrow,
46+
rho_front_pvrow=rho_front_pvrow, rho_back_pvrow=rho_back_pvrow,
47+
horizon_band_angle=horizon_band_angle,
48+
run_parallel_calculations=False, n_workers_for_parallel_calcs=None)
49+
50+
pd.testing.assert_series_equal(ipoa_front, expected_ipoa_front)
51+
pd.testing.assert_series_equal(ipoa_back, expected_ipoa_back)
52+
pd.testing.assert_index_equal(timestamps, df_registries.index.unique())
53+
54+
# Run calculations in parallel
55+
ipoa_front, ipoa_back, df_registries = pvfactors_timeseries(
56+
solar_azimuth, solar_zenith, surface_azimuth, surface_tilt,
57+
timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, albedo,
58+
n_pvrows=n_pvrows, index_observed_pvrow=index_observed_pvrow,
59+
rho_front_pvrow=rho_front_pvrow, rho_back_pvrow=rho_back_pvrow,
60+
horizon_band_angle=horizon_band_angle,
61+
run_parallel_calculations=True, n_workers_for_parallel_calcs=None)
62+
63+
pd.testing.assert_series_equal(ipoa_front, expected_ipoa_front)
64+
pd.testing.assert_series_equal(ipoa_back, expected_ipoa_back)
65+
pd.testing.assert_index_equal(timestamps, df_registries.index.unique())
66+
67+
68+
@requires_pvfactors
69+
def test_pvfactors_timeseries_pandas_inputs():
70+
""" Test that pvfactors is functional, using the TLDR section inputs of the
71+
package github repo README.md file, but converted to pandas Series:
72+
https://github.com/SunPower/pvfactors/blob/master/README.md#tldr---quick-start"""
73+
74+
# Create some inputs
75+
timestamps = pd.DatetimeIndex([datetime(2017, 8, 31, 11),
76+
datetime(2017, 8, 31, 12)]
77+
).set_names('timestamps')
78+
solar_zenith = pd.Series([20., 10.])
79+
solar_azimuth = pd.Series([110., 140.])
80+
surface_tilt = pd.Series([10., 0.])
81+
surface_azimuth = pd.Series([90., 90.])
82+
dni = pd.Series([1000., 300.])
83+
dhi = pd.Series([50., 500.])
84+
gcr = 0.4
85+
pvrow_height = 1.75
86+
pvrow_width = 2.44
87+
albedo = 0.2
88+
n_pvrows = 3
89+
index_observed_pvrow = 1
90+
rho_front_pvrow = 0.03
91+
rho_back_pvrow = 0.05
92+
horizon_band_angle = 15.
93+
94+
# Expected values
95+
expected_ipoa_front = pd.Series([1034.96216923, 795.4423259],
96+
index=timestamps,
97+
name=(1, 'front', 'qinc'))
98+
expected_ipoa_back = pd.Series([92.11871485, 70.39404124],
99+
index=timestamps,
100+
name=(1, 'back', 'qinc'))
101+
102+
# Test serial calculations
103+
ipoa_front, ipoa_back, df_registries = pvfactors_timeseries(
104+
solar_azimuth, solar_zenith, surface_azimuth, surface_tilt,
105+
timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, albedo,
106+
n_pvrows=n_pvrows, index_observed_pvrow=index_observed_pvrow,
107+
rho_front_pvrow=rho_front_pvrow, rho_back_pvrow=rho_back_pvrow,
108+
horizon_band_angle=horizon_band_angle,
109+
run_parallel_calculations=False, n_workers_for_parallel_calcs=None)
110+
111+
pd.testing.assert_series_equal(ipoa_front, expected_ipoa_front)
112+
pd.testing.assert_series_equal(ipoa_back, expected_ipoa_back)
113+
pd.testing.assert_index_equal(timestamps, df_registries.index.unique())
114+
115+
# Run calculations in parallel
116+
ipoa_front, ipoa_back, df_registries = pvfactors_timeseries(
117+
solar_azimuth, solar_zenith, surface_azimuth, surface_tilt,
118+
timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, albedo,
119+
n_pvrows=n_pvrows, index_observed_pvrow=index_observed_pvrow,
120+
rho_front_pvrow=rho_front_pvrow, rho_back_pvrow=rho_back_pvrow,
121+
horizon_band_angle=horizon_band_angle,
122+
run_parallel_calculations=True, n_workers_for_parallel_calcs=None)
123+
124+
pd.testing.assert_series_equal(ipoa_front, expected_ipoa_front)
125+
pd.testing.assert_series_equal(ipoa_back, expected_ipoa_back)
126+
pd.testing.assert_index_equal(timestamps, df_registries.index.unique())

pvlib/test/test_tools.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from pvlib import tools
44

5+
56
@pytest.mark.parametrize('keys, input_dict, expected', [
67
(['a', 'b'], {'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 2}),
78
(['a', 'b', 'd'], {'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 2}),

0 commit comments

Comments
 (0)