Skip to content

pvfactors limited implementation for bifacial calculations #635

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

Merged
merged 29 commits into from
Jan 22, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
80f910f
Set up import of pvfactors package
Dec 13, 2018
8acfd58
Add test inputs from github repo README.md TLDR section
Dec 13, 2018
2e982d0
Function implementation and testing of outputs
Dec 13, 2018
199ad02
Update docstrings
Dec 14, 2018
397c850
Update pvfactors version to latest v0.1.5
Dec 14, 2018
2168e91
Merge branch 'master' into pvfactors_implementation
Dec 14, 2018
1c976e3
Move bifacial calculation and test to new specific modules
Dec 17, 2018
a340c20
Reverting autopep8 changes in irradiance.py and test_irradiance.py
Dec 17, 2018
fd3adf3
Docstrings section name change
Dec 17, 2018
6435ce3
Implement decorator to allow use of pandas objects vs numpy arrays
Dec 17, 2018
381000c
Update function docstrings since now allowing pandas objects
Dec 17, 2018
74b20af
Small docstrings fixes
Dec 17, 2018
89bad6c
Fix decorator for dataframe case and add test for decorator
Dec 17, 2018
81b8887
Update docs for API changes and whatsnew file
Dec 17, 2018
418ca72
Use better naming
Dec 17, 2018
e93c460
Remove overkill test requirements for test_pvfactors_timeseries
Dec 18, 2018
ef203f0
Remove pvfactors version requirement
Dec 18, 2018
7b62e4a
Add pvfactors to ci requirements
Dec 18, 2018
330660a
Fix import error
Dec 18, 2018
7cd7cf9
Silence stickler and add pvfactors to ci as pip install
Dec 19, 2018
cff21dd
Fix ci typos + docstrings. And use DatetimeIndex and lists in test
Dec 19, 2018
160419a
pandas>=0.23.3 not available for py34, so removing pvfactors install
Dec 19, 2018
bd5078b
Remove decorator implementation + check for pd series in function
Dec 19, 2018
cbc7434
Remove unused import
Dec 19, 2018
9829b81
Simplify and clarify pvfactors function
Dec 19, 2018
9970179
Fix docstrings for pvlib consistency + link to pvfactors TLDR
Jan 21, 2019
608863f
Merge branch 'master' into pvfactors_implementation
Jan 21, 2019
78756cd
Always return timestamped series and dataframe and test for that
Jan 21, 2019
55dd3a6
Update whatsnew and remove old part
Jan 22, 2019
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
12 changes: 12 additions & 0 deletions docs/sphinx/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -521,3 +521,15 @@ Functions for power modeling.

modelchain.basic_chain
modelchain.get_orientation


Bifacial
========

Methods for calculating back surface irradiance
-----------------------------------------------

.. autosummary::
:toctree: generated/

