diff --git a/ci/requirements-py27.yml b/ci/requirements-py27.yml index 81df9d068c..cb77db8ca4 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 8f250f1a44..7c4d0ff35f 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 31fbdce30c..068e0bd534 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 25dac8e804..11378ba272 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 f5352e3605..0ac8aa4fde 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 2984449fce..e397456d14 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`) @@ -82,6 +84,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 @@ -95,3 +98,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 0000000000..4f63aa0922 --- /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/test/conftest.py b/pvlib/test/conftest.py index 48fbea3437..431ea8e46f 100644 --- a/pvlib/test/conftest.py +++ b/pvlib/test/conftest.py @@ -69,6 +69,7 @@ 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') @@ -76,6 +77,7 @@ def pandas_0_17(): 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 @@ def has_numba(): 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 0000000000..bb826b937f --- /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 dfecea2047..42eaedca58 100644 --- a/pvlib/test/test_tools.py +++ b/pvlib/test/test_tools.py @@ -2,6 +2,7 @@ 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}), diff --git a/setup.py b/setup.py index 08708f26b9..bd61b42c8d 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ 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