Repository: pvlib/pvlib-python
Pull request #650 (2019-01-21 16:00:21)

put SERVER in try and catch KeyError
*  if `.ecmwfapirc` not in `$HOME`
* or `ECMWF_API_KEY` not set to path to `.ecmwfapirc`

pvlib python pull request guidelines
====================================

Thank you for your contribution to pvlib python! You may delete all of these instructions except for the list below.

You may submit a pull request with your code at any stage of completion.

The following items must be addressed before the code can be merged. Please don't hesitate to ask for help if you're unsure of how to accomplish any of the items below:

 - [x] Closes issue #649
 - [x] I am familiar with the [contributing guidelines](http://pvlib-python.readthedocs.io/en/latest/contributing.html).
 - [x] Fully tested. Added and/or modified tests to ensure correct behavior for all reasonable inputs. Tests (usually) must pass on the TravisCI and Appveyor testing services.
 - [x] Updates entries to `docs/sphinx/source/api.rst` for API changes.
 - [x] Adds description and name entries in the appropriate `docs/sphinx/source/whatsnew` file for all changes.
 - [ ] Code quality and style is sufficient. Passes LGTM and SticklerCI checks.
 - [x] New code is fully documented. Includes sphinx/numpydoc compliant docstrings and comments in the code where necessary.
 - [x] Pull request is nearly complete and ready for detailed review.

Brief description of the problem and proposed solution (if not already fully described in the issue linked to above):



Patch:
diff --git a/ci/requirements-py27.yml b/ci/requirements-py27.yml
index 81df9d0..cb77db8 100644
--- a/ci/requirements-py27.yml
+++ b/ci/requirements-py27.yml
@@ -19,3 +19,4 @@ dependencies:
     - pip:
         - coveralls
         - pytest-timeout
+        - pvfactors==0.1.5
diff --git a/ci/requirements-py35.yml b/ci/requirements-py35.yml
index 8f250f1..7c4d0ff 100644
--- a/ci/requirements-py35.yml
+++ b/ci/requirements-py35.yml
@@ -19,3 +19,4 @@ dependencies:
     - pip:
         - coveralls
         - pytest-timeout
+        - pvfactors==0.1.5
diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml
index 31fbdce..068e0bd 100644
--- a/ci/requirements-py36.yml
+++ b/ci/requirements-py36.yml
@@ -18,3 +18,4 @@ dependencies:
     - nose
     - pip:
         - coveralls
+        - pvfactors==0.1.5
diff --git a/ci/requirements-py37.yml b/ci/requirements-py37.yml
index 25dac8e..11378ba 100644
--- a/ci/requirements-py37.yml
+++ b/ci/requirements-py37.yml
@@ -18,3 +18,4 @@ dependencies:
     - nose
     - pip:
         - coveralls
+        - pvfactors==0.1.5
diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst
index f5352e3..0ac8aa4 100644
--- a/docs/sphinx/source/api.rst
+++ b/docs/sphinx/source/api.rst
@@ -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
diff --git a/docs/sphinx/source/whatsnew/v0.6.1.rst b/docs/sphinx/source/whatsnew/v0.6.1.rst
index 949ad3e..6721b0a 100644
--- a/docs/sphinx/source/whatsnew/v0.6.1.rst
+++ b/docs/sphinx/source/whatsnew/v0.6.1.rst
@@ -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
@@ -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`)
 * Add `PVSystem` class method :py:func:`~pvlib.pvsystem.PVSystem.pvsyst_celltemp` (:issue:`633`)
 
 
@@ -84,6 +86,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 (:issue:`421`)
 
 
 Contributors
@@ -97,3 +100,4 @@ Contributors
 * Anton Driesse (:ghuser:`adriesse`)
 * Cameron Stark (:ghuser:`camerontstark`)
 * Jonathan Gaffiot (:ghuser:`jgaffiot`)
+* Marc Anoma (:ghuser:`anomam`)
diff --git a/pvlib/bifacial.py b/pvlib/bifacial.py
new file mode 100644
index 0000000..4f63aa0
--- /dev/null
+++ b/pvlib/bifacial.py
@@ -0,0 +1,148 @@
+"""
+The ``bifacial`` module contains functions for modeling back surface
+plane-of-array irradiance under various conditions.
+"""
+
+import pandas as pd
+
+
+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.,
+        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: datetime or DatetimeIndex
+        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 detailed outputs of the simulation; for
+        instance the shapely geometries, the irradiance components incident on
+        all surfaces of the PV array (for all timestamps), etc.
+        In the pvfactors documentation, this is refered to as the "surface
+        registry".
+
+    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.
+    """
+
+    # Convert pandas Series inputs to numpy arrays
+    if isinstance(solar_azimuth, pd.Series):
+        solar_azimuth = solar_azimuth.values
+    if isinstance(solar_zenith, pd.Series):
+        solar_zenith = solar_zenith.values
+    if isinstance(surface_azimuth, pd.Series):
+        surface_azimuth = surface_azimuth.values
+    if isinstance(surface_tilt, pd.Series):
+        surface_tilt = surface_tilt.values
+    if isinstance(dni, pd.Series):
+        dni = dni.values
+    if isinstance(dhi, pd.Series):
+        dhi = dhi.values
+
+    # 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,
+        'gcr': gcr,
+        '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']]
+
+    ipoa_back = df_outputs.loc[:, idx_slice[index_observed_pvrow,
+                                            'back', 'qinc']]
+
+    # Set timestamps as index of df_registries for consistency of outputs
+    df_registries = df_registries.set_index('timestamps')
+
+    return ipoa_front, ipoa_back, df_registries
diff --git a/pvlib/iotools/ecmwf_macc.py b/pvlib/iotools/ecmwf_macc.py
index 18262d9..ef0ee22 100644
--- a/pvlib/iotools/ecmwf_macc.py
+++ b/pvlib/iotools/ecmwf_macc.py
@@ -23,9 +23,6 @@ except ImportError:
             'To download data from ECMWF requires the API client.\nSee https:/'
             '/confluence.ecmwf.int/display/WEBAPI/Access+ECMWF+Public+Datasets'
         )
-    SERVER = None
-else:
-    SERVER = ECMWFDataServer()
 
 #: map of ECMWF MACC parameter keynames and codes used in API
 PARAMS = {
@@ -58,7 +55,7 @@ def _ecmwf(server, startdate, stopdate, params, targetname):
 
 
 def get_ecmwf_macc(filename, params, startdate, stopdate, lookup_params=True,
-                   server=SERVER, target=_ecmwf):
+                   server=None, target=_ecmwf):
     """
     Download data from ECMWF MACC Reanalysis API.
 
@@ -75,7 +72,7 @@ def get_ecmwf_macc(filename, params, startdate, stopdate, lookup_params=True,
     lookup_params : bool, default True
         optional flag, if ``False``, then codes are already formatted
     server : ecmwfapi.api.ECMWFDataServer
-        optionally provide a server object, default is given
+        optionally provide a server object, default is ``None``
     target : callable
         optional function that calls ``server.retrieve`` to pass to thread
 
@@ -86,9 +83,11 @@ def get_ecmwf_macc(filename, params, startdate, stopdate, lookup_params=True,
 
     Notes
     -----
-    To download data from ECMWF requires the API client. For more information,
-    see the `documentation
+    To download data from ECMWF requires the API client and a registration
+    key. Please read the documentation in `Access ECMWF Public Datasets
     <https://confluence.ecmwf.int/display/WEBAPI/Access+ECMWF+Public+Datasets>`_.
+    Follow the instructions in step 4 and save the ECMWF registration key
+    as `$HOME\.ecmwfapirc` or set `ECMWF_API_KEY` as the path to the key.
 
     This function returns a daemon thread that runs in the background. Exiting
     Python will kill this thread, however this thread will not block the main
diff --git a/setup.py b/setup.py
index 08708f2..bd61b42 100755
--- a/setup.py
+++ b/setup.py
@@ -45,7 +45,7 @@ INSTALL_REQUIRES = ['numpy >= 1.10.1',
 TESTS_REQUIRE = ['pytest', 'pytest-cov', 'pytest-mock', 'nose']
 EXTRAS_REQUIRE = {
     'optional': ['scipy', 'tables', 'numba', 'siphon', 'netcdf4',
-                 'ephem', 'cython'],
+                 'ephem', 'cython', 'pvfactors'],
     'doc': ['sphinx', 'ipython', 'sphinx_rtd_theme', 'numpydoc',
             'matplotlib'],
     'test': TESTS_REQUIRE


Test patch:
diff --git a/pvlib/test/conftest.py b/pvlib/test/conftest.py
index 48fbea3..431ea8e 100644
--- a/pvlib/test/conftest.py
+++ b/pvlib/test/conftest.py
@@ -69,6 +69,7 @@ requires_ephem = pytest.mark.skipif(not has_ephem, reason='requires ephem')
 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')
 
@@ -76,6 +77,7 @@ needs_pandas_0_17 = pytest.mark.skipif(
 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')
 
@@ -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
@@ -106,6 +110,7 @@ def has_numba():
         else:
             return True
 
+
 requires_numba = pytest.mark.skipif(not has_numba(), reason="requires numba")
 
 try:
@@ -125,3 +130,12 @@ except ImportError:
 
 requires_netCDF4 = pytest.mark.skipif(not has_netCDF4,
                                       reason='requires netCDF4')
+
+try:
+    import pvfactors  # noqa: F401
+    has_pvfactors = True
+except ImportError:
+    has_pvfactors = False
+
+requires_pvfactors = pytest.mark.skipif(not has_pvfactors,
+                                        reason='requires pvfactors')
diff --git a/pvlib/test/test_bifacial.py b/pvlib/test/test_bifacial.py
new file mode 100644
index 0000000..bb826b9
--- /dev/null
+++ b/pvlib/test/test_bifacial.py
@@ -0,0 +1,126 @@
+import pandas as pd
+from datetime import datetime
+from pvlib.bifacial import pvfactors_timeseries
+from conftest import requires_pvfactors
+
+
+@requires_pvfactors
+def test_pvfactors_timeseries():
+    """ Test that pvfactors is functional, using the TLDR section inputs of the
+    package github repo README.md file:
+    https://github.com/SunPower/pvfactors/blob/master/README.md#tldr---quick-start"""
+
+    # Create some inputs
+    timestamps = pd.DatetimeIndex([datetime(2017, 8, 31, 11),
+                                   datetime(2017, 8, 31, 12)]
+                                  ).set_names('timestamps')
+    solar_zenith = [20., 10.]
+    solar_azimuth = [110., 140.]
+    surface_tilt = [10., 0.]
+    surface_azimuth = [90., 90.]
+    dni = [1000., 300.]
+    dhi = [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 = pd.Series([1034.96216923, 795.4423259],
+                                    index=timestamps,
+                                    name=(1, 'front', 'qinc'))
+    expected_ipoa_back = pd.Series([92.11871485, 70.39404124],
+                                   index=timestamps,
+                                   name=(1, 'back', 'qinc'))
+
+    # Test serial calculations
+    ipoa_front, ipoa_back, df_registries = 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)
+
+    pd.testing.assert_series_equal(ipoa_front, expected_ipoa_front)
+    pd.testing.assert_series_equal(ipoa_back, expected_ipoa_back)
+    pd.testing.assert_index_equal(timestamps, df_registries.index.unique())
+
+    # Run calculations in parallel
+    ipoa_front, ipoa_back, df_registries = 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)
+
+    pd.testing.assert_series_equal(ipoa_front, expected_ipoa_front)
+    pd.testing.assert_series_equal(ipoa_back, expected_ipoa_back)
+    pd.testing.assert_index_equal(timestamps, df_registries.index.unique())
+
+
+@requires_pvfactors
+def test_pvfactors_timeseries_pandas_inputs():
+    """ Test that pvfactors is functional, using the TLDR section inputs of the
+    package github repo README.md file, but converted to pandas Series:
+    https://github.com/SunPower/pvfactors/blob/master/README.md#tldr---quick-start"""
+
+    # Create some inputs
+    timestamps = pd.DatetimeIndex([datetime(2017, 8, 31, 11),
+                                   datetime(2017, 8, 31, 12)]
+                                  ).set_names('timestamps')
+    solar_zenith = pd.Series([20., 10.])
+    solar_azimuth = pd.Series([110., 140.])
+    surface_tilt = pd.Series([10., 0.])
+    surface_azimuth = pd.Series([90., 90.])
+    dni = pd.Series([1000., 300.])
+    dhi = pd.Series([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 = pd.Series([1034.96216923, 795.4423259],
+                                    index=timestamps,
+                                    name=(1, 'front', 'qinc'))
+    expected_ipoa_back = pd.Series([92.11871485, 70.39404124],
+                                   index=timestamps,
+                                   name=(1, 'back', 'qinc'))
+
+    # Test serial calculations
+    ipoa_front, ipoa_back, df_registries = 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)
+
+    pd.testing.assert_series_equal(ipoa_front, expected_ipoa_front)
+    pd.testing.assert_series_equal(ipoa_back, expected_ipoa_back)
+    pd.testing.assert_index_equal(timestamps, df_registries.index.unique())
+
+    # Run calculations in parallel
+    ipoa_front, ipoa_back, df_registries = 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)
+
+    pd.testing.assert_series_equal(ipoa_front, expected_ipoa_front)
+    pd.testing.assert_series_equal(ipoa_back, expected_ipoa_back)
+    pd.testing.assert_index_equal(timestamps, df_registries.index.unique())
diff --git a/pvlib/test/test_tools.py b/pvlib/test/test_tools.py
index dfecea2..42eaedc 100644
--- a/pvlib/test/test_tools.py
+++ b/pvlib/test/test_tools.py
@@ -2,6 +2,7 @@ import pytest
 
 from pvlib import tools
 
+
 @pytest.mark.parametrize('keys, input_dict, expected', [
     (['a', 'b'], {'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 2}),
     (['a', 'b', 'd'], {'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 2}),