bifacial.pvfactors_timeseries
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.6.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ API Changes
(deprecated) to :py:func:`pvlib.solarposition.sun_rise_set_transit_spa.
`sun_rise_set_transit_spa` requires time input to be localized to the
specified latitude/longitude. (:issue:`316`)
* Created new bifacial section for `pvfactors` limited implementation (:issue:`421`)


Enhancements
Expand All @@ -60,6 +61,7 @@ Enhancements
* Add option for :py:func:`pvlib.irradiance.disc` to use relative airmass
by supplying `pressure=None`. (:issue:`449`)
* Created :py:func:`pvlib.pvsystem.pvsyst_celltemp` to implement PVsyst's cell temperature model. (:issue:`552`)
* Created :py:func:`pvlib.bifacial.pvfactors_timeseries` to use open-source `pvfactors` package to calculate back surface irradiance (:issue:`421`)


Bug fixes
Expand All @@ -79,6 +81,7 @@ Testing
~~~~~~~
* Add test for :func:`~pvlib.solarposition.hour_angle` (:issue:`597`)
* Update tests to be compatible with pytest 4.0. (:issue:`623`)
* Add tests for :py:func:`pvlib.bifacial.pvfactors_timeseries` implementation and :py:func:`pvlib.tools.enforce_numpy_arrays` decorator function (:issue:`421`)
Copy link
Member

Choose a reason for hiding this comment

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

The enforce_numpy_arrays section is outdated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, done in 55dd3a6



Contributors
Expand All @@ -92,3 +95,4 @@ Contributors
* Anton Driesse (:ghuser:`adriesse`)
* Cameron Stark (:ghuser:`camerontstark`)
* Jonathan Gaffiot (:ghuser:`jgaffiot`)
* Marc Anoma (:ghuser:`anomam`)
135 changes: 135 additions & 0 deletions pvlib/bifacial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""
The ``bifacial`` module contains functions for modeling back surface
plane-of-array irradiance under various conditions.
"""

import pandas as pd
from pvlib.tools import enforce_numpy_arrays


@enforce_numpy_arrays
def pvfactors_timeseries(
solar_azimuth, solar_zenith, surface_azimuth, surface_tilt,
timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, albedo,
n_pvrows=3, index_observed_pvrow=1,
rho_front_pvrow=0.03, rho_back_pvrow=0.05,
horizon_band_angle=15.,
Comment on lines +13 to +14
Copy link
Member

Choose a reason for hiding this comment

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

@anomam do you happen to remember why you chose this particular default value for horizon_band_angle? From what I can tell in the git history, the analogous default on the pvfactors side was 6.5 (both at the time of this PR and today) rather than 15. I know this PR is ancient history at this point so I'm not expecting much, just curious :)

There are now some small differences between this wrapper and pvfactors itself in the rho_* parameters as well, but I think those were only introduced later on after this PR was merged.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hey @kanderso-nrel ! It was a long time ago so I'm not entirely sure anymore, but what I remember is that these values might have needed a bit of tuning to match the measurements depending on the sites etc. I think for the horizon band it might be safe to check what the Perez paper says about what value to use.
For the reflectivity parameter values, it's something I actually never really had time to study in depth. I think the default values for the PV rows were low enough to not have much impact, but that's probably not accurate in all cases...
Sorry if my answers are vague, I think getting better ones would require more work/studies/experience...

run_parallel_calculations=True, n_workers_for_parallel_calcs=None):
"""
Calculate front and back surface plane-of-array irradiance on
a fixed tilt or single-axis tracker PV array configuration, and using
the open-source "pvfactors" package.
Please refer to pvfactors online documentation for more details:
https://sunpower.github.io/pvfactors/

Inputs
------
solar_azimuth: numeric
Sun's azimuth angles using pvlib's azimuth convention (deg)
solar_zenith: numeric
Sun's zenith angles (deg)
surface_azimuth: numeric
Azimuth angle of the front surface of the PV modules, using pvlib's
convention (deg)
surface_tilt: numeric
Tilt angle of the PV modules, going from 0 to 180 (deg)
timestamps: array of :class:datetime.datetime objects
List of simulation timestamps
dni: numeric
Direct normal irradiance (W/m2)
dhi: numeric
Diffuse horizontal irradiance (W/m2)
gcr: float
Ground coverage ratio of the pv array
pvrow_height: float
Height of the pv rows, measured at their center (m)
pvrow_width: float
Width of the pv rows in the considered 2D plane (m)
albedo: float
Ground albedo
n_pvrows: int, default 3
Number of PV rows to consider in the PV array
index_observed_pvrow: int, default 1
Index of the PV row whose incident irradiance will be returned. Indices
of PV rows go from 0 to n_pvrows-1.
rho_front_pvrow: float, default 0.03
Front surface reflectivity of PV rows
rho_back_pvrow: float, default 0.05
Back surface reflectivity of PV rows
horizon_band_angle: float, default 15
Elevation angle of the sky dome's diffuse horizon band (deg)
run_parallel_calculations: bool, default True
pvfactors is capable of using multiprocessing. Use this flag to decide
to run calculations in parallel (recommended) or not.
n_workers_for_parallel_calcs: int, default None
Number of workers to use in the case of parallel calculations. The
default value of 'None' will lead to using a value equal to the number
of CPU's on the machine running the model.

Returns
-------
front_poa_irradiance: numeric
Calculated incident irradiance on the front surface of the PV modules
(W/m2)
back_poa_irradiance: numeric
Calculated incident irradiance on the back surface of the PV modules
(W/m2)
df_registries: pandas DataFrame
DataFrame containing all the detailed outputs and elements calculated
for every timestamp of the simulation. Please refer to pvfactors
documentation for more details

References
----------
.. [1] Anoma, Marc Abou, et al. "View Factor Model and Validation for
Bifacial PV and Diffuse Shade on Single-Axis Trackers." 44th IEEE
Photovoltaic Specialist Conference. 2017.
"""

# Import pvfactors functions for timeseries calculations.
from pvfactors.timeseries import (calculate_radiosities_parallel_perez,
calculate_radiosities_serially_perez,
get_average_pvrow_outputs)
idx_slice = pd.IndexSlice

# Build up pv array configuration parameters
pvarray_parameters = {
'n_pvrows': n_pvrows,
'pvrow_height': pvrow_height,
'pvrow_width': pvrow_width,
'surface_azimuth': surface_azimuth[0], # not necessary
'surface_tilt': surface_tilt[0], # not necessary
'gcr': gcr,
'solar_zenith': solar_zenith[0], # not necessary
'solar_azimuth': solar_azimuth[0], # not necessary
'rho_ground': albedo,
'rho_front_pvrow': rho_front_pvrow,
'rho_back_pvrow': rho_back_pvrow,
'horizon_band_angle': horizon_band_angle
}

# Run pvfactors calculations: either in parallel or serially
if run_parallel_calculations:
df_registries, df_custom_perez = calculate_radiosities_parallel_perez(
pvarray_parameters, timestamps, solar_zenith, solar_azimuth,
surface_tilt, surface_azimuth, dni, dhi,
n_processes=n_workers_for_parallel_calcs)
else:
inputs = (pvarray_parameters, timestamps, solar_zenith, solar_azimuth,
surface_tilt, surface_azimuth, dni, dhi)
df_registries, df_custom_perez = calculate_radiosities_serially_perez(
inputs)

# Get the average surface outputs
df_outputs = get_average_pvrow_outputs(df_registries,
values=['qinc'],
include_shading=True)

# Select the calculated outputs from the pvrow to observe
ipoa_front = df_outputs.loc[:, idx_slice[index_observed_pvrow,
'front', 'qinc']].values
Copy link
Member

Choose a reason for hiding this comment

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

perhaps only a question because I'm not familiar enough with pvfactors, but why extract .values instead of returning the Series?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just believed that having a 1D numpy array was simpler for these results, but I can adjust it to pvlib's needs, just let me know!


ipoa_back = df_outputs.loc[:, idx_slice[index_observed_pvrow,
'back', 'qinc']].values

return ipoa_front, ipoa_back, df_registries
32 changes: 32 additions & 0 deletions pvlib/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ def inner():
def pandas_0_17():
return parse_version(pd.__version__) >= parse_version('0.17.0')


needs_pandas_0_17 = pytest.mark.skipif(
not pandas_0_17(), reason='requires pandas 0.17 or greater')


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


needs_numpy_1_10 = pytest.mark.skipif(
not numpy_1_10(), reason='requires numpy 1.10 or greater')

Expand All @@ -92,8 +94,10 @@ def has_spa_c():
else:
return True


requires_spa_c = pytest.mark.skipif(not has_spa_c(), reason="requires spa_c")


def has_numba():
try:
import numba
Expand All @@ -106,6 +110,7 @@ def has_numba():
else:
return True


requires_numba = pytest.mark.skipif(not has_numba(), reason="requires numba")

try:
Expand All @@ -125,3 +130,30 @@ def has_numba():

requires_netCDF4 = pytest.mark.skipif(not has_netCDF4,
reason='requires netCDF4')

try:
import pvfactors
has_pvfactors = True
except ImportError:
has_pvfactors = False

requires_pvfactors = pytest.mark.skipif(not has_pvfactors,
reason='requires pvfactors')

try:
import future
has_future = True
except ImportError:
has_future = False

requires_future = pytest.mark.skipif(not has_future,
reason='requires future')

try:
import shapely
has_shapely = True
except ImportError:
has_shapely = False

requires_shapely = pytest.mark.skipif(not has_shapely,
reason='requires shapely')
66 changes: 66 additions & 0 deletions pvlib/test/test_bifacial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import numpy as np
from datetime import datetime
from pvlib.bifacial import pvfactors_timeseries
from conftest import (requires_pvfactors, requires_future, requires_shapely,
requires_scipy)


@requires_scipy
@requires_shapely
@requires_future
@requires_pvfactors
def test_pvfactors_timeseries():
""" Test that pvfactors is functional, using the TLDR section inputs of the
package github repo README.md file"""

# Create some inputs
timestamps = np.array([datetime(2017, 8, 31, 11),
datetime(2017, 8, 31, 12)])
solar_zenith = np.array([20., 10.])
solar_azimuth = np.array([110., 140.])
surface_tilt = np.array([10., 0.])
surface_azimuth = np.array([90., 90.])
dni = np.array([1000., 300.])
dhi = np.array([50., 500.])
gcr = 0.4
pvrow_height = 1.75
pvrow_width = 2.44
albedo = 0.2
n_pvrows = 3
index_observed_pvrow = 1
rho_front_pvrow = 0.03
rho_back_pvrow = 0.05
horizon_band_angle = 15.

# Expected values
expected_ipoa_front = [1034.96216923, 795.4423259]
expected_ipoa_back = [92.11871485, 70.39404124]
tolerance = 1e-6

# Test serial calculations
ipoa_front, ipoa_back, _ = pvfactors_timeseries(
solar_azimuth, solar_zenith, surface_azimuth, surface_tilt,
timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, albedo,
n_pvrows=n_pvrows, index_observed_pvrow=index_observed_pvrow,
rho_front_pvrow=rho_front_pvrow, rho_back_pvrow=rho_back_pvrow,
horizon_band_angle=horizon_band_angle,
run_parallel_calculations=False, n_workers_for_parallel_calcs=None)

np.testing.assert_allclose(ipoa_front, expected_ipoa_front,
Copy link
Member

Choose a reason for hiding this comment

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

This should use the pandas test functions if we adopt the always return a Series approach

atol=0, rtol=tolerance)
np.testing.assert_allclose(ipoa_back, expected_ipoa_back,
atol=0, rtol=tolerance)

# Run calculations in parallel
ipoa_front, ipoa_back, _ = pvfactors_timeseries(
solar_azimuth, solar_zenith, surface_azimuth, surface_tilt,
timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, albedo,
n_pvrows=n_pvrows, index_observed_pvrow=index_observed_pvrow,
rho_front_pvrow=rho_front_pvrow, rho_back_pvrow=rho_back_pvrow,
horizon_band_angle=horizon_band_angle,
run_parallel_calculations=True, n_workers_for_parallel_calcs=None)

np.testing.assert_allclose(ipoa_front, expected_ipoa_front,
atol=0, rtol=tolerance)
np.testing.assert_allclose(ipoa_back, expected_ipoa_back,
atol=0, rtol=tolerance)
44 changes: 44 additions & 0 deletions pvlib/test/test_tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest

from pvlib import tools
import numpy as np
import pandas as pd

@pytest.mark.parametrize('keys, input_dict, expected', [
(['a', 'b'], {'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 2}),
Expand All @@ -11,3 +13,45 @@
def test_build_kwargs(keys, input_dict, expected):
kwargs = tools._build_kwargs(keys, input_dict)
assert kwargs == expected


def test_enforce_numpy_arrays():
""" Check that when pandas objects are included in the inputs, the
wrapped function receives numpy arrays and the outputs are turned into
pandas series """

# Create a test function and decorate it
@tools.enforce_numpy_arrays
def fun_function(a, b, c, d, kwarg_1=None, kwarg_2=None):
assert isinstance(a, np.ndarray)
assert isinstance(b, np.ndarray)
assert isinstance(c, np.ndarray)
assert isinstance(d, str)
assert isinstance(kwarg_1, np.ndarray)
assert isinstance(kwarg_2, np.ndarray)
out_1 = np.array([1, 2])
out_2 = pd.Series([3, 4])
out_3 = 5.
return out_1, out_2, out_3

# Check with no pandas inputs
a = b = c = np.array([1, 2])
d = 'string'
kwarg_1 = np.array([1, 2])
kwarg_2 = np.array([1, 2])
out_1, out_2, out_3 = fun_function(a, b, c, d,
kwarg_1=kwarg_1, kwarg_2=kwarg_2)
assert isinstance(out_1, np.ndarray)
assert isinstance(out_2, pd.Series)
assert isinstance(out_3, float)

# Check with some pandas inputs in both args and kwargs
b = pd.Series([1, 2])
c = pd.DataFrame([1, 2], columns=['e'], index=range(2))
kwarg_1 = pd.Series([1, 2])
kwarg_2 = pd.DataFrame([1, 2], columns=['kwarg_2'], index=range(2))
out_1, out_2, out_3 = fun_function(a, b, c, d,
kwarg_1=kwarg_1, kwarg_2=kwarg_2)
assert isinstance(out_1, pd.Series)
assert isinstance(out_2, pd.Series)
assert isinstance(out_3, float)
Loading