_images/pvlib_logo_horiz.png

pvlib python is a community supported tool that provides a set of functions and classes for simulating the performance of photovoltaic energy systems. pvlib python was originally ported from the PVLIB MATLAB toolbox developed at Sandia National Laboratories and it implements many of the models and methods developed at the Labs. More information on Sandia Labs PV performance modeling programs can be found at https://pvpmc.sandia.gov/. We collaborate with the PVLIB MATLAB project, but operate independently of it.

The source code for pvlib python is hosted on github.

Please see the Installation page for installation help.

For examples of how to use pvlib python, please see Package Overview and our Jupyter Notebook tutorials. The documentation assumes general familiarity with Python, NumPy, and Pandas. Google searches will yield many excellent tutorials for these packages.

The pvlib python GitHub wiki has a Projects and publications that use pvlib python page for inspiration and listing of your application.

There is a variable naming convention to ensure consistency throughout the library.

Citing pvlib python

Many of the contributors to pvlib-python work in institutions where citation metrics are used in performance or career evaluations. If you use pvlib python in a published work, please cite:

William F. Holmgren, Clifford W. Hansen, and Mark A. Mikofski. “pvlib python: a python package for modeling solar energy systems.” Journal of Open Source Software, 3(29), 884, (2018). https://doi.org/10.21105/joss.00884

Please also cite the DOI corresponding to the specific version of pvlib python that you used. pvlib python DOIs are listed at Zenodo.org

Additional pvlib python publications include:

  • J. S. Stein, “The photovoltaic performance modeling collaborative (PVPMC),” in Photovoltaic Specialists Conference, 2012.

  • R.W. Andrews, J.S. Stein, C. Hansen, and D. Riley, “Introduction to the open source pvlib for python photovoltaic system modelling package,” in 40th IEEE Photovoltaic Specialist Conference, 2014. (paper)

  • W.F. Holmgren, R.W. Andrews, A.T. Lorenzo, and J.S. Stein, “PVLIB Python 2015,” in 42nd Photovoltaic Specialists Conference, 2015. (paper and the notebook to reproduce the figures)

  • J.S. Stein, W.F. Holmgren, J. Forbess, and C.W. Hansen, “PVLIB: Open Source Photovoltaic Performance Modeling Functions for Matlab and Python,” in 43rd Photovoltaic Specialists Conference, 2016.

  • W.F. Holmgren and D.G. Groenendyk, “An Open Source Solar Power Forecasting Tool Using PVLIB-Python,” in 43rd Photovoltaic Specialists Conference, 2016.

Contents

Package Overview

Introduction

The core mission of pvlib-python is to provide open, reliable, interoperable, and benchmark implementations of PV system models.

There are at least as many opinions about how to model PV systems as there are modelers of PV systems, so pvlib-python provides several modeling paradigms: functions, the Location/PVSystem classes, and the ModelChain class. Read more about this in the Intro Examples section.

User extensions

There are many other ways to organize PV modeling code. We encourage you to build on these paradigms and to share your experiences with the pvlib community via issues and pull requests.

Getting support

pvlib usage questions can be asked on Stack Overflow and tagged with the pvlib tag.

The pvlib-python google group is used for discussing various topics of interest to the pvlib-python community. We also make new version announcements on the google group.

If you suspect that you may have discovered a bug or if you’d like to change something about pvlib, then please make an issue on our GitHub issues page .

How do I contribute?

We’re so glad you asked! Please see Contributing for information and instructions on how to contribute. We really appreciate it!

Credits

The pvlib-python community thanks Sandia National Lab for developing PVLIB Matlab and for supporting Rob Andrews of Calama Consulting to port the library to Python. Will Holmgren thanks the Department of Energy’s Energy Efficiency and Renewable Energy Postdoctoral Fellowship Program (2014-2016), the University of Arizona Institute for Energy Solutions (2017-2018), and the DOE Solar Forecasting 2 program (2018). The pvlib-python maintainers thank all of pvlib’s contributors of issues and especially pull requests. The pvlib-python community thanks all of the maintainers and contributors to the PyData stack.

Intro Examples

This page contains introductory examples of pvlib python usage.

Modeling paradigms

The backbone of pvlib-python is well-tested procedural code that implements PV system models. pvlib-python also provides a collection of classes for users that prefer object-oriented programming. These classes can help users keep track of data in a more organized way, provide some “smart” functions with more flexible inputs, and simplify the modeling process for common situations. The classes do not add any algorithms beyond what’s available in the procedural code, and most of the object methods are simple wrappers around the corresponding procedural code.

Let’s use each of these pvlib modeling paradigms to calculate the yearly energy yield for a given hardware configuration at a handful of sites listed below.

In [1]: import pandas as pd

In [2]: import matplotlib.pyplot as plt

In [3]: naive_times = pd.date_range(start='2015', end='2016', freq='1h')

# very approximate
# latitude, longitude, name, altitude, timezone
In [4]: coordinates = [(30, -110, 'Tucson', 700, 'Etc/GMT+7'),
   ...:                (35, -105, 'Albuquerque', 1500, 'Etc/GMT+7'),
   ...:                (40, -120, 'San Francisco', 10, 'Etc/GMT+8'),
   ...:                (50, 10, 'Berlin', 34, 'Etc/GMT-1')]
   ...: 

In [5]: import pvlib

# get the module and inverter specifications from SAM
In [6]: sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod')

In [7]: sapm_inverters = pvlib.pvsystem.retrieve_sam('cecinverter')

In [8]: module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']

In [9]: inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2896             try:
-> 2897                 return self._engine.get_loc(key)
   2898             except KeyError:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
<ipython-input-9-cb0c5fa8a3e2> in <module>
----> 1 inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_']

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/frame.py in __getitem__(self, key)
   2978             if self.columns.nlevels > 1:
   2979                 return self._getitem_multilevel(key)
-> 2980             indexer = self.columns.get_loc(key)
   2981             if is_integer(indexer):
   2982                 indexer = [indexer]

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2897                 return self._engine.get_loc(key)
   2898             except KeyError:
-> 2899                 return self._engine.get_loc(self._maybe_cast_indexer(key))
   2900         indexer = self.get_indexer([key], method=method, tolerance=tolerance)
   2901         if indexer.ndim > 1 or indexer.size > 1:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'

In [10]: temperature_model_parameters = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']

# specify constant ambient air temp and wind for simplicity
In [11]: temp_air = 20

In [12]: wind_speed = 0

Procedural

The straightforward procedural code can be used for all modeling steps in pvlib-python.

The following code demonstrates how to use the procedural code to accomplish our system modeling goal:

In [13]: system = {'module': module, 'inverter': inverter,
   ....:           'surface_azimuth': 180}
   ....: 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-13-8f44442cad28> in <module>
----> 1 system = {'module': module, 'inverter': inverter,
      2           'surface_azimuth': 180}

NameError: name 'inverter' is not defined

In [14]: energies = {}

In [15]: for latitude, longitude, name, altitude, timezone in coordinates:
   ....:     times = naive_times.tz_localize(timezone)
   ....:     system['surface_tilt'] = latitude
   ....:     solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude)
   ....:     dni_extra = pvlib.irradiance.get_extra_radiation(times)
   ....:     airmass = pvlib.atmosphere.get_relative_airmass(solpos['apparent_zenith'])
   ....:     pressure = pvlib.atmosphere.alt2pres(altitude)
   ....:     am_abs = pvlib.atmosphere.get_absolute_airmass(airmass, pressure)
   ....:     tl = pvlib.clearsky.lookup_linke_turbidity(times, latitude, longitude)
   ....:     cs = pvlib.clearsky.ineichen(solpos['apparent_zenith'], am_abs, tl,
   ....:                                  dni_extra=dni_extra, altitude=altitude)
   ....:     aoi = pvlib.irradiance.aoi(system['surface_tilt'], system['surface_azimuth'],
   ....:                                solpos['apparent_zenith'], solpos['azimuth'])
   ....:     total_irrad = pvlib.irradiance.get_total_irradiance(system['surface_tilt'],
   ....:                                                         system['surface_azimuth'],
   ....:                                                         solpos['apparent_zenith'],
   ....:                                                         solpos['azimuth'],
   ....:                                                         cs['dni'], cs['ghi'], cs['dhi'],
   ....:                                                         dni_extra=dni_extra,
   ....:                                                         model='haydavies')
   ....:     tcell = pvlib.temperature.sapm_cell(total_irrad['poa_global'],
   ....:                                         temp_air, wind_speed,
   ....:                                         **temperature_model_parameters)
   ....:     effective_irradiance = pvlib.pvsystem.sapm_effective_irradiance(
   ....:         total_irrad['poa_direct'], total_irrad['poa_diffuse'],
   ....:         am_abs, aoi, module)
   ....:     dc = pvlib.pvsystem.sapm(effective_irradiance, tcell, module)
   ....:     ac = pvlib.pvsystem.snlinverter(dc['v_mp'], dc['p_mp'], inverter)
   ....:     annual_energy = ac.sum()
   ....:     energies[name] = annual_energy
   ....: 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-15-aa55f107abd4> in <module>
      1 for latitude, longitude, name, altitude, timezone in coordinates:
      2     times = naive_times.tz_localize(timezone)
----> 3     system['surface_tilt'] = latitude
      4     solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude)
      5     dni_extra = pvlib.irradiance.get_extra_radiation(times)

NameError: name 'system' is not defined

In [16]: energies = pd.Series(energies)

# based on the parameters specified above, these are in W*hrs
In [17]: print(energies.round(0))
Series([], dtype: float64)

In [18]: energies.plot(kind='bar', rot=0)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-18-ce643011a4ea> in <module>
----> 1 energies.plot(kind='bar', rot=0)

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_core.py in __call__(self, *args, **kwargs)
    792                     data.columns = label_name
    793 
--> 794         return plot_backend.plot(data, kind=kind, **kwargs)
    795 
    796     def line(self, x=None, y=None, **kwargs):

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/__init__.py in plot(data, kind, **kwargs)
     60             kwargs["ax"] = getattr(ax, "left_ax", ax)
     61     plot_obj = PLOT_CLASSES[kind](data, **kwargs)
---> 62     plot_obj.generate()
     63     plot_obj.draw()
     64     return plot_obj.result

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in generate(self)
    277     def generate(self):
    278         self._args_adjust()
--> 279         self._compute_plot_data()
    280         self._setup_subplots()
    281         self._make_plot()

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in _compute_plot_data(self)
    412         # no non-numeric frames or series allowed
    413         if is_empty:
--> 414             raise TypeError("no numeric data to plot")
    415 
    416         # GH25587: cast ExtensionArray of pandas (IntegerArray, etc.) to

TypeError: no numeric data to plot

In [19]: plt.ylabel('Yearly energy yield (W hr)')
Out[19]: Text(0, 0.5, 'Yearly energy yield (W hr)')
_images/proc-energies.png

Object oriented (Location, PVSystem, ModelChain)

The first object oriented paradigm uses a model where a PVSystem object represents an assembled collection of modules, inverters, etc., a Location object represents a particular place on the planet, and a ModelChain object describes the modeling chain used to calculate PV output at that Location. This can be a useful paradigm if you prefer to think about the PV system and its location as separate concepts or if you develop your own ModelChain subclasses. It can also be helpful if you make extensive use of Location-specific methods for other calculations. pvlib-python also includes a SingleAxisTracker class that is a subclass of PVSystem.

The following code demonstrates how to use Location, PVSystem, and ModelChain objects to accomplish our system modeling goal. ModelChain objects provide convenience methods that can provide default selections for models and can also fill necessary input with modeled data. For example, no air temperature or wind speed data is provided in the input weather DataFrame, so the ModelChain object defaults to 20 C and 0 m/s. Also, no irradiance transposition model is specified (keyword argument transposition for ModelChain) so the ModelChain defaults to the haydavies model. In this example, ModelChain infers the DC power model from the module provided by examining the parameters defined for the module.

In [20]: from pvlib.pvsystem import PVSystem

In [21]: from pvlib.location import Location

In [22]: from pvlib.modelchain import ModelChain

In [23]: system = PVSystem(module_parameters=module,
   ....:                   inverter_parameters=inverter,
   ....:                   temperature_model_parameters=temperature_model_parameters)
   ....: 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-23-8644c4c6fde8> in <module>
      1 system = PVSystem(module_parameters=module,
----> 2                   inverter_parameters=inverter,
      3                   temperature_model_parameters=temperature_model_parameters)

NameError: name 'inverter' is not defined

In [24]: energies = {}

In [25]: for latitude, longitude, name, altitude, timezone in coordinates:
   ....:     times = naive_times.tz_localize(timezone)
   ....:     location = Location(latitude, longitude, name=name, altitude=altitude,
   ....:                         tz=timezone)
   ....:     weather = location.get_clearsky(times)
   ....:     mc = ModelChain(system, location,
   ....:                     orientation_strategy='south_at_latitude_tilt')
   ....:     mc.run_model(times=times, weather=weather)
   ....:     annual_energy = mc.ac.sum()
   ....:     energies[name] = annual_energy
   ....: 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-25-09581f5c3eaf> in <module>
      4                         tz=timezone)
      5     weather = location.get_clearsky(times)
----> 6     mc = ModelChain(system, location,
      7                     orientation_strategy='south_at_latitude_tilt')
      8     mc.run_model(times=times, weather=weather)

NameError: name 'system' is not defined

In [26]: energies = pd.Series(energies)

# based on the parameters specified above, these are in W*hrs
In [27]: print(energies.round(0))
Series([], dtype: float64)

In [28]: energies.plot(kind='bar', rot=0)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-28-ce643011a4ea> in <module>
----> 1 energies.plot(kind='bar', rot=0)

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_core.py in __call__(self, *args, **kwargs)
    792                     data.columns = label_name
    793 
--> 794         return plot_backend.plot(data, kind=kind, **kwargs)
    795 
    796     def line(self, x=None, y=None, **kwargs):

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/__init__.py in plot(data, kind, **kwargs)
     60             kwargs["ax"] = getattr(ax, "left_ax", ax)
     61     plot_obj = PLOT_CLASSES[kind](data, **kwargs)
---> 62     plot_obj.generate()
     63     plot_obj.draw()
     64     return plot_obj.result

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in generate(self)
    277     def generate(self):
    278         self._args_adjust()
--> 279         self._compute_plot_data()
    280         self._setup_subplots()
    281         self._make_plot()

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in _compute_plot_data(self)
    412         # no non-numeric frames or series allowed
    413         if is_empty:
--> 414             raise TypeError("no numeric data to plot")
    415 
    416         # GH25587: cast ExtensionArray of pandas (IntegerArray, etc.) to

TypeError: no numeric data to plot

In [29]: plt.ylabel('Yearly energy yield (W hr)')
Out[29]: Text(0, 0.5, 'Yearly energy yield (W hr)')
_images/modelchain-energies.png

Object oriented (LocalizedPVSystem)

The second object oriented paradigm uses a model where a LocalizedPVSystem represents a PV system at a particular place on the planet. This can be a useful paradigm if you’re thinking about a power plant that already exists.

The LocalizedPVSystem inherits from both PVSystem and Location, while the LocalizedSingleAxisTracker inherits from SingleAxisTracker (itself a subclass of PVSystem) and Location. The LocalizedPVSystem and LocalizedSingleAxisTracker classes may contain bugs due to the relative difficulty of implementing multiple inheritance. The LocalizedPVSystem and LocalizedSingleAxisTracker may be deprecated in a future release. We recommend that most modeling workflows implement Location, PVSystem, and ModelChain.

The following code demonstrates how to use a LocalizedPVSystem object to accomplish our modeling goal:

In [30]: from pvlib.pvsystem import LocalizedPVSystem

In [31]: energies = {}

In [32]: for latitude, longitude, name, altitude, timezone in coordinates:
   ....:     localized_system = LocalizedPVSystem(module_parameters=module,
   ....:                                          inverter_parameters=inverter,
   ....:                                          temperature_model_parameters=temperature_model_parameters,
   ....:                                          surface_tilt=latitude,
   ....:                                          surface_azimuth=180,
   ....:                                          latitude=latitude,
   ....:                                          longitude=longitude,
   ....:                                          name=name,
   ....:                                          altitude=altitude,
   ....:                                          tz=timezone)
   ....:     times = naive_times.tz_localize(timezone)
   ....:     clearsky = localized_system.get_clearsky(times)
   ....:     solar_position = localized_system.get_solarposition(times)
   ....:     total_irrad = localized_system.get_irradiance(solar_position['apparent_zenith'],
   ....:                                                   solar_position['azimuth'],
   ....:                                                   clearsky['dni'],
   ....:                                                   clearsky['ghi'],
   ....:                                                   clearsky['dhi'])
   ....:     tcell = localized_system.sapm_celltemp(total_irrad['poa_global'],
   ....:                                            temp_air, wind_speed)
   ....:     aoi = localized_system.get_aoi(solar_position['apparent_zenith'],
   ....:                                    solar_position['azimuth'])
   ....:     airmass = localized_system.get_airmass(solar_position=solar_position)
   ....:     effective_irradiance = localized_system.sapm_effective_irradiance(
   ....:         total_irrad['poa_direct'], total_irrad['poa_diffuse'],
   ....:         airmass['airmass_absolute'], aoi)
   ....:     dc = localized_system.sapm(effective_irradiance, tcell)
   ....:     ac = localized_system.snlinverter(dc['v_mp'], dc['p_mp'])
   ....:     annual_energy = ac.sum()
   ....:     energies[name] = annual_energy
   ....: 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-32-5b001ffad2db> in <module>
      1 for latitude, longitude, name, altitude, timezone in coordinates:
      2     localized_system = LocalizedPVSystem(module_parameters=module,
----> 3                                          inverter_parameters=inverter,
      4                                          temperature_model_parameters=temperature_model_parameters,
      5                                          surface_tilt=latitude,

NameError: name 'inverter' is not defined

In [33]: energies = pd.Series(energies)

# based on the parameters specified above, these are in W*hrs
In [34]: print(energies.round(0))
Series([], dtype: float64)

In [35]: energies.plot(kind='bar', rot=0)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-35-ce643011a4ea> in <module>
----> 1 energies.plot(kind='bar', rot=0)

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_core.py in __call__(self, *args, **kwargs)
    792                     data.columns = label_name
    793 
--> 794         return plot_backend.plot(data, kind=kind, **kwargs)
    795 
    796     def line(self, x=None, y=None, **kwargs):

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/__init__.py in plot(data, kind, **kwargs)
     60             kwargs["ax"] = getattr(ax, "left_ax", ax)
     61     plot_obj = PLOT_CLASSES[kind](data, **kwargs)
---> 62     plot_obj.generate()
     63     plot_obj.draw()
     64     return plot_obj.result

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in generate(self)
    277     def generate(self):
    278         self._args_adjust()
--> 279         self._compute_plot_data()
    280         self._setup_subplots()
    281         self._make_plot()

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in _compute_plot_data(self)
    412         # no non-numeric frames or series allowed
    413         if is_empty:
--> 414             raise TypeError("no numeric data to plot")
    415 
    416         # GH25587: cast ExtensionArray of pandas (IntegerArray, etc.) to

TypeError: no numeric data to plot

In [36]: plt.ylabel('Yearly energy yield (W hr)')
Out[36]: Text(0, 0.5, 'Yearly energy yield (W hr)')
_images/localized-pvsystem-energies.png

What’s New

These are new features and improvements of note in each release.

v0.7.0 (MONTH DAY, YEAR)

This is a major release that drops support for Python 2 and Python 3.4. We recommend all users of v0.6.3 upgrade to this release after checking API compatibility notes.

Python 2.7 support ended on June 1, 2019. (GH501) Minimum numpy version is now 1.10.4. Minimum pandas version is now 0.18.1.

API Changes

  • Changes related to cell temperature models (GH678):
    • Changes to functions
      • Moved functions for cell temperature from pvsystem.py to temperature.py.

      • Renamed pvsystem.sapm_celltemp and pvsystem.pvsyst_celltemp to temperature.sapm_cell and temperature.pvsyst_cell.

      • temperature.sapm_cell returns only the cell temperature, whereas the

        old pvsystem.sapm_celltemp returned a DataFrame with both cell and module temperatures.

      • Created temperature.sapm_module to return module temperature using the SAPM temperature model.

      • Changed the order of arguments for`pvsystem.sapm_celltemp`, pvsystem.pvsyst_celltemp and PVSystem.sapm_celltemp to be consistent among cell temperature model functions.

      • Removed model as a kwarg from temperature.sapm_cell and temperature.pvsyst_cell. These functions now require model-specific parameters.

      • Added the argument irrad_ref, default value 1000, to temperature.sapm_cell.

    • Changes to named temperature model parameter sets
      • Renamed pvsystem.TEMP_MODEL_PARAMS to temperature.TEMPERATURE_MODEL_PARAMETERS.

      • temperature.TEMPERATURE_MODEL_PARAMETERS uses dict rather than tuple for a parameter set.

      • Names for parameter sets in temperature.TEMPERATURE_MODEL_PARAMETERS have changed.

      • Parameter sets for the SAPM cell temperature model named ‘open_rack_polymer_thinfilm_steel’ and ‘22x_concentrator_tracker’ are considered obsolete and have been removed.

    • Changes to PVSystem class
      • Changed the model kwarg in PVSystem.sapm_celltemp and PVSystem.pvsyst_celltemp to parameter_set. parameter_set expects a str which is a valid key for temperature.TEMPERATURE_MODEL_PARAMETERS for the corresponding temperature model.

      • Added an attribute PVSystem.module_type (str) to record module front and back materials, default is glass_polymer.

      • Changed meaning of PVSystem.racking_model to describe racking only, e.g., default is open_rack.

      • Added an attribute PVSystem.temperature_model_parameters (dict). to contain temperature model parameters.

      • If PVSystem.temperature_model_parameters is not specified and PVSystem.racking_model and PVSystem.module_type combine to a valid parameter set name for the SAPM cell temperature model, that parameter set is assigned to PVSystem.temperature_model_parameters. Otherwise PVSystem.temperature_model_parameters is assigned an empty dict. The result is that the default parameter set for SAPM cell temperature model is open_rack_glass_polymer; the old default was open_rack_glass_glass.

    • Changes to ModelChain class
      • ModelChain.temp_model renamed to ModelChain.temperature_model.

      • ModelChain.temperature_model now defaults to None. The temperature model can be inferred from PVSystem.temperature_model_parameters.

      • ModelChain.temperature_model_parameters now defaults to None. The temperature model can be inferred from PVSystem.temperature_model_parameters.

      • ModelChain.temps attribute renamed to ModelChain.cell_temperature,

        and its datatype is now numeric rather than DataFrame.

      • If PVSystem.temperature_model_parameters is not specified, ModelChain defaults to old behavior, using the SAPM temperature model with parameter set open_rack_glass_glass. This behavior is deprecated, and will be

        removed in v0.8. In v0.8 PVSystem.temperature_model_parameters will

        be required for ModelChain.

      • Implemented pvsyst as an option for ModelChain.temperature_model.

      • modelchain.basic_chain has a new required argument temperature_model_parameters.

  • Calling pvlib.pvsystem.retrieve_sam() with no parameters will raise an exception instead of displaying a dialog.

  • The times keyword argument has been deprecated in the pvlib.modelchain.ModelChain.run_model(), pvlib.modelchain.ModelChain.prepare_inputs(), and pvlib.modelchain.ModelChain.complete_irradiance() methods. Model times are now determined by the input weather. DataFrame. Therefore, the weather DataFrame must have a DatetimeIndex. The weather argument of the above methods is now the first, required positional argument and the times argument is kept as the second keyword argument for capability during the deprecation period.

Enhancements

  • Created two new incidence angle modifier functions: pvlib.pvsystem.iam_martin_ruiz() and pvlib.pvsystem.iam_interp(). (GH751)

  • Updated the file for module parameters for the CEC model, from the SAM file dated 2017-6-5 to the SAM file dated 2019-03-05. (GH761)

  • Updated the file for inverter parameters for the CEC model, from the SAM file dated 2018-3-18 to the SAM file dated 2019-03-05. (GH761)

  • Added recombination current parameters to bishop88 single-diode functions and also to pvlib.pvsystem.max_power_point(). (GH762)

  • Add ivtools module to contain functions for IV model fitting.

  • Add fit_sde_sandia(), a simple method to fit the single diode equation to an IV curve.

  • Add fit_sdm_cec_sam(), a wrapper for the CEC single diode model fitting function ‘6parsolve’ from NREL’s System Advisor Model.

  • Add fit_sdm_desoto(), a method to fit the single diode equation to the typical specifications given in manufacturers datasheets.

Bug fixes

  • Fix handling of keyword arguments in forecasts.get_processed_data. (GH745)

  • Fix output as Series feature in pvlib.pvsystem.ashraeiam().

  • Fix rounding issue in clearsky._linearly_scale, a function that converts longitude or latitude degree to an index number in a Linke turbidity lookup table. Also rename the function to clearsky._degrees_to_index. (GH754)

Testing

  • Added 30 minutes to timestamps in test_psm3.csv to match change in NSRDB (GH733)

  • Added tests for methods in bifacial.py.

  • Added tests for changes to cell temperature models.

Documentation

  • Corrected docstring for pvsystem.PVSystem.sapm

Removal of prior version deprecations

  • Removed irradiance.extraradiation.

  • Removed irradiance.grounddiffuse.

  • Removed irradiance.total_irrad.

  • Removed irradiance.globalinplane.

  • Removed atmosphere.relativeairmass.

  • Removed atmosphere.relativeairmass.

  • Removed solarposition.get_sun_rise_set_transit.

  • Removed tmy module.

  • Removed ModelChain.singlediode method.

  • Removed ModelChain.prepare_inputs clearsky assumption when no irradiance data was provided.

Enhancements

Contributors

v0.6.3 (May 15, 2019)

This is a minor release on top of v0.6.2 to fix an installation issue. We recommend that all users of v0.6.1 and v0.6.2 upgrade to this release.

Python 2.7 support will end on June 1, 2019. Releases made after this date will require Python 3. This release is likely to be the last that supports Python 2.7. (GH501)

Bug fixes

  • Fix installation issue due to missing requests dependency. (GH725)

Contributors

v0.6.2 (May 15, 2019)

This is a minor release. We recommend all users of v0.6.1 upgrade to this release.

Python 2.7 support will end on June 1, 2019. Releases made after this date will require Python 3. This release is likely to be the last that supports Python 2.7. (GH501)

Minimum pandas requirement bumped 0.15.0=>0.16.0

API Changes

Enhancements

Bug fixes

  • Compatibility with pandas 0.24 deprecations. (GH659)

  • pvwatts_ac() raised ZeroDivisionError when called with scalar pdc=0 and a RuntimeWarning for array(0) input. Now correctly returns 0s of the appropriate type. (GH675)

  • Fixed erbs() behavior when zenith is near 90 degrees. (GH681)

  • dni() now referenced in API under Decomposing and Combining irradiance header. (GH686)

  • Fixed NaN output from singleaxis() when sun near horizon. (GH656)

  • Fixed numpy warnings in singleaxis() when comparing NaN values to limits. (GH622)

  • Change ModelChain to apply pvwatts_losses to mc.dc instead of mc.ac. (GH696)

  • Fixed a bug in the day angle equation for the ASCE extraterrestrial irradiance model. (GH211)

  • Silenced divide by 0 irradiance warnings in klucher() and calcparams_desoto(). (GH698)

  • Fix NDFD model by updating variables.

  • Fix format_index() to parse non one-minute data correctly. (GH709)

Testing

  • Remove most expected warnings emitted by test suite. (GH698)

Contributors

v0.6.1 (January 31, 2019)

This is a minor release. We recommend all users of v0.6.0 upgrade to this release.

Python 2.7 support will end on June 1, 2019. Releases made after this date will require Python 3. (GH501)

Minimum pandas requirement bumped 0.14.0=>0.15.0

API Changes

Enhancements

Bug fixes

  • Fix when building documentation using Matplotlib 3.0 or greater.

  • ~pvlib.spa.calculate_deltat: Fix constant coefficient of the polynomial expression for years >= 1860 and < 1900, fix year 2050 which was returning 0. (GH600)

  • Fix and improve hour_angle() (GH598)

  • Fix error in pvlib.clearsky.detect_clearsky() (GH506)

  • Fix documentation errors when using IPython >= 7.0.

  • Fix error in pvlib.modelchain.ModelChain.infer_spectral_model() (GH619)

  • Fix error in pvlib.spa when using Python 3.7 on some platforms.

  • Fix error in pvlib.irradiance._delta_kt_prime_dirint() (GH637). The error affects the first and last values of DNI calculated by the function pvlib.irradiance.dirint()

  • Fix errors on Python 2.7 and Numpy 1.6. (GH642)

  • Replace deprecated np.asscalar with array.item(). (GH642)

Testing

Contributors

v0.6.0 (September 17, 2018)

This is a major release and contains a large number of API changes, new features, and bug fixes. Users should carefully read the changelog below before upgrading.

Python 2.7 support will end on June 1, 2019. Releases made after this date will require Python 3. (GH501)

API Changes

  • pvlib python is changing a handful of function names. In general, functions that can calculate a quantity using multiple algorithms now start with the prefix get_. For example, relativeairmass can calculate airmass using one of many model arguments. Its name has been changed to get_relative_airmass(). The old function names remain in this release, but will emit a PVLibDeprecationWarning when called. The old functions will be removed in the v0.7 release. Functions composed of multiple words jammed together have been renamed with underscores separating the words (see above). Each change is detailed below. (GH427)

  • Deprecated relativeairmass; it will be removed in v0.7. Use the new get_relative_airmass() instead. (GH427)

  • Deprecated absoluteairmass; it will be removed in v0.7. Use the new get_absolute_airmass() instead. (GH427)

  • Deprecated irradiance.globalinplane; it will be removed in v0.7. Use the new poa_components() instead. (GH427)

  • Added poa_components(). Function is the same as the now-deprecated irradiance.globalinplane, but adds 'poa_sky_diffuse' and 'poa_ground_diffuse' to the output. (GH427)

  • Deprecated irradiance.extraradiation; it will be removed in v0.7. Use pvlib.irradiance.get_extra_radiation() instead. (GH427)

  • Deprecated irradiance.grounddiffuse; it will be removed in v0.7. Use get_ground_diffuse() instead. (GH427)

  • Added get_poa_sky_diffuse(). (GH427)

  • Deprecated irradiance.total_irrad; it will be removed in v0.7. Use get_total_poa_irradiance() instead. (GH427)

  • Removed 'klutcher' from get_sky_diffuse/total_irrad. This misspelling was deprecated long ago but never removed. (GH97)

  • calcparams_desoto() now requires arguments for each module model parameter. (GH462)

  • Add losses_parameters attribute to PVSystem objects and remove the kwargs support from PVSystem.pvwatts_losses. Enables custom losses specification in ModelChain calculations. (GH484)

  • removed irradiance parameter from ModelChain.run_model and ModelChain.prepare_inputs

  • Add perez_enhancement keyword argument to clearsky.ineichen to control whether or not the “perez enhancement factor” is applied. The enhancement factor was always applied until now. Now it is turned off by default. The enhancement factor can yield unphysical results, especially for latitudes closer to the poles and especially in the winter months. It may yield improved results under other conditions. (GH435)

  • Add min_cos_zenith, max_zenith keyword arguments to disc, dirint, and dirindex functions. (GH311, GH396)

  • Method ModelChain.infer_dc_model now returns a tuple (function handle, model name string) instead of only the function handle (GH417)

  • Add DC model methods desoto and pvsyst to ModelChain, and deprecates DC model method singlediode (singlediode defaults to desoto until v0.7.0) (GH487)

  • Add the CEC module model in pvsystem.calcparams_cec and ModelChain.cec. The CEC model differs from the desoto model by using the parameter Adjust. Modules selected from the SAM CEC library sam-library-cec-modules-2017-6-5.csv include the Adjust parameter and ModelChain.infer_dc_model will now select the cec model rather than the desoto model. (GH463)

  • The behavior of irradiance.perez(return_components=True) has changed. The function previously returned a tuple of total sky diffuse and an OrderedDict/DataFrame of components. The function now returns an OrderedDict/DataFrame with total sky diffuse and each component. The behavior for return_components=False remains unchanged. (GH434)

Enhancements

  • Add sea surface albedo in irradiance.py (GH458)

  • Implement first_solar_spectral_loss() in modelchain.py (GH359)

  • Clarify arguments Egref and dEgdT for calcparams_desoto() (GH462)

  • Add pvsystem.calcparams_pvsyst to compute values for the single diode equation using the PVsyst v6 model (GH470)

  • Extend singlediode() with an additional keyword argument method in ('lambertw', 'newton', 'brentq'), default is 'lambertw', to select a method to solve the single diode equation for points on the IV curve. Selecting either 'brentq' or 'newton' as the method uses bishop88() with the corresponding method. (GH410)

  • Implement new methods 'brentq' and 'newton' for solving the single diode equation for points on the IV curve. 'brentq' uses a bisection method (Brent, 1973) that may be slow but guarantees a solution. 'newton' uses the Newton-Raphson method and may be faster but is not guaranteed to converge. However, 'newton' should be safe for well-behaved IV curves. (GH408)

  • Implement bishop88() for explicit calculation of arbitrary IV curve points using diode voltage instead of cell voltage. If method is either 'newton' or 'brentq' and ivcurve_pnts in singlediode() is provided, the IV curve points will be log spaced instead of linear.

  • Implement estimate_voc() to estimate open circuit voltage by assuming \(R_{sh} \to \infty\) and \(R_s=0\) as an upper bound in bisection method for singlediode() when method is either 'newton' or 'brentq'.

  • Add max_power_point() method to compute the max power point using the new 'brentq' method.

  • Add new module pvlib.singlediode with low-level functions for solving the single diode equation such as: bishop88(), estimate_voc(), bishop88_i_from_v(), bishop88_v_from_i(), and bishop88_mpp().

  • Add PVSyst thin-film recombination losses for CdTe and a:Si (GH163)

  • Python 3.7 officially supported. (GH496)

  • Improve performance of solarposition.ephemeris. (GH512)

  • Improve performance of Location.get_airmass. Most noticeable when solar position is supplied, time index length is less than 10000, and method is looped over. (GH502)

  • Add irradiance.gti_dirint function. (GH396)

  • Add irradiance.clearness_index function. (GH396)

  • Add irradiance.clearness_index_zenith_independent function. (GH396)

  • Add checking for consistency between module_parameters and dc_model. (GH417)

  • Add DC model methods 'desoto' and 'pvsyst' to ModelChain (GH487)

  • Add the CEC module model in pvsystem.calcparams_cec and ModelChain.cec. (GH463)

  • Add DC model methods desoto and pvsyst to ModelChain (GH487)

  • pvlib now ships with a pvlib[optional] installation option to automatically install packages needed to support additional pvlib features: pip install pvlib[optional]. Additional installation options include doc (requirements for minimal documentation build), test (requirements for testing), and all (optional + doc + test). (GH553, GH483)

  • Set default alpha to 1.14 in angstrom_aod_at_lambda() (GH563)

  • tracking.singleaxis now accepts scalar and 1D-array input.

Bug fixes

  • Unset executable bits of irradiance.py and test_irradiance.py (GH460)

  • Fix failing tests due to column order on Python 3.6+ and Pandas 0.23+ (GH464)

  • ModelChain.prepare_inputs failed to pass solar_position and airmass to Location.get_clearsky. Fixed. (GH481)

  • Add User-Agent specification to TMY3 remote requests to avoid rejection. (GH493)

  • Fix pvlib.irradiance.klucher output is different for Pandas Series vs. floats and NumPy arrays. (GH508)

  • Make GitHub recognize the license, add AUTHORS.md, clarify shared copyright. (GH503)

  • Fix issue with non-zero direct irradiance contribution to Reindl, Klucher, and Hay-Davies diffuse sky algorithms when the sun is behind the array. (GH526)

  • Fix issue with dividing by near-0 cos(solar_zenith) values in Reindl and Hay-Davies diffuse sky algorithms. (GH432)

  • Fix argument order of longitude and latitude when querying weather forecasts by lonlat bounding box (GH521)

  • Fix issue with unbounded clearness index calculation in disc. (GH540)

  • Limit pvwatts_ac results to be greater than or equal to 0. (GH541)

  • Fix bug in get_relative_airmass(model=’youngirvine1967’). (GH545)

  • Fix bug in variable names returned by forecast.py’s HRRR_ESRL model. (GH557)

  • Fixed bug in tracking.singleaxis that mistakenly assigned nan values when the Sun was still above the horizon. No effect on systems with axis_tilt=0. (GH569)

  • Source distribution did not contain LICENSE file. Added LICENSE, AUTHORS.md, and some docs to MANIFEST. (GH579)

  • Patch SPA C-files to fix timezone macro name clash with pyconfig.h. (GH168)

Documentation

  • Expand testing section with guidelines for functions, PVSystem/Location objects, and ModelChain.

  • Updated several incorrect statements in ModelChain documentation regarding implementation status and default values. (GH480)

  • Expanded general contributing and pull request guidelines.

  • Added section on single diode equation with some detail on solutions used in pvlib-python (GH518)

  • Minor improvements and updates to installation documentation. (GH531)

  • Improve LocalizedPVSystem and LocalizedSingleAxisTracker documentation. (GH532)

  • Move the “Getting Started”/”Modeling Paradigms” section to a new top-level “Intro Examples” page.

  • Copy pvlib documentation’s “Getting support” section to README.md.

  • Add PVSystem documentation page. (GH514, GH319)

  • Add example of Kasten Linke Turbidity calculation, discuss broadband AOD and Angstrom Turbidity Model. (GH302)

  • Add JOSS paper to “Citing pvlib-python” section.

Testing

  • Add pytest-mock dependency

  • Use pytest-mock to ensure that PVSystem and ModelChain methods call corresponding functions correctly. Removes implicit dependence on precise return values of some function/methods. (GH394)

  • Additional test refactoring to limit test result dependence to a single function per test. (GH394)

  • Use pytest-mock to ensure that ModelChain DC model is set up correctly.

  • Add Python 3.7 to build matrix

  • Make test_forecast.py more robust. (GH293)

  • Improve test_atmosphere.py. (GH158)

  • Add LGTM.com integration. (GH554)

  • Add SticklerCI integration.

  • Add codecov integration.

Contributors

v0.5.2 (May 13, 2018)

API Changes

  • removed unused ‘pressure’ arg from irradiance.liujordan function (GH386)

  • replaced logging.warning calls with warnings.warn calls, and removed logging.debug calls. We encourage users to explore tools such as pdb and trackback in place of the logging.debug calls. Fixes (GH447).

Enhancements

  • Improve clearsky.lookup_linke_turbidity speed, changing .mat source file to .h5 (GH437)

  • Updated libraries for CEC module parameters to SAM release 2017.9.5 (library dated 2017.6.30) and CEC inverter parameters to file posted to www.github.com/NREL/SAM on 2018.3.18, with one entry removed due to a missing parameter value. (:issue:’440’)

Bug fixes

  • fixed redeclaration of test_simplified_solis_series_elevation (GH387)

  • physicaliam now returns a Series if called with a Series as an argument. (GH397)

  • corrected docstring for irradiance.total_irrad (:issue: ‘423’)

  • modified solar_azimuth_analytical to handle some borderline cases, thereby avoiding the NaN values and/or warnings that were previously produced (:issue: ‘420’)

  • removed RuntimeWarnings due to divide by 0 or nans in input data within irradiance.perez, clearsky.simplified_solis, pvsystem.adrinverter, pvsystem.ashraeiam, pvsystem.physicaliam, pvsystem.sapm_aoi_loss, pvsystem.v_from_i. (GH428)

Documentation

  • Improve physicaliam doc string. (GH397)

Testing

  • Test Python 3.6 on Windows with Appveyor instead of 3.4. (GH392)

  • Fix failing test on pandas 0.22 (GH406)

  • Fix too large test tolerance (GH414)

Contributors

  • Cliff Hansen

  • Will Holmgren

  • KonstantinTr

  • Anton Driesse

  • Cedric Leroy

v0.5.1 (October 17, 2017)

API Changes

  • pvsystem.v_from_i and pvsystem.i_from_v functions now accept resistance_series = 0 and/or resistance_shunt = numpy.inf as inputs (GH340)

Enhancements

  • Improve clearsky.lookup_linke_turbidity speed. (GH368)

  • Ideal devices supported in single diode model, e.g., resistance_series = 0 and/or resistance_shunt = numpy.inf (GH340)

  • pvsystem.v_from_i and pvsystem.i_from_v computations for near ideal devices are more numerically stable. However, very, very near ideal resistance_series and/or resistance_shunt may still cause issues with the implicit solver (GH340)

Bug fixes

  • Remove condition causing Overflow warning from clearsky.haurwitz (GH363)

  • modelchain.basic_chain now correctly passes ‘solar_position_method’ arg to solarposition.get_solarposition (GH370)

  • Fixed: Variables and Symbols extra references not available (GH380)

  • Removed unnecessary calculations of alpha_prime in spa.solar_position_numpy and spa.solar_position_loop (GH366)

  • Fixed args mismatch for solarposition.pyephem call from solarposition.get_solarposition with method=’pyephem’ arg to solarposition.get_solarposition (GH370)

  • ModelChain.prepare_inputs and ModelChain.complete_irradiance now correctly pass the ‘solar_position_method’ argument to solarposition.get_solarposition (GH377)

  • Fixed usage of inplace parameter for tmy._recolumn (GH342)

Documentation

  • Doc string of modelchain.basic_chain was updated to describe args more accurately. (GH370)

  • Doc strings of singlediode, pvsystem.v_from_i, and pvsystem.i_from_v were updated to describe acceptable input arg ranges. (GH340)

Testing

  • Changed test for clearsky.haurwitz to operate on zenith angles

  • Significant new test cases added for pvsystem.v_from_i and pvsystem.i_from_v (GH340)

Contributors

  • Cliff Hansen

  • KonstantinTr

  • Will Holmgren

  • Mark Campanelli

  • DaCoEx

v0.5.0 (August 11, 2017)

API Changes

  • Removed parameter w from _calc_d (GH344)

  • SingleAxisTracker.get_aoi and SingleAxisTracker.get_irradiance now require surface_zenith and surface_azimuth (GH351)

  • Changes calculation of the Incidence Angle Modifier to return 0 instead of np.nan for angles >= 90°. This improves the calculation of effective irradiance close to sunrise and sunset. (GH338)

  • Change the default ModelChain orientation strategy from ‘south_at_latitude_tilt’ to None. (GH290)

Bug fixes

  • Method of multi-inheritance has changed to make it possible to use kwargs in the parent classes of LocalizedPVSystem and LocalizedSingleAxisTracker (GH330)

  • Fix the __repr__ method of ModelChain, crashing when orientation_strategy is set to ‘None’ (GH352)

  • Fix the ModelChain’s angle of incidence calculation for SingleAxisTracker objects (GH351)

  • Fix issue with ForecastModel.cloud_cover_to_transmittance_linear method of forecast.py ignoring ‘offset’ parameter. (GH343)

Enhancements

  • Added default values to docstrings of all functions (GH336)

  • Added analytical method that calculates solar azimuth angle (GH291)

Documentation

  • Added ModelChain documentation page

  • Added nbsphinx to documentation build configuration.

  • Added a pull request template file (GH354)

Testing

  • Added explicit tests for aoi and aoi_projection functions.

  • Update test of ModelChain.__repr__ to take in account GH352

  • Added a test for solar_azimuth_analytical function.

Contributors

  • Johannes Kaufmann

  • Will Holmgren

  • Uwe Krien

  • Alaina Kafkes

  • Birgit Schachler

  • Jonathan Gaffiot

  • Siyan (Veronica) Guo

  • KonstantinTr

v0.4.5 (June 5, 2017)

Bug fixes

  • Fix pandas 0.20 incompatibilities in Location.get_clearsky, solarposition.ephemeris (GH325)

  • Fixes timezone issue in solarposition spa_c function (GH237)

  • Added NREL Bird clear sky model. (GH276)

  • Added lower accuracy formulas for equation of time, declination, hour angle and solar zenith.

  • Remove all instances of .ix (GH322)

  • Update docstring in pvlib.spa.solar_position - change units of pressure to millibars. NOTE: units of pressure in pvlib.solar_position.spa_python and pvlib.solar_position.spa_c are still Pascals. This update should have no effect on most users, since it only applies to the low-level spa.py module. (GH327)

Enhancements

  • Added irradiance.dni method that determines DNI from GHI and DHI and corrects unreasonable DNI values during sunrise/sunset transitions

  • ForecastModel will now only connect to the Unidata server when necessary, rather than when the object is created. This supports offline work and speeds up analysis of previously downloaded data.

Contributors

  • Will Holmgren

  • Marc Anoma

  • Mark Mikofski

  • Birgit Schachler

v0.4.4 (February 18, 2017)

Enhancements

  • Added Anton Driesse Inverter database and made compatible with pvsystem.retrieve_sam. (GH169)

  • Ported Anton Driesse Inverter model from PV_LIB Toolbox. (GH160)

  • Added Kasten pyrheliometric formula to calculate Linke turbidity factors with improvements by Ineichen and Perez to extend range of air mass (GH278)

  • Added coefficients for CIGS and a-Si modules types to the first_solar_spectral_correction function (GH308)

API Changes

  • Change PVSystem default module_parameters and inverter_parameters to empty dict. Code that relied on these attributes being None or raising a TypeError will need to be updated. (issue:294)

Documentation

  • Fixes the Forecasting page’s broken links to the tutorials.

  • Fixes the Forecasting page’s broken examples. (GH299)

  • Fixes broken Classes link in the v0.3.0 documentation.

Bug fixes

  • Resolved several issues with the forecast module tests. Library import errors were resolved by prioritizing the conda-forge channel over the default channel. Stalled ci runs were resolved by adding a timeout to the HRRR_ESRL test. (GH293)

  • Fixed issue with irradiance jupyter notebook tutorial. (GH309)

Contributors

  • Will Holmgren

  • Volker Beutner

  • Mark Mikofski

  • Anton Driesse

  • Mitchell Lee

v0.4.3 (December 28, 2016)

Enhancements

  • Adding implementation of Perez’s DIRINDEX model based on existing DIRINT model implementation. (GH282)

  • Added clearsky.detect_clearsky function to determine the clear times in a GHI time series. (GH284)

Other

  • Adds Python 3.6 to compatibility statement and pypi classifiers. (GH286)

Contributors

  • Marc Anoma

  • Will Holmgren

  • Cliff Hansen

  • Tony Lorenzo

v0.4.2 (December 7, 2016)

This is a minor release from 0.4.1.

Bug fixes

  • Fixed typo in __repr__ method of ModelChain and in its regarding test.

  • PVSystem.pvwatts_ac could not use the eta_inv_ref kwarg and PVSystem.pvwatts_dc could not use the temp_ref kwarg. Fixed. (GH252)

  • Fixed typo in ModelChain.infer_spectral_model error message. (GH251)

  • Fixed Linke turbdity factor out of bounds error at 90-degree latitude or at 180-degree longitude (GH262)

  • Fixed Linke turbidity factor grid spacing and centers (GH263)

API Changes

  • The run_model method of the ModelChain will use the weather parameter of all weather data instead of splitting it to irradiation and weather. The irradiation parameter still works but will be removed soon. (GH239)

  • delta_t kwarg is now 67.0 instead of None. IMPORTANT: Setting delta_t as None will break the code for the Numba accelerated calculations. This will be fixed in a future version. (GH165)

Enhancements

  • Adding a complete_irradiance method to the ModelChain to make it possible to calculate missing irradiation data from the existing columns [beta]. (GH239)

  • Added calculate_deltat method to the spa module to calculate the time difference between terrestrial time and UT1. Specifying a scalar is sufficient for most calculations. (GH165)

  • Added more attributes to ModelChain, PVSystem, and Location printed representations. (GH254)

  • Added name attribute to ModelChain and PVSystem. (GH254)

  • Restructured API section of the documentation so that there are separate pages for each function, class, or method. (GH258)

  • Improved Linke turbidity factor time interpolation with Python calendar month days and leap years (GH265)

  • Added option to return diffuse components from Perez transposition model.

Other

  • Typical modeling results could change by ~1%, depending on location, if they depend on the turbidity table

  • Fixed issues with pvsystem, tracking, and tmy_to_power jupyter notebooks (GH267, GH273)

Code Contributors

  • Uwe Krien

  • Will Holmgren

  • Volker Beutner

  • Mark Mikofski

  • Marc Anoma

  • Giuseppe Peronato

v0.4.1 (October 5, 2016)

This is a minor release from 0.4.0. We recommend that all users upgrade to this version, especially if they want to use the latest versions of pandas.

Bug fixes

  • Fixed an error in the irradiance.klucher transposition model. The error was introduced in version 0.4.0. (GH228)

  • Update RAP forecast model variable names. (GH241)

  • Fix incompatibility with pandas 0.19 and solar position calculations. (GH246)

Documentation

  • Fixed a typo in the pvsystem.sapm returns description. (GH234)

  • Replaced nosetests references with py.test. (GH232)

  • Improve the rendering of the snlinverter doc string. (GH242)

Code Contributors

  • Mark Mikofski

  • Johannes Dollinger

  • Will Holmgren

v0.4.0 (July 28, 2016)

This is a major release from 0.3.3. We recommend that all users upgrade to this version after reviewing the API Changes. Please see the Bug Fixes for changes that will result in slightly different modeling results.

API Changes

  • Remove unneeded module argument from singlediode function. (GH200)

  • In pvlib.irradiance.perez, renamed argument modelt to model. (GH196)

  • Functions in the irradiance module now work with scalar inputs in addition to arrays and Series. Furthermore, these functions no longer promote scalar or array input to Series output. Also applies to atmosphere.relativeairmass. (GH201, GH214)

  • Reorder the ashraeiam, physicaliam, and snlinverter arguments to put the most variable arguments first. Adds default arguments for the IAM functions. (GH197)

  • The irradiance.extraradiation function input/output type consistency across different methods has been dramatically improved. (GH217, GH219)

  • Updated to pvsystem.sapm to be consistent with the PVLIB MATLAB API. pvsystem.sapm now takes an effective irradiance argument instead of POA irradiances, airmass, and AOI. Implements closely related sapm_spectral_loss, sapm_aoi_loss, and sapm_effective_irradiance functions, as well as PVSystem methods. The sapm_aoi_loss function includes an optional argument to apply an upper limit to the output (output can be ~1% larger than 1 for AOI of ~30 degrees). (GH198, GH205, GH218)

  • The pvsystem.retrieve_sam keyword argument samfile has been replaced with path. A selection dialog window is now activated by not supplying any arguments to the function. The API for typical usage remains unchanged, however, the data will be loaded from a local file rather than the SAM website. (GH52)

Enhancements

  • Adds the First Solar spectral correction model. (GH115)

  • Adds the Gueymard 1994 integrated precipitable water model. (GH115)

  • Adds the PVWatts DC, AC, and system losses model. (GH195)

  • Improve PEP8 conformity in irradiance module. (GH214)

  • irradiance.disc is up to 10x faster. (GH214)

  • Add solarposition.nrel_earthsun_distance function and option to calculate extraterrestrial radiation using the NREL solar position algorithm. (GH211, GH215)

  • pvsystem.singlediode can now calculate IV curves if a user supplies an ivcurve_pnts keyword argument. (GH83)

  • Includes SAM data files in the distribution. (GH52)

  • ModelChain now implements SAPM, PVWatts, Single Diode and user-defined modeling options. See Will Holmgren’s ModelChain refactor gist for more discussion about new features in ModelChain. (GH143, GH194)

  • Added forecast.py module for solar power forecasts. (GH86, GH124, GH180)

Bug fixes

  • Fixed an error in pvsystem.singlediode’s i_mp, v_mp, and p_mp calculations when using array or Series input. The function wrongly returned solutions when any single point is within the error tolerance, rather than requiring that the solution for all points be within the error tolerance. Results in test scenarios changed by 1-10%. (GH221)

  • Fixed a numerical overflow error in pvsystem.singlediode’s v_oc determination for some combinations of parameters. (GH225)

  • dirint function yielded the wrong results for non-sea-level pressures. Fixed. (GH212)

  • Fixed a bug in the day angle calculation used by the ‘spencer’ and ‘asce’ extraterrestrial radiation options. Most modeling results will be changed by less than 1 part in 1000. (GH211)

  • irradiance.extraradiation now raises a ValueError for invalid method input. It previously failed silently. (GH215)

Documentation

  • Added new terms to the variables documentation. (GH195)

  • Added clear sky documentation page.

  • Fix documentation build warnings. (GH210)

  • Removed an unneeded note in irradiance.extraradiation. (GH216)

Other

  • pvlib-python is now available on the conda-forge channel: conda install pvlib-python -c conda-forge (GH154)

  • Switch to the py.test testing framework. (GH204)

  • Reconfigure Appveyor CI builds and resolve an issue in which the command line length was too long. (GH207)

  • Manually build numpy and pandas for the min requirements test. This is needed to avoid Continuum’s bad practice of bundling scipy with pandas. (GH214)

Requirements

  • pvlib now requires pandas >= 0.14.0 and numpy >= 1.9.0, both released in 2014. Most of pvlib will work with lesser versions. (GH214)

Code Contributors

  • Will Holmgren

  • Jonathan Chambers

  • Mitchell Lee

  • Derek Groenendyk

v0.3.3 (June 15, 2016)

This is a minor release from 0.3.2. We recommend that all users upgrade to this version.

API Changes

  • Renamed series_modules to modules_per_string and parallel_modules to strings_per_inverter. (GH176)

  • Changed two of the TMY3 data reader fields for consistency with the rest of the fields and with PVLIB MATLAB. Changed ‘PresWth source’ to ‘PresWthSource’, and ‘PresWth uncert’ to ‘PresWthUncertainty’. (GH193)

Enhancements

  • Adds the Erbs model. (GH2)

  • Adds the scale_voltage_current_power function and PVSystem method to support simple array modeling. (GH159)

  • Adds support for SingleAxisTracker objects in ModelChain. (GH169)

  • Add __repr__ method to PVSystem, LocalizedPVSystem, ModelChain, SingleAxisTracker, Location. (GH142)

  • Add v_from_i function for solving the single diode model. (GH190)

  • Improve speed of singlediode function by using v_from_i to determine v_oc. Speed is ~2x faster. (GH190)

  • Adds the Simplified Solis clear sky model. (GH148)

Bug fixes

  • Fix another bug with the Appveyor continuous integration builds. (GH170)

  • Add classifiers to setup.py. (GH181)

  • Fix snlinverter and singlediode documentation. They incorrectly said that inverter/module must be a DataFrame, when in reality they can be any dict-like object. (GH157)

  • Fix numpy 1.11 deprecation warnings caused by some functions using non-integer indices.

  • Propagate airmass data through ModelChain’s get_irradiance call so that the perez model can use it, if necessary. (GH172)

  • Fix problem in which the perez function dropped nighttime values. Nighttime values are now set to 0. (GH191)

Documentation

  • Localize datetime indices in package overview examples. (GH156)

  • Clarify that ModelChain and basic_chain currently only supports SAPM. (GH177)

  • Fix version number in 0.3.2 whatsnew file.

  • Shorten README.md file and move information to official documentation. (GH182)

  • Change authors to PVLIB Python Developers and clean up setup.py. (GH184)

  • Document the PresWth, PresWthSource, and PresWthUncertainty fields in the TMY3 data reader. (GH193)

Other

  • Removed test skip decorator functions for Linux + Python 3 and for pandas 0.18.0. (GH187)

Contributors

  • Will Holmgren

  • Mark Mikofski

  • Johannes Oos

  • Tony Lorenzo

v0.3.2 (May 3, 2016)

This is a minor release from 0.3.1. We recommend that all users upgrade to this version.

Bug fixes

  • Updates the SAM file URL. (GH152)

Contributors

  • Will Holmgren

v0.3.1 (April 19, 2016)

This is a minor release from 0.3.0. We recommend that all users upgrade to this version.

Enhancements

  • Added versioneer to keep track of version changes instead of manually updating pvlib/version.py. This will aid developers because the version string includes the specific git commit of the library code currently imported. (issue:150)

Bug fixes

  • Fixes night tare issue in snlinverter. When the DC input power (p_dc) to an inverter is below the inversion startup power (Ps0), the model should set the AC output (ac_power) to the night tare value (Pnt). The night tare power indicates the power consumed by the inverter to sense PV array voltage. The model was erroneously comparing Ps0 with the AC output power (ac_power), rather than the DC input power (p_dc). (GH140)

  • Fixed the azimuth calculation of rotated PV panel in function pvlib.tracking.singleaxis(…) so that the results are consistent with PVsyst. (GH144)

Contributors

  • ejmiller2

  • Yudong Ma

  • Tony Lorenzo

  • Will Holmgren

v0.3.0 (March 21, 2016)

This is a major release from 0.2.2. It will almost certainly break your code, but it’s worth it! We recommend that all users upgrade to this version after testing their code for compatibility and updating as necessary.

API changes

  • The location argument in solarposition.get_solarposition and clearsky.ineichen has been replaced with latitude, longitude, altitude, and tz as appropriate. This separates the object-oriented API from the procedural API. (GH17)

  • Location classes gain the get_solarposition, get_clearsky, and get_airmass functions.

  • Adds ModelChain, PVSystem, LocalizedPVSystem, SingleAxisTracker, and LocalizedSingleAxisTracker classes. (GH17)

  • Location objects can be created from TMY2/TMY3 metadata using the from_tmy constructor.

  • Change default Location timezone to 'UTC'.

  • The solar position calculators now assume UTC time if the input time is not localized. The calculators previously tried to infer the timezone from the now defunct location argument.

  • pvsystem.sapm_celltemp argument names now follow the variable conventions.

  • irradiance.total_irrad now follows the variable conventions. (GH105)

  • atmosphere.relativeairmass now raises a ValueError instead of assuming 'kastenyoung1989' if an invalid model is supplied. (GH119)

Enhancements

  • Added new sections to the documentation:

  • Adds support for Appveyor, a Windows continuous integration service. (GH111)

  • The readthedocs documentation build now uses conda packages instead of mock packages. This enables code to be run and figures to be generated during the documentation builds. (GH104)

  • Reconfigures TravisCI builds and adds e.g. has_numba decorators to the test suite. The result is that the TravisCI test suite runs almost 10x faster and users do not have to install all optional dependencies to run the test suite. (GH109)

  • Adds more unit tests that test that the return values are actually correct.

  • Add atmosphere.APPARENT_ZENITH_MODELS and atmosphere.TRUE_ZENITH_MODELS to enable code that can automatically determine which type of zenith data to use e.g. Location.get_airmass.

  • Modify sapm documentation to clarify that it does not work with the CEC database. (GH122)

  • Adds citation information to the documentation. (GH73)

  • Updates the Comparison with PVLIB MATLAB documentation. (GH116)

Bug fixes

  • Fixed the metadata key specification in documentation of the readtmy2 function.

  • Fixes the import of tkinter on Python 3 (GH112)

  • Add a decorator to skip test_calcparams_desoto on pandas 0.18.0. (GH130)

  • Fixes i_from_v documentation. (GH126)

  • Fixes two minor sphinx documentation errors: a too short heading underline in whatsnew/v0.2.2.txt and a table format in pvsystem. (GH123)

Contributors

  • Will Holmgren

  • pyElena21

  • DaCoEx

  • Uwe Krien

Will Holmgren, Jessica Forbess, bmu, Cliff Hansen, Tony Lorenzo, Uwe Krien, and bt- contributed to the object model discussion.

v0.2.2 (November 13, 2015)

This is a minor release from 0.2.1. We recommend that all users upgrade to this version.

Enhancements

  • Adds Python 3.5 compatibility (GH87)

  • Moves the Linke turbidity lookup into clearsky.lookup_linke_turbidity. The API for clearsky.ineichen remains the same. (GH95)

Bug fixes

  • irradiance.total_irrad had a typo that required the Klucher model to be accessed with 'klutcher'. Both spellings will work for the remaining 0.2.* versions of pvlib, but the misspelled method will be removed in 0.3. (GH97)

  • Fixes an import and KeyError in the IPython notebook tutorials (GH94).

  • Uses the logging module properly by replacing format calls with args. This results in a 5x speed increase for tracking.singleaxis (GH89).

  • Adds a link to the 2015 PVSC paper (GH81)

Contributors

  • Will Holmgren

  • jetheurer

  • dacoex

v0.2.1 (July 16, 2015)

This is a minor release from 0.2. It includes a large number of bug fixes for the IPython notebook tutorials. We recommend that all users upgrade to this version.

Enhancements

  • Update component info from SAM (csvs dated 2015-6-30) (GH75)

Bug fixes

  • Fix incorrect call to Perez irradiance function (GH76)

  • Fix numerous bugs in the IPython notebook tutorials (GH30)

Contributors

  • Will Holmgren

  • Jessica Forbess

v0.2.0 (July 6, 2015)

This is a major release from 0.1 and includes a large number of API changes, several new features and enhancements along with a number of bug fixes. We recommend that all users upgrade to this version.

Due to the large number of API changes, you will probably need to update your code.

API changes

  • Change variable names to conform with new Variables and style rules wiki. This impacts many function declarations and return values. Your existing code probably will not work! (GH37, GH54).

  • Move dirint and disc algorithms from clearsky.py to irradiance.py (GH42)

  • Mark some pvsystem.py methods as private (GH20)

  • Make output of pvsystem.sapm_celltemp a DataFrame (GH54)

Enhancements

  • Add conda installer

  • PEP8 fixups to solarposition.py and spa.py (GH50)

  • Add optional projection_ratio keyword argument to the haydavies calculator. Speeds calculations when irradiance changes but solar position remains the same (GH58)

  • Improved installation instructions in README.

Bug fixes

  • fix local build of the documentation (GH49, GH56)

  • The release date of 0.1 was fixed in the documentation (see v0.1.0 (April 20, 2015))

  • fix casting of DateTimeIndex to int64 epoch timestamp on machines with 32 bit python int (GH63)

  • fixed some docstrings with failing doctests (GH62)

Contributors

  • Will Holmgren

  • Rob Andrews

  • bmu

  • Tony Lorenzo

v0.1.0 (April 20, 2015)

This is the first official release of the pvlib-python project. As such, a “What’s new” document is a little hard to write. There will be significant overlap with the to-be-written document that describes the differences between pvlib-python and PVLIB_Matlab.

API changes

  • Remove pvl_ from module names.

  • Consolidation of similar modules. For example, functions from pvl_clearsky_ineichen.py and pvl_clearsky_haurwitz.py have been consolidated into clearsky.py.

  • Return one DataFrame instead of a tuple of DataFrames.

  • Change function and module names so that they do not conflict.

New features

  • Library is Python 3.3 and 3.4 compatible

  • Add What’s New section to docs (GH10)

  • Add PyEphem option to solar position calculations.

  • Add a Python translation of NREL’s SPA algorithm.

  • irradiance.py has more AOI, projection, and irradiance sum and calculation functions

  • TMY data import has a coerce_year option

  • TMY data can be loaded from a url (GH5)

  • Locations are now pvlib.location.Location objects, not “structs”.

  • Specify time zones using a string from the standard IANA Time Zone Database naming conventions or using a pytz.timezone instead of an integer GMT offset. We may add dateutils support in the future.

  • clearsky.ineichen supports interpolating monthly Linke Turbidities to daily resolution.

Other changes

  • Removed Vars=Locals(); Expect...; var=pvl\_tools.Parse(Vars,Expect); pattern. Very few tests of input validitity remain. Garbage in, garbage or nan out.

  • Removing unnecssary and sometimes undesired behavior such as setting maximum zenith=90 or airmass=0. Instead, we make extensive use of nan values.

  • Adding logging calls, removing print calls.

  • Improved PEP8 compliance.

  • Added /pvlib/data for lookup tables, test, and tutorial data.

  • Limited the scope of clearsky.py’s scipy dependency. clearsky.ineichen will work without scipy so long as the Linke Turbidity is supplied as a keyword argument. (GH13)

  • Removed NREL’s SPA code to comply with their license (GH9).

  • Revised the globalinplane function and added a test_globalinplane (GH21, GH33).

Documentation

  • Using readthedocs for documentation hosting.

  • Many typos and formatting errors corrected (GH16)

  • Documentation source code and tutorials live in / rather than /pvlib/docs.

  • Additional tutorials in /docs/tutorials.

  • Clarify pvsystem.systemdef input (GH17)

Testing

  • Tests are cleaner and more thorough. They are still nowhere near complete.

  • Using Coveralls to measure test coverage.

  • Using TravisCI for automated testing.

  • Using nosetests for more concise test code.

Bug fixes

  • Fixed DISC algorithm bugs concerning modifying input zenith Series (GH24), the Kt conditional evaluation (GH6), and ignoring the input pressure (GH25).

  • Many more bug fixes were made, but you’ll have to look at the detailed commit history.

  • Fixed inconsistent azimuth angle in the ephemeris function (GH40)

Contributors

This list includes all (I hope) contributors to pvlib/pvlib-python, Sandia-Labs/PVLIB_Python, and UARENForecasting/PVLIB_Python.

  • Rob Andrews

  • Will Holmgren

  • bmu

  • Tony Lorenzo

  • jforbess

  • Jorissup

  • dacoex

  • alexisph

  • Uwe Krien

Installation

Installing pvlib-python ranges from trivial to difficult depending on your python experience, how you want to use pvlib, and your system configuration.

Do you already have Python and the NumPy and Pandas libraries?

If the answer to this is No, follow the If you don’t have Python instructions to obtain the Anaconda Python distribution before proceeding.

Do you want to use the pvlib-python as-is, or do you want to be able to edit the source code?

If you want to use pvlib-python as-is, follow the simple Install standard release instructions.

If you want to be able to edit the source code, follow the Install as an editable library instructions.

Installing pvlib-python is similar to installing most scientific python packages, so see the References section for further help.

Please see the Compatibility section for information on the optional packages that are needed for some pvlib-python features.

If you don’t have Python

There are many ways to install Python on your system, but the Anaconda Python distribution is the easiest way for most new users to get started. Anaconda includes all of the popular libraries that you’ll need for pvlib, including Pandas, NumPy, and SciPy.

  1. Install the Anaconda Python distribution available at Anaconda.com.

See What is Anaconda? and the Anaconda Documentation for more information.

You can now install pvlib-python by one of the methods below.

Install standard release

Users may install pvlib-python using either the conda or pip package manager. We recommend that most users install pvlib-python using the conda package manager in the Anaconda Python distribution. To install the most recent stable release of pvlib-python in a non-editable way, use one of the following commands to install pvlib-python:

# get the package from the pvlib conda channel
# best option for installing pvlib in the base Anaconda distribution
conda install -c pvlib pvlib

# get the package from the conda-forge conda channel
# best option if using pvlib.forecast module
# strongly recommend installing in a separate conda env as shown below
conda create -n pvlib -c conda-forge pvlib-python; conda activate pvlib

# get the package from the Python Package Index
# best option if you know what you are doing
pip install pvlib

# get pvlib and optional dependencies from the Python Package Index
# another option if you know what you are doing
pip install pvlib[optional]

Note

By default, pvlib will not install some infrequently used dependencies. If you run into an error such as ModuleNotFoundError: No module named ‘netCDF4’ you can either install pvlib with all optional dependencies using pip install pvlib[optional], or you can install pvlib from conda-forge conda create -n pvlib -c conda-forge pvlib-python; conda activate pvlib.

If your system complains that you don’t have access privileges or asks for a password then you’re probably trying to install pvlib into your system’s Python distribution. This is usually a bad idea and you should follow the If you don’t have Python instructions before installing pvlib.

You may still want to download the Python source code so that you can easily get all of the Jupyter Notebook tutorials. Either clone the git repository or go to the Releases page to download the zip file of the most recent release. You can also use the nbviewer website to choose a tutorial to experiment with. Go to our nbviewer tutorial page.

Install as an editable library

Installing pvlib-python as an editable library involves 3 steps:

  1. Obtain the source code

  2. Set up a virtual environment

  3. Install the source code

None of these steps are particularly challenging, but they become more difficult when combined. With a little bit of practice the process will be fast and easy. Experienced users can easily execute these steps in less than a minute. You’ll get there.

Obtain the source code

We will briefly describe how to obtain the pvlib-python source code using the git/GitHub version control system. We strongly encourage users to learn how to use these powerful tools (see the References!), but we also recognize that they can be a substantial roadblock to getting started with pvlib-python. Therefore, you should know that you can download a zip file of the most recent development version of the source code by clicking on the Download Zip button on the right side of our GitHub page or download a zip file of any stable release from our Releases page.

Follow these steps to obtain the library using git/GitHub:

  1. Download the GitHub Desktop application.

  2. Fork the pvlib-python project by clicking on the “Fork” button on the upper right corner of the pvlib-python GitHub page.

  3. Clone your fork to your computer using the GitHub Desktop application by clicking on the Clone to Desktop button on your fork’s homepage. This button is circled in the image below. Remember the system path that you clone the library to.

_images/clonebutton.png

Please see GitHub’s Forking Projects, Fork A Repo, and the git-scm for more details.

Set up a virtual environment

We strongly recommend working in a virtual environment if you’re going to use an editable version of the library. You can skip this step if:

  1. You already have Anaconda or another scientific Python distribution

  2. You don’t mind polluting your Python installation with your development version of pvlib.

  3. You don’t want to work with multiple versions of pvlib.

There are many ways to use virtual environments in Python, but Anaconda again provides the easiest solution. These are often referred to as conda environments, but they’re the same for our purposes.

  1. Create a new conda environment for pvlib and pre-install the required packages into the environment: conda create --name pvlibdev python pandas scipy

  2. Activate the new conda environment: conda activate pvlibdev

  3. Install additional packages into your development environment: conda install jupyter ipython matplotlib pytest nose flake8

The conda documentation has more information on how to use conda virtual environments. You can also add -h to most pip and conda commands to get help (e.g. conda -h or conda env -h)

Install the source code

Good news – installing the source code is the easiest part! With your conda/virtual environment still active…

  1. Install pvlib-python in “development mode” by running pip install -e . from within the directory you previously cloned. Consider installing pvlib using pip install -e .[all] so that you can run the unit tests and build the documentation. Your clone directory is probably similar to C:\Users\%USER%\Documents\GitHub\pvlib-python``(Windows) or ``/Users/%USER%/Documents/pvlib-python (Mac).

  2. Test your installation by running python -c 'import pvlib'. You’re good to go if it returns without an exception.

The version of pvlib-python that is on that path is now available as an installed package inside your conda/virtual environment.

Any changes that you make to this pvlib-python will be available inside your environment. If you run a git checkout, branch, or pull command the result will be applied to your pvlib-python installation. This is great for development. Note, however, that you will need to use Python’s reload function (python 3) if you make changes to pvlib during an interactive Python session (including a Jupyter notebook). Restarting the Python interpreter will also work.

Remember to conda activate pvlibdev (or whatever you named your environment) when you start a new shell or terminal.

Compatibility

pvlib-python is compatible with Python 3.5 and above.

pvlib-python requires Pandas and Numpy. The minimum version requirements are specified in setup.py. They are typically releases from several years ago.

A handful of pvlib-python features require additional packages that must be installed separately using pip or conda. These packages/features include:

  • scipy: single diode model, clear sky detection

  • pytables (tables on PyPI): Linke turbidity look up for clear sky models

  • numba: fastest solar position calculations

  • pyephem: solar positions calculations using an astronomical library

  • siphon: forecasting PV power using the pvlib.forecast module

The Anaconda distribution includes most of the above packages.

Alternatively, users may install all optional dependencies using

pip install pvlib[optional]

NREL SPA algorithm

pvlib-python is distributed with several validated, high-precision, and high-performance solar position calculators. We strongly recommend using the built-in solar position calculators.

pvlib-python also includes unsupported wrappers for the official NREL SPA algorithm. NREL’s license does not allow redistribution of the source code, so you must jump through some hoops to use it with pvlib. You will need a C compiler to use this code.

To install the NREL SPA algorithm for use with pvlib:

  1. Download the pvlib repository (as described in Obtain the source code)

  2. Download the SPA files from NREL

  3. Copy the SPA files into pvlib-python/pvlib/spa_c_files

  4. From the pvlib-python directory, run pip uninstall pvlib followed by pip install .

References

Here are a few recommended references for installing Python packages:

Here are a few recommended references for git and GitHub:

Contributing

Encouraging more people to help develop pvlib-python is essential to our success. Therefore, we want to make it easy and rewarding for you to contribute.

There is a lot of material in this section, aimed at a variety of contributors from novice to expert. Don’t worry if you don’t (yet) understand parts of it.

Easy ways to contribute

Here are a few ideas for how you can contribute, even if you are new to pvlib-python, git, or Python:

  • Ask and answer pvlib questions on StackOverflow and participate in discussions in the pvlib-python google group.

  • Make GitHub issues and contribute to the conversations about how to resolve them.

  • Read issues and pull requests that other people created and contribute to the conversation about how to resolve them. Look for issues tagged with good first issue, easy, or help wanted.

  • Improve the documentation and the unit tests.

  • Improve the IPython/Jupyter Notebook tutorials or write new ones that demonstrate how to use pvlib-python in your area of expertise.

  • If you have MATLAB experience, you can help us keep pvlib-python up to date with PVLIB_MATLAB or help us develop common unit tests. For more, see Issue #2 and Issue #3.

  • Tell your friends and colleagues about pvlib-python.

  • Add your project to our Projects and publications that use pvlib-python wiki.

How to contribute new code

The basics

Contributors to pvlib-python use GitHub’s pull requests to add/modify its source code. The GitHub pull request process can be intimidating for new users, but you’ll find that it becomes straightforward once you use it a few times. Please let us know if you get stuck at any point in the process. Here’s an outline of the process:

  1. Create a GitHub issue and get initial feedback from users and maintainers. If the issue is a bug report, please include the code needed to reproduce the problem.

  2. Obtain the latest version of pvlib-python: Fork the pvlib-python project to your GitHub account, git clone your fork to your computer.

  3. Make some or all of your changes/additions and git commit them to your local repository.

  4. Share your changes with us via a pull request: git push your local changes to your GitHub fork, then go to GitHub make a pull request.

The Pandas project maintains an excellent contributing page that goes into detail on each of these steps. Also see GitHub’s Set Up Git and Using Pull Requests.

We strongly recommend using virtual environments for development. Virtual environments make it trivial to switch between different versions of software. This astropy guide is a good reference for virtual environments. If this is your first pull request, don’t worry about using a virtual environment.

You must include documentation and unit tests for any new or improved code. We can provide help and advice on this after you start the pull request. See the Testing section below.

Pull request scope

This section can be summed up as “less is more”.

A pull request can quickly become unmanageable if too many lines are added or changed. “Too many” is hard to define, but as a rule of thumb, we encourage contributions that contain less than 50 lines of primary code. 50 lines of primary code will typically need at least 250 lines of documentation and testing. This is about the limit of what the maintainers can review on a regular basis.

A pull request can also quickly become unmanageable if it proposes changes to the API in order to implement another feature. Consider clearly and concisely documenting all proposed API changes before implementing any code. Modifying api.rst and/or the latest whatsnew file can help formalize this process.

Questions about related issues frequently come up in the process of addressing implementing code for a pull request. Please try to avoid expanding the scope of your pull request (this also applies to reviewers!). We’d rather see small, well-documented additions to the project’s technical debt than see a pull request languish because its scope expanded beyond what the reviewer community is capable of processing.

Of course, sometimes it is necessary to make a large pull request. We only ask that you take a few minutes to consider how to break it into smaller chunks before proceeding.

pvlib-python contains 3 “layers” of code: functions, PVSystem/Location, and ModelChain. We recommend that contributors focus their work on only one or two of those layers in a single pull request. New models are not required to be available to the higher-level API!

When should I submit a pull request?

The short answer: anytime.

The long answer: it depends. If in doubt, go ahead and submit. You do not need to make all of your changes before creating a pull request. Your pull requests will automatically be updated when you commit new changes and push them to GitHub.

There are pros and cons to submitting incomplete pull-requests. On the plus side, it gives everybody an easy way to comment on the code and can make the process more efficient. On the minus side, it’s easy for an incomplete pull request to grow into a multi-month saga that leaves everyone unhappy. If you submit an incomplete pull request, please be very clear about what you would like feedback on and what we should ignore. Alternatives to incomplete pull requests include creating a gist or experimental branch and linking to it in the corresponding issue.

The best way to ensure that a pull request will be reviewed and merged in a timely manner is to:

  1. Start by creating an issue. The issue should be well-defined and actionable.

  2. Ask the maintainers to tag the issue with the appropriate milestone.

  3. Make a limited-scope pull request. It can be a lot of work to check all of the boxes in pull request guidelines, especially for pull requests with a lot of new primary code. See Pull request scope.

  4. Tag pvlib community members or @pvlib/maintainer when the pull request is ready for review. (see Pull request reviews)

Pull request reviews

The pvlib community and maintainers will review your pull request in a timely fashion. Please “ping” @pvlib/maintainer if it seems that your pull request has been forgotten at any point in the pull request process.

Keep in mind that the PV modeling community is diverse and each pvlib community member brings a different perspective when reviewing code. Some reviewers bring years of expertise in the sub-field that your code contributes to and will focus on the details of the algorithm. Other reviewers will be more focused on integrating your code with the rest of pvlib, ensuring that it is feasible to maintain, that it meets the code style guidelines, and that it is comprehensively tested. Limiting the scope of the pull request makes it much more likely that all of these reviews can be conducted and any issues can be resolved in a timely fashion.

Sometimes it’s hard for reviewers to be immediately available, so the right amount of patience is to be expected. That said, interested reviewers should do their best to not wait until the last minute to put in their two cents.

Code style

pvlib python generally follows the PEP 8 – Style Guide for Python Code. Maximum line length for code is 79 characters.

Code must be compatible with Python 3.5 and above.

pvlib python uses a mix of full and abbreviated variable names. See Variables and Symbols. We could be better about consistency. Prefer full names for new contributions. This is especially important for the API. Abbreviations can be used within a function to improve the readability of formulae.

Set your editor to strip extra whitespace from line endings. This prevents the git commit history from becoming cluttered with whitespace changes.

Please see Documentation for information specific to documentation style.

Remove any logging calls and print statements that you added during development. warning is ok.

We typically use GitHub’s “squash and merge <https://help.github.com/articles/about-pull-request-merges/#squash-and-merge-your-pull-request-commits>_” feature to merge your pull request into pvlib. GitHub will condense the commit history of your branch into a single commit when merging into pvlib-python/master (the commit history on your branch remains unchanged). Therefore, you are free to make commits that are as big or small as you’d like while developing your pull request.

Documentation

Documentation must be written in numpydoc format format which is rendered using the Sphinx Napoleon extension.

The numpydoc format includes a specification for the allowable input types. Python’s duck typing allows for multiple input types to work for many parameters. pvlib uses the following generic descriptors as short-hand to indicate which specific types may be used:

  • dict-like : dict, OrderedDict, pd.Series

  • numeric : scalar, np.array, pd.Series. Typically int or float dtype.

  • array-like : np.array, pd.Series. Typically int or float dtype.

Parameters that specify a specific type require that specific input type.

A relatively easy way to test your documentation is to build it on readthedocs.org <https://readthedocs.org> by following their Import Your Docs instructions and enabling your branch on the readthedocs versions admin page.

Another option is to install the required dependencies in your virtual/conda environment. See docs/environment.yml for the latest dependences for building the complete documentation. Some doc files can be compiled with fewer dependencies, but this is beyond the scope of this guide.

Testing

pvlib’s unit tests can easily be run by executing pytest on the pvlib directory:

pytest pvlib

or, for a single module:

pytest pvlib/test/test_clearsky.py

or, for a single test:

pytest pvlib/test/test_clearsky.py::test_ineichen_nans

We suggest using pytest’s --pdb flag to debug test failures rather than using print or logging calls. For example:

pytest pvlib --pdb

will drop you into the pdb debugger at the location of a test failure. As described in Code style, pvlib code does not use print or logging calls, and this also applies to the test suite (with rare exceptions).

New unit test code should be placed in the corresponding test module in the pvlib/test directory.

Developers must include comprehensive tests for any additions or modifications to pvlib.

pvlib-python contains 3 “layers” of code: functions, PVSystem/Location, and ModelChain. Contributors will need to add tests that correspond to the layers that they modify.

Functions

Tests of core pvlib functions should ensure that the function returns the desired output for a variety of function inputs. The tests should be independent of other pvlib functions (see GH394). The tests should ensure that all reasonable combinations of input types (floats, nans, arrays, series, scalars, etc) work as expected. Remember that your use case is likely not the only way that this function will be used, and your input data may not be generic enough to fully test the function. Write tests that cover the full range of validity of the algorithm. It is also important to write tests that assert the return value of the function or that the function throws an exception when input data is beyond the range of algorithm validity.

PVSystem/Location

The PVSystem and Location classes provide convenience wrappers around the core pvlib functions. The tests in test_pvsystem.py and test_location.py should ensure that the method calls correctly wrap the function calls. Many PVSystem/Location methods pass one or more of their object’s attributes (e.g. PVSystem.module_parameters, Location.latitude) to a function. Tests should ensure that attributes are passed correctly. These tests should also ensure that the method returns some reasonable data, though the precise values of the data should be covered by function-specific tests discussed above.

We prefer to use the pytest-mock framework to write these tests. The test below shows an example of testing the PVSystem.ashraeiam method. mocker is a pytest-mock object. mocker.spy adds features to the pvsystem.ashraeiam function that keep track of how it was called. Then a PVSystem object is created and the PVSystem.ashraeiam method is called in the usual way. The PVSystem.ashraeiam method is supposed to call the pvsystem.ashraeiam function with the angles supplied to the method call and the value of b that we defined in module_parameters. The pvsystem.ashraeiam.assert_called_once_with tests that this does, in fact, happen. Finally, we check that the output of the method call is reasonable.

def test_PVSystem_ashraeiam(mocker):
    # mocker is a pytest-mock object.
    # mocker.spy adds code to a function to keep track of how it is called
    mocker.spy(pvsystem, 'ashraeiam')

    # set up inputs
    module_parameters = {'b': 0.05}
    system = pvsystem.PVSystem(module_parameters=module_parameters)
    thetas = 1

    # call the method
    iam = system.ashraeiam(thetas)

    # did the method call the function as we expected?
    # mocker.spy added assert_called_once_with to the function
    pvsystem.ashraeiam.assert_called_once_with(thetas, b=module_parameters['b'])

    # check that the output is reasonable, but no need to duplicate
    # the rigorous tests of the function
    assert iam < 1.

Avoid writing PVSystem/Location tests that depend sensitively on the return value of a statement as a substitute for using mock. These tests are sensitive to changes in the functions, which is not what we want to test here, and are difficult to maintain.

ModelChain

The tests in test_modelchain.py should ensure that ModelChain.__init__ correctly configures the ModelChain object to eventually run the selected models. A test should ensure that the appropriate method is actually called in the course of ModelChain.run_model. A test should ensure that the model selection does have a reasonable effect on the subsequent calculations, though the precise values of the data should be covered by the function tests discussed above. pytest-mock can also be used for testing ModelChain.

The example below shows how mock can be used to assert that the correct PVSystem method is called through ModelChain.run_model.

def test_modelchain_dc_model(mocker):
    # set up location and system for model chain
    location = location.Location(32, -111)
    system = pvsystem.PVSystem(module_parameters=some_sandia_mod_params,
                               inverter_parameters=some_cecinverter_params)

    # mocker.spy adds code to the system.sapm method to keep track of how
    # it is called. use returned mock object m to make assertion later,
    # but see example above for alternative
    m = mocker.spy(system, 'sapm')

    # make and run the model chain
    mc = ModelChain(system, location,
                    aoi_model='no_loss', spectral_model='no_loss')
    times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
    mc.run_model(times)

    # assertion fails if PVSystem.sapm is not called once
    # if using returned m, prefer this over m.assert_called_once()
    # for compatibility with python < 3.6
    assert m.call_count == 1

    # ensure that dc attribute now exists and is correct type
    assert isinstance(mc.dc, (pd.Series, pd.DataFrame))

This documentation

If this documentation is unclear, help us improve it! Consider looking at the pandas documentation for inspiration.

PVSystem

The PVSystem class wraps many of the functions in the pvsystem module. This simplifies the API by eliminating the need for a user to specify arguments such as module and inverter properties when calling PVSystem methods. PVSystem is not better or worse than the functions it wraps – it is simply an alternative way of organizing your data and calculations.

This guide aims to build understanding of the PVSystem class. It assumes basic familiarity with object-oriented code in Python, but most information should be understandable without a solid understanding of classes. Keep in mind that functions are independent of objects, while methods are attached to objects.

See ModelChain for an application of PVSystem to time series modeling.

Design philosophy

The PVSystem class allows modelers to easily separate the data that represents a PV system (e.g. tilt angle or module parameters) from the data that influences the PV system (e.g. the weather).

The data that represents the PV system is intrinsic. The data that influences the PV system is extrinsic.

Intrinsic data is stored in object attributes. For example, the data that describes a PV system’s module parameters is stored in PVSystem.module_parameters.

In [1]: module_parameters = {'pdc0': 10, 'gamma_pdc': -0.004}

In [2]: system = pvsystem.PVSystem(module_parameters=module_parameters)

In [3]: print(system.module_parameters)
{'pdc0': 10, 'gamma_pdc': -0.004}

Extrinsic data is passed to a PVSystem as method arguments. For example, the pvwatts_dc() method accepts extrinsic data irradiance and temperature.

In [4]: pdc = system.pvwatts_dc(1000, 30)

In [5]: print(pdc)
9.8

Methods attached to a PVSystem object wrap corresponding functions in pvsystem. The methods simplify the argument list by using data stored in the PVSystem attributes. Compare the pvwatts_dc() method signature to the pvwatts_dc() function signature:

How does this work? The pvwatts_dc() method looks in PVSystem.module_parameters for the pdc0, and gamma_pdc arguments. Then the PVSystem.pvwatts_dc method calls the pvsystem.pvwatts_dc function with all of the arguments and returns the result to the user. Note that the function includes a default value for the parameter temp_ref. This default value may be overridden by specifying the temp_ref key in the PVSystem.module_parameters dictionary.

In [6]: system.module_parameters['temp_ref'] = 0

# lower temp_ref should lead to lower DC power than calculated above
In [7]: pdc = system.pvwatts_dc(1000, 30)

In [8]: print(pdc)
8.8

Multiple methods may pull data from the same attribute. For example, the PVSystem.module_parameters attribute is used by the DC model methods as well as the incidence angle modifier methods.

PVSystem attributes

Here we review the most commonly used PVSystem attributes. Please see the PVSystem class documentation for a comprehensive list.

The first PVSystem parameters are surface_tilt and surface_azimuth. These parameters are used in PVSystem methods such as get_aoi() and get_irradiance(). Angle of incidence (AOI) calculations require surface_tilt, surface_azimuth and also the sun position. The get_aoi() method uses the surface_tilt and surface_azimuth attributes in its PVSystem object, and so requires only solar_zenith and solar_azimuth as arguments.

# 20 deg tilt, south-facing
In [9]: system = pvsystem.PVSystem(surface_tilt=20, surface_azimuth=180)

In [10]: print(system.surface_tilt, system.surface_azimuth)
20 180

# call get_aoi with solar_zenith, solar_azimuth
In [11]: aoi = system.get_aoi(30, 180)

In [12]: print(aoi)
9.999999999999975

module_parameters and inverter_parameters contain the data necessary for computing DC and AC power using one of the available PVSystem methods. These are typically specified using data from the retrieve_sam() function:

# retrieve_sam returns a dict. the dict keys are module names,
# and the values are model parameters for that module
In [13]: modules = pvsystem.retrieve_sam('cecmod')

In [14]: module_parameters = modules['Example_Module']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2896             try:
-> 2897                 return self._engine.get_loc(key)
   2898             except KeyError:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'Example_Module'

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
<ipython-input-14-412284c56432> in <module>
----> 1 module_parameters = modules['Example_Module']

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/frame.py in __getitem__(self, key)
   2978             if self.columns.nlevels > 1:
   2979                 return self._getitem_multilevel(key)
-> 2980             indexer = self.columns.get_loc(key)
   2981             if is_integer(indexer):
   2982                 indexer = [indexer]

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2897                 return self._engine.get_loc(key)
   2898             except KeyError:
-> 2899                 return self._engine.get_loc(self._maybe_cast_indexer(key))
   2900         indexer = self.get_indexer([key], method=method, tolerance=tolerance)
   2901         if indexer.ndim > 1 or indexer.size > 1:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'Example_Module'

In [15]: inverters = pvsystem.retrieve_sam('cecinverter')

In [16]: inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2896             try:
-> 2897                 return self._engine.get_loc(key)
   2898             except KeyError:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
<ipython-input-16-3357ac02d500> in <module>
----> 1 inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_']

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/frame.py in __getitem__(self, key)
   2978             if self.columns.nlevels > 1:
   2979                 return self._getitem_multilevel(key)
-> 2980             indexer = self.columns.get_loc(key)
   2981             if is_integer(indexer):
   2982                 indexer = [indexer]

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2897                 return self._engine.get_loc(key)
   2898             except KeyError:
-> 2899                 return self._engine.get_loc(self._maybe_cast_indexer(key))
   2900         indexer = self.get_indexer([key], method=method, tolerance=tolerance)
   2901         if indexer.ndim > 1 or indexer.size > 1:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'

In [17]: system = pvsystem.PVSystem(module_parameters=module_parameters, inverter_parameters=inverter_parameters)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-17-e25aeba9b3e9> in <module>
----> 1 system = pvsystem.PVSystem(module_parameters=module_parameters, inverter_parameters=inverter_parameters)

NameError: name 'inverter_parameters' is not defined

The module and/or inverter parameters can also be specified manually. This is useful for specifying modules and inverters that are not included in the supplied databases. It is also useful for specifying systems for use with the PVWatts models, as demonstrated in Design philosophy.

The losses_parameters attribute contains data that may be used with methods that calculate system losses. At present, these methods include only PVSystem.pvwatts_losses and pvsystem.pvwatts_losses, but we hope to add more related functions and methods in the future.

The attributes modules_per_string and strings_per_inverter are used in the scale_voltage_current_power() method. Some DC power models in ModelChain automatically call this method and make use of these attributes. As an example, consider a system with 35 modules arranged into 5 strings of 7 modules each.

In [18]: system = pvsystem.PVSystem(modules_per_string=7, strings_per_inverter=5)

# crude numbers from a single module
In [19]: data = pd.DataFrame({'v_mp': 8, 'v_oc': 10, 'i_mp': 5, 'i_x': 6,
   ....:                      'i_xx': 4, 'i_sc': 7, 'p_mp': 40}, index=[0])
   ....: 

In [20]: data_scaled = system.scale_voltage_current_power(data)

In [21]: print(data_scaled)
   v_mp  v_oc  i_mp  i_x  i_xx  i_sc  p_mp
0    56    70    25   30    20    35  1400

SingleAxisTracker

The SingleAxisTracker is a subclass of PVSystem. The SingleAxisTracker class includes a few more keyword arguments and attributes that are specific to trackers, plus the singleaxis() method. It also overrides the get_aoi and get_irradiance methods.

ModelChain

The ModelChain class provides a high-level interface for standardized PV modeling. The class aims to automate much of the modeling process while providing user-control and remaining extensible. This guide aims to build users’ understanding of the ModelChain class. It assumes some familiarity with object-oriented code in Python, but most information should be understandable even without a solid understanding of classes.

A ModelChain is composed of a PVSystem object and a Location object. A PVSystem object represents an assembled collection of modules, inverters, etc., a Location object represents a particular place on the planet, and a ModelChain object describes the modeling chain used to calculate a system’s output at that location. The PVSystem and Location objects will be described in detail in another guide.

Modeling with a ModelChain typically involves 3 steps:

  1. Creating the ModelChain.

  2. Executing the ModelChain.run_model() method with prepared weather data.

  3. Examining the model results that run_model() stored in attributes of the ModelChain.

A simple ModelChain example

Before delving into the intricacies of ModelChain, we provide a brief example of the modeling steps using ModelChain. First, we import pvlib’s objects, module data, and inverter data.

In [1]: import pandas as pd

In [2]: import numpy as np

# pvlib imports
In [3]: import pvlib

In [4]: from pvlib.pvsystem import PVSystem

In [5]: from pvlib.location import Location

In [6]: from pvlib.modelchain import ModelChain

In [7]: from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS

In [8]: temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']

# load some module and inverter specifications
In [9]: sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod')

In [10]: cec_inverters = pvlib.pvsystem.retrieve_sam('cecinverter')

In [11]: sandia_module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']

In [12]: cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2896             try:
-> 2897                 return self._engine.get_loc(key)
   2898             except KeyError:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
<ipython-input-12-7e123d4a37af> in <module>
----> 1 cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_']

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/frame.py in __getitem__(self, key)
   2978             if self.columns.nlevels > 1:
   2979                 return self._getitem_multilevel(key)
-> 2980             indexer = self.columns.get_loc(key)
   2981             if is_integer(indexer):
   2982                 indexer = [indexer]

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2897                 return self._engine.get_loc(key)
   2898             except KeyError:
-> 2899                 return self._engine.get_loc(self._maybe_cast_indexer(key))
   2900         indexer = self.get_indexer([key], method=method, tolerance=tolerance)
   2901         if indexer.ndim > 1 or indexer.size > 1:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'

Now we create a Location object, a PVSystem object, and a ModelChain object.

In [13]: location = Location(latitude=32.2, longitude=-110.9)

In [14]: system = PVSystem(surface_tilt=20, surface_azimuth=200,
   ....:                   module_parameters=sandia_module,
   ....:                   inverter_parameters=cec_inverter,
   ....:                   temperature_model_parameters=temperature_model_parameters)
   ....: 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-14-9ac9b596dc7c> in <module>
      1 system = PVSystem(surface_tilt=20, surface_azimuth=200,
      2                   module_parameters=sandia_module,
----> 3                   inverter_parameters=cec_inverter,
      4                   temperature_model_parameters=temperature_model_parameters)

NameError: name 'cec_inverter' is not defined

In [15]: mc = ModelChain(system, location)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-15-c237d7250396> in <module>
----> 1 mc = ModelChain(system, location)

NameError: name 'system' is not defined

Printing a ModelChain object will display its models.

In [16]: print(mc)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-16-4b7954403baa> in <module>
----> 1 print(mc)

NameError: name 'mc' is not defined

Next, we run a model with some simple weather data.

In [17]: weather = pd.DataFrame([[1050, 1000, 100, 30, 5]],
   ....:                        columns=['ghi', 'dni', 'dhi', 'temp_air', 'wind_speed'],
   ....:                        index=[pd.Timestamp('20170401 1200', tz='US/Arizona')])
   ....: 

In [18]: mc.run_model(times=weather.index, weather=weather);

ModelChain stores the modeling results on a series of attributes. A few examples are shown below.

In [19]: mc.aoi
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-19-9af3190081df> in <module>
----> 1 mc.aoi

NameError: name 'mc' is not defined
In [20]: mc.cell_temperature
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-20-ab0b6163d9b2> in <module>
----> 1 mc.cell_temperature

NameError: name 'mc' is not defined
In [21]: mc.dc
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-21-b9022cfccb62> in <module>
----> 1 mc.dc

NameError: name 'mc' is not defined
In [22]: mc.ac
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-22-db2d87a3a7b7> in <module>
----> 1 mc.ac

NameError: name 'mc' is not defined

The remainder of this guide examines the ModelChain functionality and explores common pitfalls.

Defining a ModelChain

A ModelChain object is defined by:

  1. The properties of its PVSystem and Location objects

  2. The keyword arguments passed to it at construction

ModelChain uses the keyword arguments passed to it to determine the models for the simulation. The documentation describes the allowed values for each keyword argument. If a keyword argument is not supplied, ModelChain will attempt to infer the correct set of models by inspecting the Location and PVSystem attributes.

Below, we show some examples of how to define a ModelChain.

Let’s make the most basic Location and PVSystem objects and build from there.

In [23]: location = Location(32.2, -110.9)

In [24]: poorly_specified_system = PVSystem()

In [25]: print(location)
Location: 
  name: None
  latitude: 32.2
  longitude: -110.9
  altitude: 0
  tz: UTC

In [26]: print(poorly_specified_system)
PVSystem: 
  name: None
  surface_tilt: 0
  surface_azimuth: 180
  module: None
  inverter: None
  albedo: 0.25
  racking_model: open_rack

These basic objects do not have enough information for ModelChain to be able to automatically determine its set of models, so the ModelChain will throw an error when we try to create it.

In [27]: ModelChain(poorly_specified_system, location)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-27-4e9151e6ff63> in <module>
----> 1 ModelChain(poorly_specified_system, location)

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/checkouts/stable/pvlib/modelchain.py in __init__(self, system, location, orientation_strategy, clearsky_model, transposition_model, solar_position_method, airmass_model, dc_model, ac_model, aoi_model, spectral_model, temperature_model, losses_model, name, **kwargs)
    317 
    318         # calls setters
--> 319         self.dc_model = dc_model
    320         self.ac_model = ac_model
    321         self.aoi_model = aoi_model

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/checkouts/stable/pvlib/modelchain.py in dc_model(self, model)
    389         # guess at model if None
    390         if model is None:
--> 391             self._dc_model, model = self.infer_dc_model()
    392 
    393         # Set model and validate parameters

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/checkouts/stable/pvlib/modelchain.py in infer_dc_model(self)
    433             return self.pvwatts_dc, 'pvwatts'
    434         else:
--> 435             raise ValueError('could not infer DC model from '
    436                              'system.module_parameters. Check '
    437                              'system.module_parameters or explicitly '

ValueError: could not infer DC model from system.module_parameters. Check system.module_parameters or explicitly set the model with the dc_model kwarg.

Next, we define a PVSystem with a module from the SAPM database and an inverter from the CEC database. ModelChain will examine the PVSystem object’s properties and determine that it should choose the SAPM DC model, AC model, AOI loss model, and spectral loss model.

In [28]: sapm_system = PVSystem(
   ....:     module_parameters=sandia_module,
   ....:     inverter_parameters=cec_inverter,
   ....:     temperature_model_parameters=temperature_model_parameters)
   ....: 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-28-13767fce5736> in <module>
      1 sapm_system = PVSystem(
      2     module_parameters=sandia_module,
----> 3     inverter_parameters=cec_inverter,
      4     temperature_model_parameters=temperature_model_parameters)

NameError: name 'cec_inverter' is not defined

In [29]: mc = ModelChain(sapm_system, location)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-29-9886b976fbf1> in <module>
----> 1 mc = ModelChain(sapm_system, location)

NameError: name 'sapm_system' is not defined

In [30]: print(mc)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-30-4b7954403baa> in <module>
----> 1 print(mc)

NameError: name 'mc' is not defined
In [31]: mc.run_model(times=weather.index, weather=weather);

In [32]: mc.ac
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-32-db2d87a3a7b7> in <module>
----> 1 mc.ac

NameError: name 'mc' is not defined

Alternatively, we could have specified single diode or PVWatts related information in the PVSystem construction. Here we pass PVWatts data to the PVSystem. ModelChain will automatically determine that it should choose PVWatts DC and AC models. ModelChain still needs us to specify aoi_model and spectral_model keyword arguments because the system.module_parameters dictionary does not contain enough information to determine which of those models to choose.

In [33]: pvwatts_system = PVSystem(
   ....:     module_parameters={'pdc0': 240, 'gamma_pdc': -0.004},
   ....:     inverter_parameters={'pdc0': 240},
   ....:     temperature_model_parameters=temperature_model_parameters)
   ....: 

In [34]: mc = ModelChain(pvwatts_system, location,
   ....:                 aoi_model='physical', spectral_model='no_loss')
   ....: 

In [35]: print(mc)
ModelChain: 
  name: None
  orientation_strategy: None
  clearsky_model: ineichen
  transposition_model: haydavies
  solar_position_method: nrel_numpy
  airmass_model: kastenyoung1989
  dc_model: pvwatts_dc
  ac_model: pvwatts_inverter
  aoi_model: physical_aoi_loss
  spectral_model: no_spectral_loss
  temperature_model: sapm_temp
  losses_model: no_extra_losses
In [36]: mc.run_model(times=weather.index, weather=weather);

In [37]: mc.ac
Out[37]: 
2017-04-01 12:00:00-07:00    198.519999
dtype: float64

User-supplied keyword arguments override ModelChain’s inspection methods. For example, we can tell ModelChain to use different loss functions for a PVSystem that contains SAPM-specific parameters.

In [38]: sapm_system = PVSystem(
   ....:     module_parameters=sandia_module,
   ....:     inverter_parameters=cec_inverter,
   ....:     temperature_model_parameters=temperature_model_parameters)
   ....: 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-38-13767fce5736> in <module>
      1 sapm_system = PVSystem(
      2     module_parameters=sandia_module,
----> 3     inverter_parameters=cec_inverter,
      4     temperature_model_parameters=temperature_model_parameters)

NameError: name 'cec_inverter' is not defined

In [39]: mc = ModelChain(sapm_system, location, aoi_model='physical', spectral_model='no_loss')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-39-1fd631bb9eb3> in <module>
----> 1 mc = ModelChain(sapm_system, location, aoi_model='physical', spectral_model='no_loss')

NameError: name 'sapm_system' is not defined

In [40]: print(mc)
ModelChain: 
  name: None
  orientation_strategy: None
  clearsky_model: ineichen
  transposition_model: haydavies
  solar_position_method: nrel_numpy
  airmass_model: kastenyoung1989
  dc_model: pvwatts_dc
  ac_model: pvwatts_inverter
  aoi_model: physical_aoi_loss
  spectral_model: no_spectral_loss
  temperature_model: sapm_temp
  losses_model: no_extra_losses
In [41]: mc.run_model(times=weather.index, weather=weather);

In [42]: mc.ac
Out[42]: 
2017-04-01 12:00:00-07:00    198.519999
dtype: float64

Of course, these choices can also lead to failure when executing run_model() if your system objects do not contain the required parameters for running the model.

Demystifying ModelChain internals

The ModelChain class has a lot going in inside it in order to make users’ code as simple as possible.

The key parts of ModelChain are:

  1. The ModelChain.run_model() method

  2. A set of methods that wrap and call the PVSystem methods.

  3. A set of methods that inspect user-supplied objects to determine the appropriate default models.

run_model

Most users will only interact with the run_model() method. The run_model() method, shown below, calls a series of methods to complete the modeling steps. The first method, prepare_inputs(), computes parameters such as solar position, airmass, angle of incidence, and plane of array irradiance. The prepare_inputs() method also assigns default values for temperature (20 C) and wind speed (0 m/s) if these inputs are not provided. prepare_inputs() requires all irradiance components (GHI, DNI, and DHI). See complete_irradiance() and DNI estimation models for methods and functions that can help fully define the irradiance inputs.

Next, run_model() calls the wrapper methods for AOI loss, spectral loss, effective irradiance, cell temperature, DC power, AC power, and other losses. These methods are assigned to standard names, as described in the next section.

The methods called by run_model() store their results in a series of ModelChain attributes: times, solar_position, airmass, irradiance, total_irrad, effective_irradiance, weather, temps, aoi, aoi_modifier, spectral_modifier, dc, ac, losses.

In [43]: mc.run_model??
Signature: mc.run_model(weather, times=None)
Source:   
    def run_model(self, weather, times=None):
        """
        Run the model.

        Parameters
        ----------
        weather : DataFrame
            Column names must be ``'dni'``, ``'ghi'``, ``'dhi'``,
            ``'wind_speed'``, ``'temp_air'``. All irradiance components
            are required. Air temperature of 20 C and wind speed
            of 0 m/s will be added to the DataFrame if not provided.
        times : None, deprecated
            Deprecated argument included for API compatibility, but not
            used internally. The index of the weather DataFrame is used
            for times.

        Returns
        -------
        self

        Assigns attributes: solar_position, airmass, irradiance,
        total_irrad, effective_irradiance, weather, cell_temperature, aoi,
        aoi_modifier, spectral_modifier, dc, ac, losses.
        """
        if times is not None:
            warnings.warn('times keyword argument is deprecated and will be '
                          'removed in 0.8. The index of the weather DataFrame '
                          'is used for times.', pvlibDeprecationWarning)

        self.prepare_inputs(weather)
        self.aoi_model()
        self.spectral_model()
        self.effective_irradiance_model()
        self.temperature_model()
        self.dc_model()
        self.losses_model()
        self.ac_model()

        return self
File:      ~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      method

Finally, the complete_irradiance() method is available for calculating the full set of GHI, DNI, or DHI if only two of these three series are provided. The completed dataset can then be passed to run_model().

Wrapping methods into a unified API

Readers may notice that the source code of the ModelChain.run_model method is model-agnostic. ModelChain.run_model calls generic methods such as self.dc_model rather than a specific model such as singlediode. So how does the ModelChain.run_model know what models it’s supposed to run? The answer comes in two parts, and allows us to explore more of the ModelChain API along the way.

First, ModelChain has a set of methods that wrap the PVSystem methods that perform the calculations (or further wrap the pvsystem.py module’s functions). Each of these methods takes the same arguments (self) and sets the same attributes, thus creating a uniform API. For example, the ModelChain.pvwatts_dc method is shown below. Its only argument is self, and it sets the dc attribute.

In [44]: mc.pvwatts_dc??
Signature: mc.pvwatts_dc()
Docstring: <no docstring>
Source:   
    def pvwatts_dc(self):
        self.dc = self.system.pvwatts_dc(self.effective_irradiance,
                                         self.cell_temperature)
        return self
File:      ~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      method

The ModelChain.pvwatts_dc method calls the pvwatts_dc method of the PVSystem object that we supplied using data that is stored in its own effective_irradiance and cell_temperature attributes. Then it assigns the result to the dc attribute of the ModelChain object. The code below shows a simple example of this.

# make the objects
In [45]: pvwatts_system = PVSystem(
   ....:     module_parameters={'pdc0': 240, 'gamma_pdc': -0.004},
   ....:     inverter_parameters={'pdc0': 240},
   ....:     temperature_model_parameters=temperature_model_parameters)
   ....: 

In [46]: mc = ModelChain(pvwatts_system, location,
   ....:                 aoi_model='no_loss', spectral_model='no_loss')
   ....: 

# manually assign data to the attributes that ModelChain.pvwatts_dc will need.
# for standard workflows, run_model would assign these attributes.
In [47]: mc.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')])

In [48]: mc.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')])

# run ModelChain.pvwatts_dc and look at the result
In [49]: mc.pvwatts_dc();

In [50]: mc.dc
Out[50]: 
2017-04-01 12:00:00-07:00    216.0
dtype: float64

The ModelChain.sapm method works similarly to the ModelChain.pvwatts_dc method. It calls the PVSystem.sapm method using stored data, then assigns the result to the dc attribute. The ModelChain.sapm method differs from the ModelChain.pvwatts_dc method in three notable ways. First, the PVSystem.sapm method expects different units for effective irradiance, so ModelChain handles the conversion for us. Second, the PVSystem.sapm method (and the PVSystem.singlediode method) returns a DataFrame with current, voltage, and power parameters rather than a simple Series of power. Finally, this current and voltage information allows the SAPM and single diode model paths to support the concept of modules in series and parallel, which is handled by the PVSystem.scale_voltage_current_power method.

In [51]: mc.sapm??
Signature: mc.sapm()
Docstring: <no docstring>
Source:   
    def sapm(self):
        self.dc = self.system.sapm(self.effective_irradiance/1000.,
                                   self.cell_temperature)

        self.dc = self.system.scale_voltage_current_power(self.dc)

        return self
File:      ~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      method
# make the objects
In [52]: sapm_system = PVSystem(
   ....:     module_parameters=sandia_module,
   ....:     inverter_parameters=cec_inverter,
   ....:     temperature_model_parameters=temperature_model_parameters)
   ....: 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-52-13767fce5736> in <module>
      1 sapm_system = PVSystem(
      2     module_parameters=sandia_module,
----> 3     inverter_parameters=cec_inverter,
      4     temperature_model_parameters=temperature_model_parameters)

NameError: name 'cec_inverter' is not defined

In [53]: mc = ModelChain(sapm_system, location)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-53-9886b976fbf1> in <module>
----> 1 mc = ModelChain(sapm_system, location)

NameError: name 'sapm_system' is not defined

# manually assign data to the attributes that ModelChain.sapm will need.
# for standard workflows, run_model would assign these attributes.
In [54]: mc.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')])

In [55]: mc.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')])

# run ModelChain.sapm and look at the result
In [56]: mc.sapm();

In [57]: mc.dc
Out[57]: 
2017-04-01 12:00:00-07:00    216.0
dtype: float64

We’ve established that the ModelChain.pvwatts_dc and ModelChain.sapm have the same API: they take the same arugments (self) and they both set the dc attribute.* Because the methods have the same API, we can call them in the same way. ModelChain includes a large number of methods that perform the same API-unification roles for each modeling step.

Again, so how does the ModelChain.run_model know which models it’s supposed to run?

At object construction, ModelChain assigns the desired model’s method (e.g. ModelChain.pvwatts_dc) to the corresponding generic attribute (e.g. ModelChain.dc_model) using a method described in the next section.

In [58]: pvwatts_system = PVSystem(
   ....:     module_parameters={'pdc0': 240, 'gamma_pdc': -0.004},
   ....:     inverter_parameters={'pdc0': 240},
   ....:     temperature_model_parameters=temperature_model_parameters)
   ....: 

In [59]: mc = ModelChain(pvwatts_system, location,
   ....:                 aoi_model='no_loss', spectral_model='no_loss')
   ....: 

In [60]: mc.dc_model.__func__
Out[60]: <function pvlib.modelchain.ModelChain.pvwatts_dc(self)>

The ModelChain.run_model method can ignorantly call self.dc_module because the API is the same for all methods that may be assigned to this attribute.

* some readers may object that the API is not actually the same because the type of the dc attribute is different (Series vs. DataFrame)!

Inferring models

How does ModelChain infer the appropriate model types? ModelChain uses a series of methods (ModelChain.infer_dc_model, ModelChain.infer_ac_model, etc.) that examine the user-supplied PVSystem object. The inference methods use set logic to assign one of the model-specific methods, such as ModelChain.sapm or ModelChain.snlinverter, to the universal method names ModelChain.dc_model and ModelChain.ac_model. A few examples are shown below.

In [61]: mc.infer_dc_model??
Signature: mc.infer_dc_model()
Docstring: <no docstring>
Source:   
    def infer_dc_model(self):
        params = set(self.system.module_parameters.keys())
        if set(['A0', 'A1', 'C7']) <= params:
            return self.sapm, 'sapm'
        elif set(['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref',
                  'R_s', 'Adjust']) <= params:
            return self.cec, 'cec'
        elif set(['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref',
                  'R_s']) <= params:
            return self.desoto, 'desoto'
        elif set(['gamma_ref', 'mu_gamma', 'I_L_ref', 'I_o_ref',
                  'R_sh_ref', 'R_sh_0', 'R_sh_exp', 'R_s']) <= params:
            return self.pvsyst, 'pvsyst'
        elif set(['pdc0', 'gamma_pdc']) <= params:
            return self.pvwatts_dc, 'pvwatts'
        else:
            raise ValueError('could not infer DC model from '
                             'system.module_parameters. Check '
                             'system.module_parameters or explicitly '
                             'set the model with the dc_model kwarg.')
File:      ~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      method
In [62]: mc.infer_ac_model??
Signature: mc.infer_ac_model()
Docstring: <no docstring>
Source:   
    def infer_ac_model(self):
        inverter_params = set(self.system.inverter_parameters.keys())
        if set(['C0', 'C1', 'C2']) <= inverter_params:
            return self.snlinverter
        elif set(['ADRCoefficients']) <= inverter_params:
            return self.adrinverter
        elif set(['pdc0']) <= inverter_params:
            return self.pvwatts_inverter
        else:
            raise ValueError('could not infer AC model from '
                             'system.inverter_parameters. Check '
                             'system.inverter_parameters or explicitly '
                             'set the model with the ac_model kwarg.')
File:      ~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      method

User-defined models

Users may also write their own functions and pass them as arguments to ModelChain. The first argument of the function must be a ModelChain instance. For example, the functions below implement the PVUSA model and a wrapper function appropriate for use with ModelChain. This follows the pattern of implementing the core models using the simplest possible functions, and then implementing wrappers to make them easier to use in specific applications. Of course, you could implement it in a single function if you wanted to.

In [63]: def pvusa(poa_global, wind_speed, temp_air, a, b, c, d):
   ....:     """
   ....:     Calculates system power according to the PVUSA equation
   ....:     P = I * (a + b*I + c*W + d*T)
   ....:     where
   ....:     P is the output power,
   ....:     I is the plane of array irradiance,
   ....:     W is the wind speed, and
   ....:     T is the temperature
   ....:     a, b, c, d are empirically derived parameters.
   ....:     """
   ....:     return poa_global * (a + b*poa_global + c*wind_speed + d*temp_air)
   ....: 

In [64]: def pvusa_mc_wrapper(mc):
   ....:     mc.dc = pvusa(mc.total_irrad['poa_global'], mc.weather['wind_speed'], mc.weather['temp_air'],
   ....:                   mc.system.module_parameters['a'], mc.system.module_parameters['b'],
   ....:                   mc.system.module_parameters['c'], mc.system.module_parameters['d'])
   ....: 

    # returning mc is optional, but enables method chaining
In [65]: def pvusa_ac_mc(mc):
   ....:     mc.ac = mc.dc
   ....:     return mc
   ....: 

In [66]: def no_loss_temperature(mc):
   ....:     mc.cell_temperature = mc.weather['temp_air']
   ....:     return mc
   ....: 
In [67]: module_parameters = {'a': 0.2, 'b': 0.00001, 'c': 0.001, 'd': -0.00005}

In [68]: pvusa_system = PVSystem(module_parameters=module_parameters)

In [69]: mc = ModelChain(pvusa_system, location,
   ....:                 dc_model=pvusa_mc_wrapper, ac_model=pvusa_ac_mc,
   ....:                 temperature_model=no_loss_temperature,
   ....:                 aoi_model='no_loss', spectral_model='no_loss')
   ....: 

A ModelChain object uses Python’s functools.partial function to assign itself as the argument to the user-supplied functions.

In [70]: mc.dc_model.func
Out[70]: <function __main__.pvusa_mc_wrapper(mc)>

The end result is that ModelChain.run_model works as expected!

In [71]: mc.run_model(times=weather.index, weather=weather);

In [72]: mc.dc
Out[72]: 
2017-04-01 12:00:00-07:00    209.519752
dtype: float64

Time and time zones

Dealing with time and time zones can be a frustrating experience in any programming language and for any application. pvlib-python relies on pandas and pytz to handle time and time zones. Therefore, the vast majority of the information in this document applies to any time series analysis using pandas and is not specific to pvlib-python.

General functionality

pvlib makes extensive use of pandas due to its excellent time series functionality. Take the time to become familiar with pandas’ Time Series / Date functionality page. It is also worthwhile to become familiar with pure Python’s datetime module, although we usually recommend using the corresponding pandas functionality where possible.

First, we’ll import the libraries that we’ll use to explore the basic time and time zone functionality in python and pvlib.

In [1]: import datetime

In [2]: import pandas as pd

In [3]: import pytz

Finding a time zone

pytz is based on the Olson time zone database. You can obtain a list of all valid time zone strings with pytz.all_timezones. It’s a long list, so we only print every 20th time zone.

In [4]: len(pytz.all_timezones)
Out[4]: 592

In [5]: pytz.all_timezones[::20]
Out[5]: 
['Africa/Abidjan',
 'Africa/Douala',
 'Africa/Mbabane',
 'America/Argentina/Catamarca',
 'America/Belize',
 'America/Curacao',
 'America/Guatemala',
 'America/Kentucky/Louisville',
 'America/Mexico_City',
 'America/Port-au-Prince',
 'America/Sitka',
 'Antarctica/Casey',
 'Asia/Ashkhabad',
 'Asia/Dubai',
 'Asia/Khandyga',
 'Asia/Qatar',
 'Asia/Tomsk',
 'Atlantic/Reykjavik',
 'Australia/Queensland',
 'Canada/Yukon',
 'Etc/GMT+7',
 'Etc/UCT',
 'Europe/Guernsey',
 'Europe/Paris',
 'Europe/Vienna',
 'Indian/Cocos',
 'NZ',
 'Pacific/Honolulu',
 'Pacific/Samoa',
 'US/Eastern']

Wikipedia’s List of tz database time zones is also good reference.

The pytz.country_timezones function is useful, too.

In [6]: pytz.country_timezones('US')
Out[6]: 
['America/New_York',
 'America/Detroit',
 'America/Kentucky/Louisville',
 'America/Kentucky/Monticello',
 'America/Indiana/Indianapolis',
 'America/Indiana/Vincennes',
 'America/Indiana/Winamac',
 'America/Indiana/Marengo',
 'America/Indiana/Petersburg',
 'America/Indiana/Vevay',
 'America/Chicago',
 'America/Indiana/Tell_City',
 'America/Indiana/Knox',
 'America/Menominee',
 'America/North_Dakota/Center',
 'America/North_Dakota/New_Salem',
 'America/North_Dakota/Beulah',
 'America/Denver',
 'America/Boise',
 'America/Phoenix',
 'America/Los_Angeles',
 'America/Anchorage',
 'America/Juneau',
 'America/Sitka',
 'America/Metlakatla',
 'America/Yakutat',
 'America/Nome',
 'America/Adak',
 'Pacific/Honolulu']

And don’t forget about Python’s filter() function.

In [7]: list(filter(lambda x: 'GMT' in x, pytz.all_timezones))
Out[7]: 
['Etc/GMT',
 'Etc/GMT+0',
 'Etc/GMT+1',
 'Etc/GMT+10',
 'Etc/GMT+11',
 'Etc/GMT+12',
 'Etc/GMT+2',
 'Etc/GMT+3',
 'Etc/GMT+4',
 'Etc/GMT+5',
 'Etc/GMT+6',
 'Etc/GMT+7',
 'Etc/GMT+8',
 'Etc/GMT+9',
 'Etc/GMT-0',
 'Etc/GMT-1',
 'Etc/GMT-10',
 'Etc/GMT-11',
 'Etc/GMT-12',
 'Etc/GMT-13',
 'Etc/GMT-14',
 'Etc/GMT-2',
 'Etc/GMT-3',
 'Etc/GMT-4',
 'Etc/GMT-5',
 'Etc/GMT-6',
 'Etc/GMT-7',
 'Etc/GMT-8',
 'Etc/GMT-9',
 'Etc/GMT0',
 'GMT',
 'GMT+0',
 'GMT-0',
 'GMT0']

Note that while pytz has 'EST' and 'MST', it does not have 'PST'. Use 'Etc/GMT+8' instead, or see Fixed offsets.

Timestamps

pandas.Timestamp and pandas.DatetimeIndex can be created in many ways. Here we focus on the time zone issues surrounding them; see the pandas documentation for more information.

First, create a time zone naive pandas.Timestamp.

In [8]: pd.Timestamp('2015-1-1 00:00')
Out[8]: Timestamp('2015-01-01 00:00:00')

You can specify the time zone using the tz keyword argument or the tz_localize method of Timestamp and DatetimeIndex objects.

In [9]: pd.Timestamp('2015-1-1 00:00', tz='America/Denver')
Out[9]: Timestamp('2015-01-01 00:00:00-0700', tz='America/Denver')

In [10]: pd.Timestamp('2015-1-1 00:00').tz_localize('America/Denver')
Out[10]: Timestamp('2015-01-01 00:00:00-0700', tz='America/Denver')

Localized Timestamps can be converted from one time zone to another.

In [11]: midnight_mst = pd.Timestamp('2015-1-1 00:00', tz='America/Denver')

In [12]: corresponding_utc = midnight_mst.tz_convert('UTC')  # returns a new Timestamp

In [13]: corresponding_utc
Out[13]: Timestamp('2015-01-01 07:00:00+0000', tz='UTC')

It does not make sense to convert a time stamp that has not been localized, and pandas will raise an exception if you try to do so.

In [14]: midnight = pd.Timestamp('2015-1-1 00:00')

In [15]: midnight.tz_convert('UTC')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-f99dcf3885a1> in <module>
----> 1 midnight.tz_convert('UTC')

pandas/_libs/tslibs/timestamps.pyx in pandas._libs.tslibs.timestamps.Timestamp.tz_convert()

TypeError: Cannot convert tz-naive Timestamp, use tz_localize to localize

The difference between tz_localize and tz_convert is a common source of confusion for new users. Just remember: localize first, convert later.

Daylight savings time

Some time zones are aware of daylight savings time and some are not. For example the winter time results are the same for US/Mountain and MST, but the summer time results are not.

Note the UTC offset in winter…

In [16]: pd.Timestamp('2015-1-1 00:00').tz_localize('US/Mountain')
Out[16]: Timestamp('2015-01-01 00:00:00-0700', tz='US/Mountain')

In [17]: pd.Timestamp('2015-1-1 00:00').tz_localize('Etc/GMT+7')
Out[17]: Timestamp('2015-01-01 00:00:00-0700', tz='Etc/GMT+7')

vs. the UTC offset in summer…

In [18]: pd.Timestamp('2015-6-1 00:00').tz_localize('US/Mountain')
Out[18]: Timestamp('2015-06-01 00:00:00-0600', tz='US/Mountain')

In [19]: pd.Timestamp('2015-6-1 00:00').tz_localize('Etc/GMT+7')
Out[19]: Timestamp('2015-06-01 00:00:00-0700', tz='Etc/GMT+7')

pandas and pytz make this time zone handling possible because pandas stores all times as integer nanoseconds since January 1, 1970. Here is the pandas time representation of the integers 1 and 1e9.

In [20]: pd.Timestamp(1)
Out[20]: Timestamp('1970-01-01 00:00:00.000000001')

In [21]: pd.Timestamp(1e9)
Out[21]: Timestamp('1970-01-01 00:00:01')

So if we specify times consistent with the specified time zone, pandas will use the same integer to represent them.

# US/Mountain
In [22]: pd.Timestamp('2015-6-1 01:00', tz='US/Mountain').value
Out[22]: 1433142000000000000

# MST
In [23]: pd.Timestamp('2015-6-1 00:00', tz='Etc/GMT+7').value
Out[23]: 1433142000000000000

# Europe/Berlin
In [24]: pd.Timestamp('2015-6-1 09:00', tz='Europe/Berlin').value
Out[24]: 1433142000000000000

# UTC
In [25]: pd.Timestamp('2015-6-1 07:00', tz='UTC').value
Out[25]: 1433142000000000000

# UTC
In [26]: pd.Timestamp('2015-6-1 07:00').value
Out[26]: 1433142000000000000

It’s ultimately these integers that are used when calculating quantities in pvlib such as solar position.

As stated above, pandas will assume UTC if you do not specify a time zone. This is dangerous, and we recommend using localized timeseries, even if it is UTC.

Fixed offsets

The 'Etc/GMT*' time zones mentioned above provide fixed offset specifications, but watch out for the counter-intuitive sign convention.

In [27]: pd.Timestamp('2015-1-1 00:00', tz='Etc/GMT-2')
Out[27]: Timestamp('2015-01-01 00:00:00+0200', tz='Etc/GMT-2')

Fixed offset time zones can also be specified as offset minutes from UTC using pytz.FixedOffset.

In [28]: pd.Timestamp('2015-1-1 00:00', tz=pytz.FixedOffset(120))
Out[28]: Timestamp('2015-01-01 00:00:00+0200', tz='pytz.FixedOffset(120)')

You can also specify the fixed offset directly in the tz_localize method, however, be aware that this is not documented and that the offset must be in seconds, not minutes.

In [29]: pd.Timestamp('2015-1-1 00:00', tz=7200)
Out[29]: Timestamp('2015-01-01 00:00:00+0200', tz='pytz.FixedOffset(120)')

Yet another way to specify a time zone with a fixed offset is by using the string formulation.

In [30]: pd.Timestamp('2015-1-1 00:00+0200')
Out[30]: Timestamp('2015-01-01 00:00:00+0200', tz='pytz.FixedOffset(120)')

Native Python objects

Sometimes it’s convenient to use native Python datetime.date and datetime.datetime objects, so we demonstrate their use next. pandas Timestamp objects can also be created from time zone aware or naive datetime.datetime objects. The behavior is as expected.

# tz naive python datetime.datetime object
In [31]: naive_python_dt = datetime.datetime(2015, 6, 1, 0)

# tz naive pandas Timestamp object
In [32]: pd.Timestamp(naive_python_dt)
Out[32]: Timestamp('2015-06-01 00:00:00')

# tz aware python datetime.datetime object
In [33]: aware_python_dt = pytz.timezone('US/Mountain').localize(naive_python_dt)

# tz aware pandas Timestamp object
In [34]: pd.Timestamp(aware_python_dt)
Out[34]: Timestamp('2015-06-01 00:00:00-0600', tz='US/Mountain')

One thing to watch out for is that python datetime.date objects gain time information when passed to Timestamp.

# tz naive python datetime.date object (no time info)
In [35]: naive_python_date = datetime.date(2015, 6, 1)

# tz naive pandas Timestamp object (time=midnight)
In [36]: pd.Timestamp(naive_python_date)
Out[36]: Timestamp('2015-06-01 00:00:00')

You cannot localize a native Python date object.

 # fail
In [37]: pytz.timezone('US/Mountain').localize(naive_python_date)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-37-c4cced4ef9d9> in <module>
----> 1 pytz.timezone('US/Mountain').localize(naive_python_date)

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pytz/tzinfo.py in localize(self, dt, is_dst)
    315         Non-existent
    316         '''
--> 317         if dt.tzinfo is not None:
    318             raise ValueError('Not naive datetime (tzinfo is already set)')
    319 

AttributeError: 'datetime.date' object has no attribute 'tzinfo'

pvlib-specific functionality

Note

This section applies to pvlib >= 0.3. Version 0.2 of pvlib used a Location object’s tz attribute to auto-magically correct for some time zone issues. This behavior was counter-intuitive to many users and was removed in version 0.3.

How does this general functionality interact with pvlib? Perhaps the two most common places to get tripped up with time and time zone issues in solar power analysis occur during data import and solar position calculations.

Data import

Let’s first examine how pvlib handles time when it imports a TMY3 file.

In [38]: import os

In [39]: import inspect

In [40]: import pvlib

# some gymnastics to find the example file
In [41]: pvlib_abspath = os.path.dirname(os.path.abspath(inspect.getfile(pvlib)))

In [42]: file_abspath = os.path.join(pvlib_abspath, 'data', '703165TY.csv')

In [43]: tmy3_data, tmy3_metadata = pvlib.iotools.read_tmy3(file_abspath)

In [44]: tmy3_metadata
Out[44]: 
{'USAF': 703165,
 'Name': '"SAND POINT"',
 'State': 'AK',
 'TZ': -9.0,
 'latitude': 55.317,
 'longitude': -160.517,
 'altitude': 7.0}

The metadata has a 'TZ' key with a value of -9.0. This is the UTC offset in hours in which the data has been recorded. The readtmy3() function read the data in the file, created a DataFrame with that data, and then localized the DataFrame’s index to have this fixed offset. Here, we print just a few of the rows and columns of the large dataframe.

In [45]: tmy3_data.index.tz
Out[45]: pytz.FixedOffset(-540)

In [46]: tmy3_data.loc[tmy3_data.index[0:3], ['GHI', 'DNI', 'AOD']]
Out[46]: 
                           GHI  DNI    AOD
datetime                                  
1997-01-01 01:00:00-09:00    0    0  0.051
1997-01-01 02:00:00-09:00    0    0  0.051
1997-01-01 03:00:00-09:00    0    0  0.051

The readtmy2() function also returns a DataFrame with a localized DatetimeIndex.

Solar position

The correct solar position can be immediately calculated from the DataFrame’s index since the index has been localized.

In [47]: solar_position = pvlib.solarposition.get_solarposition(tmy3_data.index,
   ....:                                                        tmy3_metadata['latitude'],
   ....:                                                        tmy3_metadata['longitude'])
   ....: 

In [48]: ax = solar_position.loc[solar_position.index[0:24], ['apparent_zenith', 'apparent_elevation', 'azimuth']].plot()

In [49]: ax.legend(loc=1);

In [50]: ax.axhline(0, color='darkgray');  # add 0 deg line for sunrise/sunset

In [51]: ax.axhline(180, color='darkgray');  # add 180 deg line for azimuth at solar noon

In [52]: ax.set_ylim(-60, 200);  # zoom in, but cuts off full azimuth range

In [53]: ax.set_xlabel('Local time ({})'.format(solar_position.index.tz));

In [54]: ax.set_ylabel('(degrees)');
_images/solar-position.png

According to the US Navy, on January 1, 1997 at Sand Point, Alaska, sunrise was at 10:09 am, solar noon was at 1:46 pm, and sunset was at 5:23 pm. This is consistent with the data plotted above (and depressing).

Solar position (assumed UTC)

What if we had a DatetimeIndex that was not localized, such as the one below? The solar position calculator will assume UTC time.

In [55]: index = pd.DatetimeIndex(start='1997-01-01 01:00', freq='1h', periods=24)

In [56]: index
Out[56]: 
DatetimeIndex(['1997-01-01 01:00:00', '1997-01-01 02:00:00',
               '1997-01-01 03:00:00', '1997-01-01 04:00:00',
               '1997-01-01 05:00:00', '1997-01-01 06:00:00',
               '1997-01-01 07:00:00', '1997-01-01 08:00:00',
               '1997-01-01 09:00:00', '1997-01-01 10:00:00',
               '1997-01-01 11:00:00', '1997-01-01 12:00:00',
               '1997-01-01 13:00:00', '1997-01-01 14:00:00',
               '1997-01-01 15:00:00', '1997-01-01 16:00:00',
               '1997-01-01 17:00:00', '1997-01-01 18:00:00',
               '1997-01-01 19:00:00', '1997-01-01 20:00:00',
               '1997-01-01 21:00:00', '1997-01-01 22:00:00',
               '1997-01-01 23:00:00', '1997-01-02 00:00:00'],
              dtype='datetime64[ns]', freq='H')

In [57]: solar_position_notz = pvlib.solarposition.get_solarposition(index,
   ....:                                                             tmy3_metadata['latitude'],
   ....:                                                             tmy3_metadata['longitude'])
   ....: 

In [58]: ax = solar_position_notz.loc[solar_position_notz.index[0:24], ['apparent_zenith', 'apparent_elevation', 'azimuth']].plot()

In [59]: ax.legend(loc=1);

In [60]: ax.axhline(0, color='darkgray');  # add 0 deg line for sunrise/sunset

In [61]: ax.axhline(180, color='darkgray');  # add 180 deg line for azimuth at solar noon

In [62]: ax.set_ylim(-60, 200);  # zoom in, but cuts off full azimuth range

In [63]: ax.set_xlabel('Time (UTC)');

In [64]: ax.set_ylabel('(degrees)');
_images/solar-position-nolocal.png

This looks like the plot above, but shifted by 9 hours.

Solar position (calculate and convert)

In principle, one could localize the tz-naive solar position data to UTC, and then convert it to the desired time zone.

In [65]: fixed_tz = pytz.FixedOffset(tmy3_metadata['TZ'] * 60)

In [66]: solar_position_hack = solar_position_notz.tz_localize('UTC').tz_convert(fixed_tz)

In [67]: solar_position_hack.index
Out[67]: 
DatetimeIndex(['1996-12-31 16:00:00-09:00', '1996-12-31 17:00:00-09:00',
               '1996-12-31 18:00:00-09:00', '1996-12-31 19:00:00-09:00',
               '1996-12-31 20:00:00-09:00', '1996-12-31 21:00:00-09:00',
               '1996-12-31 22:00:00-09:00', '1996-12-31 23:00:00-09:00',
               '1997-01-01 00:00:00-09:00', '1997-01-01 01:00:00-09:00',
               '1997-01-01 02:00:00-09:00', '1997-01-01 03:00:00-09:00',
               '1997-01-01 04:00:00-09:00', '1997-01-01 05:00:00-09:00',
               '1997-01-01 06:00:00-09:00', '1997-01-01 07:00:00-09:00',
               '1997-01-01 08:00:00-09:00', '1997-01-01 09:00:00-09:00',
               '1997-01-01 10:00:00-09:00', '1997-01-01 11:00:00-09:00',
               '1997-01-01 12:00:00-09:00', '1997-01-01 13:00:00-09:00',
               '1997-01-01 14:00:00-09:00', '1997-01-01 15:00:00-09:00'],
              dtype='datetime64[ns, pytz.FixedOffset(-540)]', freq='H')

In [68]: ax = solar_position_hack.loc[solar_position_hack.index[0:24], ['apparent_zenith', 'apparent_elevation', 'azimuth']].plot()

In [69]: ax.legend(loc=1);

In [70]: ax.axhline(0, color='darkgray');  # add 0 deg line for sunrise/sunset

In [71]: ax.axhline(180, color='darkgray');  # add 180 deg line for azimuth at solar noon

In [72]: ax.set_ylim(-60, 200);  # zoom in, but cuts off full azimuth range

In [73]: ax.set_xlabel('Local time ({})'.format(solar_position_hack.index.tz));

In [74]: ax.set_ylabel('(degrees)');
_images/solar-position-hack.png

Note that the time has been correctly localized and converted, however, the calculation bounds still correspond to the original assumed-UTC range.

For this and other reasons, we recommend that users supply time zone information at the beginning of a calculation rather than localizing and converting the results at the end of a calculation.

Clear sky

This section reviews the clear sky modeling capabilities of pvlib-python.

pvlib-python supports two ways to generate clear sky irradiance:

  1. A Location object’s get_clearsky() method.

  2. The functions contained in the clearsky module, including ineichen() and simplified_solis().

Users that work with simple time series data may prefer to use get_clearsky(), while users that want finer control, more explicit code, or work with multidimensional data may prefer to use the basic functions in the clearsky module.

The Location subsection demonstrates the easiest way to obtain a time series of clear sky data for a location. The Ineichen and Perez and Simplified Solis subsections detail the clear sky algorithms and input data. The Detect Clearsky subsection demonstrates the use of the clear sky detection algorithm.

We’ll need these imports for the examples below.

In [1]: import os

In [2]: import itertools

In [3]: import matplotlib.pyplot as plt

In [4]: import pandas as pd

In [5]: import pvlib

In [6]: from pvlib import clearsky, atmosphere, solarposition

In [7]: from pvlib.location import Location

In [8]: from pvlib.iotools import read_tmy3

Location

The easiest way to obtain a time series of clear sky irradiance is to use a Location object’s get_clearsky() method. The get_clearsky() method does the dirty work of calculating solar position, extraterrestrial irradiance, airmass, and atmospheric pressure, as appropriate, leaving the user to only specify the most important parameters: time and atmospheric attenuation. The time input must be a pandas.DatetimeIndex, while the atmospheric attenuation inputs may be constants or arrays. The get_clearsky() method always returns a pandas.DataFrame.

In [9]: tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson')

In [10]: times = pd.date_range(start='2016-07-01', end='2016-07-04', freq='1min', tz=tus.tz)

In [11]: cs = tus.get_clearsky(times)  # ineichen with climatology table by default

In [12]: cs.plot();

In [13]: plt.ylabel('Irradiance $W/m^2$');

In [14]: plt.title('Ineichen, climatological turbidity');
_images/location-basic.png

The get_clearsky() method accepts a model keyword argument and propagates additional arguments to the functions that do the computation.

In [15]: cs = tus.get_clearsky(times, model='ineichen', linke_turbidity=3)

In [16]: cs.plot();

In [17]: plt.title('Ineichen, linke_turbidity=3');

In [18]: plt.ylabel('Irradiance $W/m^2$');
_images/location-ineichen.png
In [19]: cs = tus.get_clearsky(times, model='simplified_solis', aod700=0.2, precipitable_water=3)

In [20]: cs.plot();

In [21]: plt.title('Simplfied Solis, aod700=0.2, precipitable_water=3');

In [22]: plt.ylabel('Irradiance $W/m^2$');
_images/location-solis.png

See the sections below for more detail on the clear sky models.

Ineichen and Perez

The Ineichen and Perez clear sky model parameterizes irradiance in terms of the Linke turbidity [Ine02]. pvlib-python implements this model in the pvlib.clearsky.ineichen() function.

Turbidity data

pvlib includes a file with monthly climatological turbidity values for the globe. The code below creates turbidity maps for a few months of the year. You could run it in a loop to create plots for all months.

In [23]: import calendar

In [24]: import os

In [25]: import tables

In [26]: pvlib_path = os.path.dirname(os.path.abspath(pvlib.clearsky.__file__))

In [27]: filepath = os.path.join(pvlib_path, 'data', 'LinkeTurbidities.h5')

In [28]: def plot_turbidity_map(month, vmin=1, vmax=100):
   ....:     plt.figure();
   ....:     with tables.open_file(filepath) as lt_h5_file:
   ....:         ltdata = lt_h5_file.root.LinkeTurbidity[:, :, month-1]
   ....:     plt.imshow(ltdata, vmin=vmin, vmax=vmax);
   ....:     # data is in units of 20 x turbidity
   ....:     plt.title('Linke turbidity x 20, ' + calendar.month_name[month]);
   ....:     plt.colorbar(shrink=0.5);
   ....:     plt.tight_layout();
   ....: 

In [29]: plot_turbidity_map(1)

In [30]: plot_turbidity_map(7)
_images/turbidity-1.png _images/turbidity-7.png

The lookup_linke_turbidity() function takes a time, latitude, and longitude and gets the corresponding climatological turbidity value for that time at those coordinates. By default, the lookup_linke_turbidity() function will linearly interpolate turbidity from month to month, assuming that the raw data is valid on 15th of each month. This interpolation removes discontinuities in multi-month PV models. Here’s a plot of a few locations in the Southwest U.S. with and without interpolation. We chose points that are relatively close so that you can get a better sense of the spatial noise and variability of the data set. Note that the altitude of these sites varies from 300 m to 1500 m.

In [31]: times = pd.date_range(start='2015-01-01', end='2016-01-01', freq='1D')

In [32]: sites = [(32, -111, 'Tucson1'), (32.2, -110.9, 'Tucson2'),
   ....:          (33.5, -112.1, 'Phoenix'), (35.1, -106.6, 'Albuquerque')]
   ....: 

In [33]: plt.figure();

In [34]: for lat, lon, name in sites:
   ....:     turbidity = pvlib.clearsky.lookup_linke_turbidity(times, lat, lon, interp_turbidity=False)
   ....:     turbidity.plot(label=name)
   ....: 

In [35]: plt.legend();

In [36]: plt.title('Raw data (no interpolation)');

In [37]: plt.ylabel('Linke Turbidity');

In [38]: plt.figure();

In [39]: for lat, lon, name in sites:
   ....:     turbidity = pvlib.clearsky.lookup_linke_turbidity(times, lat, lon)
   ....:     turbidity.plot(label=name)
   ....: 

In [40]: plt.legend();

In [41]: plt.title('Interpolated to the day');

In [42]: plt.ylabel('Linke Turbidity');
_images/turbidity-no-interp.png _images/turbidity-yes-interp.png

The kasten96_lt() function can be used to calculate Linke turbidity [Kas96] as input to the clear sky Ineichen and Perez function. The Kasten formulation requires precipitable water and broadband aerosol optical depth (AOD). According to Molineaux, broadband AOD can be approximated by a single measurement at 700-nm [Mol98]. An alternate broadband AOD approximation from Bird and Hulstrom combines AOD measured at two wavelengths [Bir80], and is implemented in bird_hulstrom80_aod_bb().

In [43]: pvlib_data = os.path.join(os.path.dirname(pvlib.__file__), 'data')

In [44]: mbars = 100  # conversion factor from mbars to Pa

In [45]: tmy_file = os.path.join(pvlib_data, '703165TY.csv')  # TMY file

In [46]: tmy_data, tmy_header = read_tmy3(tmy_file, coerce_year=1999)  # read TMY data

In [47]: tl_historic = clearsky.lookup_linke_turbidity(time=tmy_data.index,
   ....:     latitude=tmy_header['latitude'], longitude=tmy_header['longitude'])
   ....: 

In [48]: solpos = solarposition.get_solarposition(time=tmy_data.index,
   ....:     latitude=tmy_header['latitude'], longitude=tmy_header['longitude'],
   ....:     altitude=tmy_header['altitude'], pressure=tmy_data['Pressure']*mbars,
   ....:     temperature=tmy_data['DryBulb'])
   ....: 

In [49]: am_rel = atmosphere.get_relative_airmass(solpos.apparent_zenith)

In [50]: am_abs = atmosphere.get_absolute_airmass(am_rel, tmy_data['Pressure']*mbars)

In [51]: airmass = pd.concat([am_rel, am_abs], axis=1).rename(
   ....:     columns={0: 'airmass_relative', 1: 'airmass_absolute'})
   ....: 

In [52]: tl_calculated = atmosphere.kasten96_lt(
   ....:     airmass.airmass_absolute, tmy_data['Pwat'], tmy_data['AOD'])
   ....: 

In [53]: tl = pd.concat([tl_historic, tl_calculated], axis=1).rename(
   ....:     columns={0:'Historic', 1:'Calculated'})
   ....: 

In [54]: tl.index = tmy_data.index.tz_convert(None)  # remove timezone

In [55]: tl.resample('W').mean().plot();

In [56]: plt.grid()

In [57]: plt.title('Comparison of Historic Linke Turbidity Factors vs. \n'
   ....:     'Kasten Pyrheliometric Formula at {name:s}, {state:s} ({usaf:d}TY)'.format(
   ....:     name=tmy_header['Name'], state=tmy_header['State'], usaf=tmy_header['USAF']));
   ....: 

In [58]: plt.ylabel('Linke Turbidity Factor, TL');

In [59]: plt.tight_layout()
_images/kasten-tl.png

Examples

A clear sky time series using only basic pvlib functions.

In [60]: latitude, longitude, tz, altitude, name = 32.2, -111, 'US/Arizona', 700, 'Tucson'

In [61]: times = pd.date_range(start='2014-01-01', end='2014-01-02', freq='1Min', tz=tz)

In [62]: solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude)

In [63]: apparent_zenith = solpos['apparent_zenith']

In [64]: airmass = pvlib.atmosphere.get_relative_airmass(apparent_zenith)

In [65]: pressure = pvlib.atmosphere.alt2pres(altitude)

In [66]: airmass = pvlib.atmosphere.get_absolute_airmass(airmass, pressure)

In [67]: linke_turbidity = pvlib.clearsky.lookup_linke_turbidity(times, latitude, longitude)

In [68]: dni_extra = pvlib.irradiance.get_extra_radiation(times)

# an input is a pandas Series, so solis is a DataFrame
In [69]: ineichen = clearsky.ineichen(apparent_zenith, airmass, linke_turbidity, altitude, dni_extra)

In [70]: plt.figure();

In [71]: ax = ineichen.plot()

In [72]: ax.set_ylabel('Irradiance $W/m^2$');

In [73]: ax.set_title('Ineichen Clear Sky Model');

In [74]: ax.legend(loc=2);
_images/ineichen-vs-time-climo.png

The input data types determine the returned output type. Array input results in an OrderedDict of array output, and Series input results in a DataFrame output. The keys are ‘ghi’, ‘dni’, and ‘dhi’.

Grid with a clear sky irradiance for a few turbidity values.

In [75]: times = pd.date_range(start='2014-09-01', end='2014-09-02', freq='1Min', tz=tz)

In [76]: solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude)

In [77]: apparent_zenith = solpos['apparent_zenith']

In [78]: airmass = pvlib.atmosphere.get_relative_airmass(apparent_zenith)

In [79]: pressure = pvlib.atmosphere.alt2pres(altitude)

In [80]: airmass = pvlib.atmosphere.get_absolute_airmass(airmass, pressure)

In [81]: linke_turbidity = pvlib.clearsky.lookup_linke_turbidity(times, latitude, longitude)

In [82]: print('climatological linke_turbidity = {}'.format(linke_turbidity.mean()))
climatological linke_turbidity = 3.329496820286459

In [83]: dni_extra = pvlib.irradiance.get_extra_radiation(times)

In [84]: linke_turbidities = [linke_turbidity.mean(), 2, 4]

In [85]: fig, axes = plt.subplots(ncols=3, nrows=1, sharex=True, sharey=True, squeeze=True, figsize=(12, 4))

In [86]: axes = axes.flatten()

In [87]: for linke_turbidity, ax in zip(linke_turbidities, axes):
   ....:     ineichen = clearsky.ineichen(apparent_zenith, airmass, linke_turbidity, altitude, dni_extra)
   ....:     ineichen.plot(ax=ax, title='Linke turbidity = {:0.1f}'.format(linke_turbidity));
   ....: 

In [88]: ax.legend(loc=1);
_images/ineichen-grid.png

Validation

See [Ine02], [Ren12].

Will Holmgren compared pvlib’s Ineichen model and climatological turbidity to SoDa’s McClear service in Arizona. Here are links to an ipynb notebook and its html rendering.

Simplified Solis

The Simplified Solis model parameterizes irradiance in terms of precipitable water and aerosol optical depth [Ine08ss]. pvlib-python implements this model in the pvlib.clearsky.simplified_solis() function.

Aerosol and precipitable water data

There are a number of sources for aerosol and precipitable water data of varying accuracy, global coverage, and temporal resolution. Ground based aerosol data can be obtained from Aeronet. Precipitable water can be obtained from radiosondes, ESRL GPS-MET, or derived from surface relative humidity using functions such as pvlib.atmosphere.gueymard94_pw(). Numerous gridded products from satellites, weather models, and climate models contain one or both of aerosols and precipitable water. Consider data from the ECMWF and SoDa.

Aerosol optical depth (AOD) is a function of wavelength, and the Simplified Solis model requires AOD at 700 nm. angstrom_aod_at_lambda() is useful for converting AOD between different wavelengths using the Angstrom turbidity model. The Angstrom exponent, \(\alpha\), can be calculated from AOD at two wavelengths with angstrom_alpha(). [Ine08con], [Ine16], [Ang61].

In [89]: aod1240nm = 1.2  # fictitious AOD measured at 1240-nm

In [90]: aod550nm = 3.1  # fictitious AOD measured at 550-nm

In [91]: alpha_exponent = atmosphere.angstrom_alpha(aod1240nm, 1240, aod550nm, 550)

In [92]: aod700nm = atmosphere.angstrom_aod_at_lambda(aod1240nm, 1240, alpha_exponent, 700)

In [93]: aod380nm = atmosphere.angstrom_aod_at_lambda(aod550nm, 550, alpha_exponent, 380)

In [94]: aod500nm = atmosphere.angstrom_aod_at_lambda(aod550nm, 550, alpha_exponent, 500)

In [95]: aod_bb = atmosphere.bird_hulstrom80_aod_bb(aod380nm, aod500nm)

In [96]: print('compare AOD at 700-nm = {:g}, to estimated broadband AOD = {:g}, '
   ....:     'with alpha = {:g}'.format(aod700nm, aod_bb, alpha_exponent))
   ....: 
compare AOD at 700-nm = 2.33931, to estimated broadband AOD = 2.52936, with alpha = 1.16745

Examples

A clear sky time series using only basic pvlib functions.

In [97]: latitude, longitude, tz, altitude, name = 32.2, -111, 'US/Arizona', 700, 'Tucson'

In [98]: times = pd.date_range(start='2014-01-01', end='2014-01-02', freq='1Min', tz=tz)

In [99]: solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude)

In [100]: apparent_elevation = solpos['apparent_elevation']

In [101]: aod700 = 0.1

In [102]: precipitable_water = 1

In [103]: pressure = pvlib.atmosphere.alt2pres(altitude)

In [104]: dni_extra = pvlib.irradiance.get_extra_radiation(times)

# an input is a Series, so solis is a DataFrame
In [105]: solis = clearsky.simplified_solis(apparent_elevation, aod700, precipitable_water,
   .....:                                   pressure, dni_extra)
   .....: 

In [106]: ax = solis.plot();

In [107]: ax.set_ylabel('Irradiance $W/m^2$');

In [108]: ax.set_title('Simplified Solis Clear Sky Model');

In [109]: ax.legend(loc=2);
_images/solis-vs-time-0.1-1.png

The input data types determine the returned output type. Array input results in an OrderedDict of array output, and Series input results in a DataFrame output. The keys are ‘ghi’, ‘dni’, and ‘dhi’.

Irradiance as a function of solar elevation.

In [110]: apparent_elevation = pd.Series(np.linspace(-10, 90, 101))

In [111]: aod700 = 0.1

In [112]: precipitable_water = 1

In [113]: pressure = 101325

In [114]: dni_extra = 1364

In [115]: solis = clearsky.simplified_solis(apparent_elevation, aod700,
   .....:                                   precipitable_water, pressure, dni_extra)
   .....: 

In [116]: ax = solis.plot();

In [117]: ax.set_xlabel('Apparent elevation (deg)');

In [118]: ax.set_ylabel('Irradiance $W/m^2$');

In [119]: ax.set_title('Irradiance vs Solar Elevation')
Out[119]: Text(0.5, 1.0, 'Irradiance vs Solar Elevation')

In [120]: ax.legend(loc=2);
_images/solis-vs-elevation.png

Grid with clear sky irradiance for a few PW and AOD values.

In [121]: times = pd.date_range(start='2014-09-01', end='2014-09-02', freq='1Min', tz=tz)

In [122]: solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude)

In [123]: apparent_elevation = solpos['apparent_elevation']

In [124]: pressure = pvlib.atmosphere.alt2pres(altitude)

In [125]: dni_extra = pvlib.irradiance.get_extra_radiation(times)

In [126]: aod700 = [0.01, 0.1]

In [127]: precipitable_water = [0.5, 5]

In [128]: fig, axes = plt.subplots(ncols=2, nrows=2, sharex=True, sharey=True, squeeze=True)

In [129]: axes = axes.flatten()

In [130]: for (aod, pw), ax in zip(itertools.chain(itertools.product(aod700, precipitable_water)), axes):
   .....:     cs = clearsky.simplified_solis(apparent_elevation, aod, pw, pressure, dni_extra)
   .....:     cs.plot(ax=ax, title='aod700={}, pw={}'.format(aod, pw))
   .....: 
_images/solis-grid.png

Contour plots of irradiance as a function of both PW and AOD.

In [131]: aod700 = np.linspace(0, 0.5, 101)

In [132]: precipitable_water = np.linspace(0, 10, 101)

In [133]: apparent_elevation = 70

In [134]: pressure = 101325

In [135]: dni_extra = 1364

In [136]: aod700, precipitable_water = np.meshgrid(aod700, precipitable_water)

# inputs are arrays, so solis is an OrderedDict
In [137]: solis = clearsky.simplified_solis(apparent_elevation, aod700,
   .....:                                   precipitable_water, pressure,
   .....:                                   dni_extra)
   .....: 

In [138]: n = 15

In [139]: vmin = None

In [140]: vmax = None

In [141]: def plot_solis(key):
   .....:     irrad = solis[key]
   .....:     fig, ax = plt.subplots()
   .....:     im = ax.contour(aod700, precipitable_water, irrad[:, :], n, vmin=vmin, vmax=vmax)
   .....:     imf = ax.contourf(aod700, precipitable_water, irrad[:, :], n, vmin=vmin, vmax=vmax)
   .....:     ax.set_xlabel('AOD')
   .....:     ax.set_ylabel('Precipitable water (cm)')
   .....:     ax.clabel(im, colors='k', fmt='%.0f')
   .....:     fig.colorbar(imf, label='{} (W/m**2)'.format(key))
   .....:     ax.set_title('{}, elevation={}'.format(key, apparent_elevation))
   .....: 
In [142]: plot_solis('ghi')


In [143]: plot_solis('dni')


In [144]: plot_solis('dhi')
_images/solis-ghi.png _images/solis-dni.png _images/solis-dhi.png

Validation

See [Ine16].

We encourage users to compare the pvlib implementation to Ineichen’s Excel tool.

Detect Clearsky

The detect_clearsky() function implements the [Ren16] algorithm to detect the clear and cloudy points of a time series. The algorithm was designed and validated for analyzing GHI time series only. Users may attempt to apply it to other types of time series data using different filter settings, but should be skeptical of the results.

The algorithm detects clear sky times by comparing statistics for a measured time series and an expected clearsky time series. Statistics are calculated using a sliding time window (e.g., 10 minutes). An iterative algorithm identifies clear periods, uses the identified periods to estimate bias in the clearsky data, scales the clearsky data and repeats.

Clear times are identified by meeting 5 criteria. Default values for these thresholds are appropriate for 10 minute windows of 1 minute GHI data.

Next, we show a simple example of applying the algorithm to synthetic GHI data. We first generate and plot the clear sky and measured data.

In [145]: abq = Location(35.04, -106.62, altitude=1619)

In [146]: times = pd.date_range(start='2012-04-01 10:30:00', tz='Etc/GMT+7', periods=30, freq='1min')

In [147]: cs = abq.get_clearsky(times)

# scale clear sky data to account for possibility of different turbidity
In [148]: ghi = cs['ghi']*.953

# add a cloud event
In [149]: ghi['2012-04-01 10:42:00':'2012-04-01 10:44:00'] = [500, 300, 400]

# add an overirradiance event
In [150]: ghi['2012-04-01 10:56:00'] = 950

In [151]: fig, ax = plt.subplots()

In [152]: ghi.plot(label='input');

In [153]: cs['ghi'].plot(label='ineichen clear');

In [154]: ax.set_ylabel('Irradiance $W/m^2$');

In [155]: plt.legend(loc=4);
_images/detect-clear-ghi.png

Now we run the synthetic data and clear sky estimate through the detect_clearsky() function.

In [156]: clear_samples = clearsky.detect_clearsky(ghi, cs['ghi'], cs.index, 10)

In [157]: fig, ax = plt.subplots()

In [158]: clear_samples.astype(int).plot();

In [159]: ax.set_ylabel('Clear (1) or Cloudy (0)');
_images/detect-clear-detected.png

The algorithm detected the cloud event and the overirradiance event.

References

Ine02(1,2)

P. Ineichen and R. Perez, “A New airmass independent formulation for the Linke turbidity coefficient”, Solar Energy, 73, pp. 151-157, 2002.

Ine08ss

P. Ineichen, “A broadband simplified version of the Solis clear sky model,” Solar Energy, 82, 758-762 (2008).

Ine16(1,2)

P. Ineichen, “Validation of models that estimate the clear sky global and beam solar irradiance,” Solar Energy, 132, 332-344 (2016).

Ine08con

P. Ineichen, “Conversion function between the Linke turbidity and the atmospheric water vapor and aerosol content”, Solar Energy, 82, 1095 (2008).

Ren12

M. Reno, C. Hansen, and J. Stein, “Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis”, Sandia National Laboratories, SAND2012-2389, 2012.

Ren16

Reno, M.J. and C.W. Hansen, “Identification of periods of clear sky irradiance in time series of GHI measurements” Renewable Energy, v90, p. 520-531, 2016.

Mol98

B. Molineaux, P. Ineichen, and N. O’Neill, “Equivalence of pyrheliometric and monochromatic aerosol optical depths at a single key wavelength.,” Appl. Opt., vol. 37, no. 30, pp. 7008–18, Oct. 1998.

Kas96

F. Kasten, “The linke turbidity factor based on improved values of the integral Rayleigh optical thickness,” Sol. Energy, vol. 56, no. 3, pp. 239–244, Mar. 1996.

Bir80

R. E. Bird and R. L. Hulstrom, “Direct Insolation Models,” 1980.

Ang61

A. ÅNGSTRÖM, “Techniques of Determinig the Turbidity of the Atmosphere,” Tellus A, vol. 13, no. 2, pp. 214–223, 1961.

Forecasting

pvlib-python provides a set of functions and classes that make it easy to obtain weather forecast data and convert that data into a PV power forecast. Users can retrieve standardized weather forecast data relevant to PV power modeling from NOAA/NCEP/NWS models including the GFS, NAM, RAP, HRRR, and the NDFD. A PV power forecast can then be obtained using the weather data as inputs to the comprehensive modeling capabilities of PVLIB-Python. Standardized, open source, reference implementations of forecast methods using publicly available data may help advance the state-of-the-art of solar power forecasting.

pvlib-python uses Unidata’s Siphon library to simplify access to real-time forecast data hosted on the Unidata THREDDS catalog. Siphon is great for programatic access of THREDDS data, but we also recommend using tools such as Panoply to easily browse the catalog and become more familiar with its contents.

We do not know of a similarly easy way to access archives of forecast data.

This document demonstrates how to use pvlib-python to create a PV power forecast using these tools. The forecast and forecast_to_power Jupyter notebooks provide additional example code.

Warning

The forecast module algorithms and features are highly experimental. The API may change, the functionality may be consolidated into an io module, or the module may be separated into its own package.

Note

This documentation is difficult to reliably build on readthedocs. If you do not see images, try building the documentation on your own machine or see the notebooks linked to above.

Accessing Forecast Data

The Siphon library provides access to, among others, forecasts from the Global Forecast System (GFS), North American Model (NAM), High Resolution Rapid Refresh (HRRR), Rapid Refresh (RAP), and National Digital Forecast Database (NDFD) on a Unidata THREDDS server. Unfortunately, many of these models use different names to describe the same quantity (or a very similar one), and not all variables are present in all models. For example, on the THREDDS server, the GFS has a field named Total_cloud_cover_entire_atmosphere_Mixed_intervals_Average, while the NAM has a field named Total_cloud_cover_entire_atmosphere_single_layer, and a similar field in the HRRR is named Total_cloud_cover_entire_atmosphere.

PVLIB-Python aims to simplify the access of the model fields relevant for solar power forecasts. Model data accessed with PVLIB-Python is returned as a pandas DataFrame with consistent column names: temp_air, wind_speed, total_clouds, low_clouds, mid_clouds, high_clouds, dni, dhi, ghi. To accomplish this, we use an object-oriented framework in which each weather model is represented by a class that inherits from a parent ForecastModel class. The parent ForecastModel class contains the common code for accessing and parsing the data using Siphon, while the child model-specific classes (GFS, HRRR, etc.) contain the code necessary to map and process that specific model’s data to the standardized fields.

The code below demonstrates how simple it is to access and plot forecast data using PVLIB-Python. First, we set up make the basic imports and then set the location and time range data.

In [1]: import pandas as pd

In [2]: import matplotlib.pyplot as plt

In [3]: import datetime

# import pvlib forecast models
In [4]: from pvlib.forecast import GFS, NAM, NDFD, HRRR, RAP

# specify location (Tucson, AZ)
In [5]: latitude, longitude, tz = 32.2, -110.9, 'US/Arizona'

# specify time range.
In [6]: start = pd.Timestamp(datetime.date.today(), tz=tz)

In [7]: end = start + pd.Timedelta(days=7)

In [8]: irrad_vars = ['ghi', 'dni', 'dhi']

Next, we instantiate a GFS model object and get the forecast data from Unidata.

# GFS model, defaults to 0.5 degree resolution
# 0.25 deg available
In [9]: model = GFS()

# retrieve data. returns pandas.DataFrame object
In [10]: raw_data = model.get_data(latitude, longitude, start, end)

In [11]: print(raw_data.head())
                           Total_cloud_cover_high_cloud_Mixed_intervals_Average  ...  v-component_of_wind_isobaric
2019-10-10 09:00:00-07:00                                                0.0     ...                      5.378662
2019-10-10 12:00:00-07:00                                                0.0     ...                      3.623459
2019-10-10 15:00:00-07:00                                                0.0     ...                      2.341147
2019-10-10 18:00:00-07:00                                                0.0     ...                      2.289145
2019-10-10 21:00:00-07:00                                                0.0     ...                      2.377673

[5 rows x 11 columns]

It will be useful to process this data before using it with pvlib. For example, the column names are non-standard, the temperature is in Kelvin, the wind speed is broken into east/west and north/south components, and most importantly, most of the irradiance data is missing. The forecast module provides a number of methods to fix these problems.

In [12]: data = raw_data

# rename the columns according the key/value pairs in model.variables.
In [13]: data = model.rename(data)

# convert temperature
In [14]: data['temp_air'] = model.kelvin_to_celsius(data['temp_air'])

# convert wind components to wind speed
In [15]: data['wind_speed'] = model.uv_to_speed(data)

# calculate irradiance estimates from cloud cover.
# uses a cloud_cover to ghi to dni model or a
# uses a cloud cover to transmittance to irradiance model.
# this step is discussed in more detail in the next section
In [16]: irrad_data = model.cloud_cover_to_irradiance(data['total_clouds'])

In [17]: data = data.join(irrad_data, how='outer')

# keep only the final data
In [18]: data = data[model.output_variables]

In [19]: print(data.head())
                            temp_air  wind_speed  ...  mid_clouds  high_clouds
2019-10-10 09:00:00-07:00  15.750000    5.856925  ...         0.0          0.0
2019-10-10 12:00:00-07:00  14.449982    3.907449  ...         0.0          0.0
2019-10-10 15:00:00-07:00  22.050018    2.634409  ...         0.0          0.0
2019-10-10 18:00:00-07:00  37.582275    5.424297  ...         0.0          0.0
2019-10-10 21:00:00-07:00  38.550018    6.207394  ...         0.0          0.0

[5 rows x 9 columns]

Much better.

The GFS class’s process_data() method combines these steps in a single function. In fact, each forecast model class implements its own process_data method since the data from each weather model is slightly different. The process_data functions are designed to be explicit about how the data is being processed, and users are strongly encouraged to read the source code of these methods.

In [20]: data = model.process_data(raw_data)

In [21]: print(data.head())
                            temp_air  wind_speed  ...  mid_clouds  high_clouds
2019-10-10 09:00:00-07:00  15.750000    5.856925  ...         0.0          0.0
2019-10-10 12:00:00-07:00  14.449982    3.907449  ...         0.0          0.0
2019-10-10 15:00:00-07:00  22.050018    2.634409  ...         0.0          0.0
2019-10-10 18:00:00-07:00  37.582275    5.424297  ...         0.0          0.0
2019-10-10 21:00:00-07:00  38.550018    6.207394  ...         0.0          0.0

[5 rows x 9 columns]

Users can easily implement their own process_data methods on inherited classes or implement similar stand-alone functions.

The forecast model classes also implement a get_processed_data() method that combines the get_data() and process_data() calls.

In [22]: data = model.get_processed_data(latitude, longitude, start, end)

In [23]: print(data.head())
                            temp_air  wind_speed  ...  mid_clouds  high_clouds
2019-10-10 09:00:00-07:00  15.750000    5.856925  ...         0.0          0.0
2019-10-10 12:00:00-07:00  14.449982    3.907449  ...         0.0          0.0
2019-10-10 15:00:00-07:00  22.050018    2.634409  ...         0.0          0.0
2019-10-10 18:00:00-07:00  37.582275    5.424297  ...         0.0          0.0
2019-10-10 21:00:00-07:00  38.550018    6.207394  ...         0.0          0.0

[5 rows x 9 columns]

Cloud cover and radiation

All of the weather models currently accessible by pvlib include one or more cloud cover forecasts. For example, below we plot the GFS cloud cover forecasts.

# plot cloud cover percentages
In [24]: cloud_vars = ['total_clouds', 'low_clouds',
   ....:               'mid_clouds', 'high_clouds']
   ....: 

In [25]: data[cloud_vars].plot();

In [26]: plt.ylabel('Cloud cover %');

In [27]: plt.xlabel('Forecast Time ({})'.format(tz));

In [28]: plt.title('GFS 0.5 deg forecast for lat={}, lon={}'
   ....:           .format(latitude, longitude));
   ....: 

In [29]: plt.legend();
_images/gfs_cloud_cover.png

However, many of forecast models do not include radiation components in their output fields, or if they do then the radiation fields suffer from poor solar position or radiative transfer algorithms. It is often more accurate to create empirically derived radiation forecasts from the weather models’ cloud cover forecasts.

PVLIB-Python provides two basic ways to convert cloud cover forecasts to irradiance forecasts. One method assumes a linear relationship between cloud cover and GHI, applies the scaling to a clear sky climatology, and then uses the DISC model to calculate DNI. The second method assumes a linear relationship between cloud cover and atmospheric transmittance, and then uses the Liu-Jordan [Liu60] model to calculate GHI, DNI, and DHI.

Caveat emptor: these algorithms are not rigorously verified! The purpose of the forecast module is to provide a few exceedingly simple options for users to play with before they develop their own models. We strongly encourage pvlib users first read the source code and second to implement new cloud cover to irradiance algorithms.

The essential parts of the clear sky scaling algorithm are as follows. Clear sky scaling of climatological GHI is also used in Larson et. al. [Lar16].

solpos = location.get_solarposition(cloud_cover.index)
cs = location.get_clearsky(cloud_cover.index, model='ineichen')
# offset and cloud cover in decimal units here
# larson et. al. use offset = 0.35
ghi = (offset + (1 - offset) * (1 - cloud_cover)) * ghi_clear
dni = disc(ghi, solpos['zenith'], cloud_cover.index)['dni']
dhi = ghi - dni * np.cos(np.radians(solpos['zenith']))

The figure below shows the result of the total cloud cover to irradiance conversion using the clear sky scaling algorithm.

# plot irradiance data
In [30]: data = model.rename(raw_data)

In [31]: irrads = model.cloud_cover_to_irradiance(data['total_clouds'], how='clearsky_scaling')

In [32]: irrads.plot();

In [33]: plt.ylabel('Irradiance ($W/m^2$)');

In [34]: plt.xlabel('Forecast Time ({})'.format(tz));

In [35]: plt.title('GFS 0.5 deg forecast for lat={}, lon={} using "clearsky_scaling"'
   ....:           .format(latitude, longitude));
   ....: 

In [36]: plt.legend();
_images/gfs_irrad_cs.png

The essential parts of the Liu-Jordan cloud cover to irradiance algorithm are as follows.

# cloud cover in percentage units here
transmittance = ((100.0 - cloud_cover) / 100.0) * 0.75
# irrads is a DataFrame containing ghi, dni, dhi
irrads = liujordan(apparent_zenith, transmittance, airmass_absolute)

The figure below shows the result of the Liu-Jordan total cloud cover to irradiance conversion.

# plot irradiance data
In [37]: irrads = model.cloud_cover_to_irradiance(data['total_clouds'], how='liujordan')

In [38]: irrads.plot();

In [39]: plt.ylabel('Irradiance ($W/m^2$)');

In [40]: plt.xlabel('Forecast Time ({})'.format(tz));

In [41]: plt.title('GFS 0.5 deg forecast for lat={}, lon={} using "liujordan"'
   ....:           .format(latitude, longitude));
   ....: 

In [42]: plt.legend();
_images/gfs_irrad_lj.png

Most weather model output has a fairly coarse time resolution, at least an hour. The irradiance forecasts have the same time resolution as the weather data. However, it is straightforward to interpolate the cloud cover forecasts onto a higher resolution time domain, and then recalculate the irradiance.

In [43]: resampled_data = data.resample('5min').interpolate()

In [44]: resampled_irrads = model.cloud_cover_to_irradiance(resampled_data['total_clouds'], how='clearsky_scaling')

In [45]: resampled_irrads.plot();

In [46]: plt.ylabel('Irradiance ($W/m^2$)');

In [47]: plt.xlabel('Forecast Time ({})'.format(tz));

In [48]: plt.title('GFS 0.5 deg forecast for lat={}, lon={} resampled'
   ....:           .format(latitude, longitude));
   ....: 

In [49]: plt.legend();
_images/gfs_irrad_high_res.png

Users may then recombine resampled_irrads and resampled_data using slicing pandas.concat() or pandas.DataFrame.join().

We reiterate that the open source code enables users to customize the model processing to their liking.

Lar16

Larson et. al. “Day-ahead forecasting of solar power output from photovoltaic plants in the American Southwest” Renewable Energy 91, 11-20 (2016).

Liu60

B. Y. Liu and R. C. Jordan, The interrelationship and characteristic distribution of direct, diffuse, and total solar radiation, Solar Energy 4, 1 (1960).

Weather Models

Next, we provide a brief description of the weather models available to pvlib users. Note that the figures are generated when this documentation is compiled so they will vary over time.

GFS

The Global Forecast System (GFS) is the US model that provides forecasts for the entire globe. The GFS is updated every 6 hours. The GFS is run at two resolutions, 0.25 deg and 0.5 deg, and is available with 3 hour time resolution. Forecasts from GFS model were shown above. Use the GFS, among others, if you want forecasts for 1-7 days or if you want forecasts for anywhere on Earth.

HRRR

The High Resolution Rapid Refresh (HRRR) model is perhaps the most accurate model, however, it is only available for ~15 hours. It is updated every hour and runs at 3 km resolution. The HRRR excels in severe weather situations. See the NOAA ESRL HRRR page for more information. Use the HRRR, among others, if you want forecasts for less than 24 hours. The HRRR model covers the continental United States.

In [50]: model = HRRR()

In [51]: data = model.get_processed_data(latitude, longitude, start, end)

In [52]: data[irrad_vars].plot();

In [53]: plt.ylabel('Irradiance ($W/m^2$)');

In [54]: plt.xlabel('Forecast Time ({})'.format(tz));

In [55]: plt.title('HRRR 3 km forecast for lat={}, lon={}'
   ....:           .format(latitude, longitude));
   ....: 

In [56]: plt.legend();
_images/hrrr_irrad.png

RAP

The Rapid Refresh (RAP) model is the parent model for the HRRR. It is updated every hour and runs at 40, 20, and 13 km resolutions. Only the 20 and 40 km resolutions are currently available in pvlib. It is also excels in severe weather situations. See the NOAA ESRL HRRR page for more information. Use the RAP, among others, if you want forecasts for less than 24 hours. The RAP model covers most of North America.

In [57]: model = RAP()

In [58]: data = model.get_processed_data(latitude, longitude, start, end)

In [59]: data[irrad_vars].plot();

In [60]: plt.ylabel('Irradiance ($W/m^2$)');

In [61]: plt.xlabel('Forecast Time ({})'.format(tz));

In [62]: plt.title('RAP 13 km forecast for lat={}, lon={}'
   ....:           .format(latitude, longitude));
   ....: 

In [63]: plt.legend();
_images/rap_irrad.png

NAM

The North American Mesoscale model covers, not surprisingly, North America. It is updated every 6 hours. pvlib provides access to 20 km resolution NAM data with a time horizon of up to 4 days.

In [64]: model = NAM()

In [65]: data = model.get_processed_data(latitude, longitude, start, end)

In [66]: data[irrad_vars].plot();

In [67]: plt.ylabel('Irradiance ($W/m^2$)');

In [68]: plt.xlabel('Forecast Time ({})'.format(tz));

In [69]: plt.title('NAM 20 km forecast for lat={}, lon={}'
   ....:           .format(latitude, longitude));
   ....: 

In [70]: plt.legend();
_images/nam_irrad.png

NDFD

The National Digital Forecast Database is not a model, but rather a collection of forecasts made by National Weather Service offices across the country. It is updated every 6 hours. Use the NDFD, among others, for forecasts at all time horizons. The NDFD is available for the United States.

In [71]: model = NDFD()

In [72]: data = model.get_processed_data(latitude, longitude, start, end)

In [73]: data[irrad_vars].plot();

In [74]: plt.ylabel('Irradiance ($W/m^2$)');

In [75]: plt.xlabel('Forecast Time ({})'.format(tz));

In [76]: plt.title('NDFD forecast for lat={}, lon={}'
   ....:            .format(latitude, longitude));
   ....:  plt.legend();
   ....:  plt.close();
   ....: 
  File "<ipython-input-76-1c7cdda0217f>", line 3
    plt.legend();
    ^
IndentationError: unexpected indent

PV Power Forecast

Finally, we demonstrate the application of the weather forecast data to a PV power forecast. Please see the remainder of the pvlib documentation for details.

In [77]: from pvlib.pvsystem import PVSystem, retrieve_sam

In [78]: from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS

In [79]: from pvlib.tracking import SingleAxisTracker

In [80]: from pvlib.modelchain import ModelChain

In [81]: sandia_modules = retrieve_sam('sandiamod')

In [82]: cec_inverters = retrieve_sam('cecinverter')

In [83]: module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']

In [84]: inverter = cec_inverters['SMA_America__SC630CP_US_315V__CEC_2012_']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2896             try:
-> 2897                 return self._engine.get_loc(key)
   2898             except KeyError:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'SMA_America__SC630CP_US_315V__CEC_2012_'

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
<ipython-input-84-857f3df47609> in <module>
----> 1 inverter = cec_inverters['SMA_America__SC630CP_US_315V__CEC_2012_']

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/frame.py in __getitem__(self, key)
   2978             if self.columns.nlevels > 1:
   2979                 return self._getitem_multilevel(key)
-> 2980             indexer = self.columns.get_loc(key)
   2981             if is_integer(indexer):
   2982                 indexer = [indexer]

~/checkouts/readthedocs.org/user_builds/tylunelpvlib-python/envs/stable/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2897                 return self._engine.get_loc(key)
   2898             except KeyError:
-> 2899                 return self._engine.get_loc(self._maybe_cast_indexer(key))
   2900         indexer = self.get_indexer([key], method=method, tolerance=tolerance)
   2901         if indexer.ndim > 1 or indexer.size > 1:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'SMA_America__SC630CP_US_315V__CEC_2012_'

In [85]: temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']

# model a big tracker for more fun
In [86]: system = SingleAxisTracker(module_parameters=module, inverter_parameters=inverter, temperature_model_parameters=temperature_model_parameters, modules_per_string=15, strings_per_inverter=300)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-86-ca98666f4ea5> in <module>
----> 1 system = SingleAxisTracker(module_parameters=module, inverter_parameters=inverter, temperature_model_parameters=temperature_model_parameters, modules_per_string=15, strings_per_inverter=300)

NameError: name 'inverter' is not defined

# fx is a common abbreviation for forecast
In [87]: fx_model = GFS()

In [88]: fx_data = fx_model.get_processed_data(latitude, longitude, start, end)

# use a ModelChain object to calculate modeling intermediates
In [89]: mc = ModelChain(system, fx_model.location)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-89-a32dd34c6acb> in <module>
----> 1 mc = ModelChain(system, fx_model.location)

NameError: name 'system' is not defined

# extract relevant data for model chain
In [90]: mc.run_model(fx_data.index, weather=fx_data);

Now we plot a couple of modeling intermediates and the forecast power. Here’s the forecast plane of array irradiance…

In [91]: mc.total_irrad.plot();

In [92]: plt.ylabel('Plane of array irradiance ($W/m^2$)');

In [93]: plt.legend(loc='best');
_images/poa_irrad.png

…the cell and module temperature…

In [94]: mc.cell_temperature.plot();

In [95]: plt.ylabel('Cell Temperature (C)');
_images/pv_temps.png

…and finally AC power…

In [96]: mc.ac.fillna(0).plot();

In [97]: plt.ylim(0, None);

In [98]: plt.ylabel('AC Power (W)');
_images/ac_power.png

API reference

Classes

pvlib-python provides a collection of classes for users that prefer object-oriented programming. These classes can help users keep track of data in a more organized way, and can help to simplify the modeling process. The classes do not add any functionality beyond the procedural code. Most of the object methods are simple wrappers around the corresponding procedural code.

location.Location(latitude, longitude[, tz, …])

Location objects are convenient containers for latitude, longitude, timezone, and altitude data associated with a particular geographic location.

pvsystem.PVSystem([surface_tilt, …])

The PVSystem class defines a standard set of PV system attributes and modeling functions.

tracking.SingleAxisTracker([axis_tilt, …])

Inherits the PV modeling methods from PVSystem.

modelchain.ModelChain(system, location[, …])

The ModelChain class to provides a standardized, high-level interface for all of the modeling steps necessary for calculating PV power from a time series of weather inputs.

pvsystem.LocalizedPVSystem([pvsystem, location])

The LocalizedPVSystem class defines a standard set of installed PV system attributes and modeling functions.

tracking.LocalizedSingleAxisTracker([…])

The LocalizedSingleAxisTracker class defines a standard set of installed PV system attributes and modeling functions.

Solar Position

Functions and methods for calculating solar position.

The location.Location.get_solarposition() method and the solarposition.get_solarposition() function with default parameters are fast and accurate. We recommend using these functions unless you know that you need a different function.

location.Location.get_solarposition(times[, …])

Uses the solarposition.get_solarposition() function to calculate the solar zenith, azimuth, etc.

solarposition.get_solarposition(time, …[, …])

A convenience wrapper for the solar position calculators.

solarposition.spa_python(time, latitude, …)

Calculate the solar position using a python implementation of the NREL SPA algorithm described in [1].

solarposition.ephemeris(time, latitude, …)

Python-native solar position calculator.

solarposition.pyephem(time, latitude, longitude)

Calculate the solar position using the PyEphem package.

solarposition.spa_c(time, latitude, longitude)

Calculate the solar position using the C implementation of the NREL SPA code.

Additional functions for quantities closely related to solar position.

solarposition.calc_time(lower_bound, …[, …])

Calculate the time between lower_bound and upper_bound where the attribute is equal to value.

solarposition.pyephem_earthsun_distance(time)

Calculates the distance from the earth to the sun using pyephem.

solarposition.nrel_earthsun_distance(time[, …])

Calculates the distance from the earth to the sun using the NREL SPA algorithm described in [1]_.

spa.calculate_deltat(year, month)

Calculate the difference between Terrestrial Dynamical Time (TD) and Universal Time (UT).

Functions for calculating sunrise, sunset and transit times.

location.Location.get_sun_rise_set_transit(times)

Calculate sunrise, sunset and transit times.

solarposition.sun_rise_set_transit_ephem(…)

Calculate the next sunrise and sunset times using the PyEphem package.

solarposition.sun_rise_set_transit_spa(…)

Calculate the sunrise, sunset, and sun transit times using the NREL SPA algorithm described in [1].

solarposition.sun_rise_set_transit_geometric(…)

Geometric calculation of solar sunrise, sunset, and transit.

The spa module contains the implementation of the built-in NREL SPA algorithm.

spa

Calculate the solar position using the NREL SPA algorithm either using numpy arrays or compiling the code to machine language with numba.

Correlations and analytical expressions for low precision solar position calculations.

solarposition.solar_zenith_analytical(…)

Analytical expression of solar zenith angle based on spherical trigonometry.

solarposition.solar_azimuth_analytical(…)

Analytical expression of solar azimuth angle based on spherical trigonometry.

solarposition.declination_spencer71(dayofyear)

Solar declination from Duffie & Beckman [1] and attributed to Spencer (1971) and Iqbal (1983).

solarposition.declination_cooper69(dayofyear)

Solar declination from Duffie & Beckman [1] and attributed to Cooper (1969)

solarposition.equation_of_time_spencer71(…)

Equation of time from Duffie & Beckman and attributed to Spencer (1971) and Iqbal (1983).

solarposition.equation_of_time_pvcdrom(dayofyear)

Equation of time from PVCDROM.

solarposition.hour_angle(times, longitude, …)

Hour angle in local solar time.

solarposition.sun_rise_set_transit_geometric(…)

Geometric calculation of solar sunrise, sunset, and transit.

Clear sky

location.Location.get_clearsky(times[, …])

Calculate the clear sky estimates of GHI, DNI, and/or DHI at this location.

clearsky.ineichen(apparent_zenith, …[, …])

Determine clear sky GHI, DNI, and DHI from Ineichen/Perez model.

clearsky.lookup_linke_turbidity(time, …[, …])

Look up the Linke Turibidity from the LinkeTurbidities.h5 data file supplied with pvlib.

clearsky.simplified_solis(apparent_elevation)

Calculate the clear sky GHI, DNI, and DHI according to the simplified Solis model [1]_.

clearsky.haurwitz(apparent_zenith)

Determine clear sky GHI using the Haurwitz model.

clearsky.detect_clearsky(measured, clearsky, …)

Detects clear sky times according to the algorithm developed by Reno and Hansen for GHI measurements [1].

clearsky.bird(zenith, airmass_relative, …)

Bird Simple Clear Sky Broadband Solar Radiation Model

Airmass and atmospheric models

location.Location.get_airmass([times, …])

Calculate the relative and absolute airmass.

atmosphere.get_absolute_airmass(airmass_relative)

Determine absolute (pressure corrected) airmass from relative airmass and pressure

atmosphere.get_relative_airmass(zenith[, model])

Gives the relative (not pressure-corrected) airmass.

atmosphere.pres2alt(pressure)

Determine altitude from site pressure.

atmosphere.alt2pres(altitude)

Determine site pressure from altitude.

atmosphere.gueymard94_pw(temp_air, …)

Calculates precipitable water (cm) from ambient air temperature (C) and relatively humidity (%) using an empirical model.

atmosphere.first_solar_spectral_correction(pw, …)

Spectral mismatch modifier based on precipitable water and absolute (pressure corrected) airmass.

atmosphere.bird_hulstrom80_aod_bb(aod380, aod500)

Approximate broadband aerosol optical depth.

atmosphere.kasten96_lt(airmass_absolute, …)

Calculate Linke turbidity factor using Kasten pyrheliometric formula.

atmosphere.angstrom_aod_at_lambda(aod0, lambda0)

Get AOD at specified wavelength using Angstrom turbidity model.

atmosphere.angstrom_alpha(aod1, lambda1, …)

Calculate Angstrom alpha exponent.

Irradiance

Methods for irradiance calculations

pvsystem.PVSystem.get_irradiance(…[, …])

Uses the irradiance.get_total_irradiance() function to calculate the plane of array irradiance components on a tilted surface defined by self.surface_tilt, self.surface_azimuth, and self.albedo.

pvsystem.PVSystem.get_aoi(solar_zenith, …)

Get the angle of incidence on the system.

tracking.SingleAxisTracker.get_irradiance(…)

Uses the irradiance.get_total_irradiance() function to calculate the plane of array irradiance components on a tilted surface defined by the input data and self.albedo.

Decomposing and combining irradiance

irradiance.get_extra_radiation(datetime_or_doy)

Determine extraterrestrial radiation from day of year.

irradiance.aoi(surface_tilt, …)

Calculates the angle of incidence of the solar vector on a surface.

irradiance.aoi_projection(surface_tilt, …)

Calculates the dot product of the sun position unit vector and the surface normal unit vector; in other words, the cosine of the angle of incidence.

irradiance.poa_horizontal_ratio(…)

Calculates the ratio of the beam components of the plane of array irradiance and the horizontal irradiance.

irradiance.beam_component(surface_tilt, …)

Calculates the beam component of the plane of array irradiance.

irradiance.poa_components(aoi, dni, …)

Determine in-plane irradiance components.

irradiance.get_ground_diffuse(surface_tilt, ghi)

Estimate diffuse irradiance from ground reflections given irradiance, albedo, and surface tilt

irradiance.dni(ghi, dhi, zenith[, …])

Determine DNI from GHI and DHI.

Transposition models

irradiance.get_total_irradiance(…[, …])

Determine total in-plane irradiance and its beam, sky diffuse and ground reflected components, using the specified sky diffuse irradiance model.

irradiance.get_sky_diffuse(surface_tilt, …)

Determine in-plane sky diffuse irradiance component using the specified sky diffuse irradiance model.

irradiance.isotropic(surface_tilt, dhi)

Determine diffuse irradiance from the sky on a tilted surface using the isotropic sky model.

irradiance.perez(surface_tilt, …[, model, …])

Determine diffuse irradiance from the sky on a tilted surface using one of the Perez models.

irradiance.haydavies(surface_tilt, …[, …])

Determine diffuse irradiance from the sky on a tilted surface using Hay & Davies’ 1980 model

irradiance.klucher(surface_tilt, …)

Determine diffuse irradiance from the sky on a tilted surface using Klucher’s 1979 model

irradiance.reindl(surface_tilt, …)

Determine diffuse irradiance from the sky on a tilted surface using Reindl’s 1990 model

irradiance.king(surface_tilt, dhi, ghi, …)

Determine diffuse irradiance from the sky on a tilted surface using the King model.

DNI estimation models

irradiance.disc(ghi, solar_zenith, …[, …])

Estimate Direct Normal Irradiance from Global Horizontal Irradiance using the DISC model.

irradiance.dirint(ghi, solar_zenith, times)

Determine DNI from GHI using the DIRINT modification of the DISC model.

irradiance.dirindex(ghi, ghi_clearsky, …)

Determine DNI from GHI using the DIRINDEX model.

irradiance.erbs(ghi, zenith, datetime_or_doy)

Estimate DNI and DHI from GHI using the Erbs model.

irradiance.liujordan(zenith, transmittance, …)

Determine DNI, DHI, GHI from extraterrestrial flux, transmittance, and optical air mass number.

irradiance.gti_dirint(poa_global, aoi, …)

Determine GHI, DNI, DHI from POA global using the GTI DIRINT model.

Clearness index models

irradiance.clearness_index(ghi, …[, …])

Calculate the clearness index.

irradiance.clearness_index_zenith_independent(…)

Calculate the zenith angle independent clearness index.

irradiance.clearsky_index(ghi, clearsky_ghi)

Calculate the clearsky index.

PV Modeling

Classes

The PVSystem class provides many methods that wrap the functions listed below. See its documentation for details.

pvsystem.PVSystem([surface_tilt, …])

The PVSystem class defines a standard set of PV system attributes and modeling functions.

pvsystem.LocalizedPVSystem([pvsystem, location])

The LocalizedPVSystem class defines a standard set of installed PV system attributes and modeling functions.

AOI modifiers

pvsystem.physicaliam(aoi[, n, K, L])

Determine the incidence angle modifier using refractive index, extinction coefficient, and glazing thickness.

pvsystem.ashraeiam(aoi[, b])

Determine the incidence angle modifier using the ASHRAE transmission model.

pvsystem.sapm_aoi_loss(aoi, module[, upper])

Calculates the SAPM angle of incidence loss coefficient, F2.

PV temperature models

temperature.sapm_cell(poa_global, temp_air, …)

Calculate cell temperature per the Sandia PV Array Performance Model [1].

temperature.sapm_module(poa_global, …)

Calculate module back surface temperature per the Sandia PV Array Performance Model [1].

temperature.pvsyst_cell(poa_global, temp_air)

Calculate cell temperature using an empirical heat loss factor model as implemented in PVsyst.

Single diode models

Functions relevant for single diode models.

pvsystem.calcparams_cec(…[, EgRef, dEgdT, …])

Calculates five parameter values for the single diode equation at effective irradiance and cell temperature using the CEC model described in [1].

pvsystem.calcparams_desoto(…[, EgRef, …])

Calculates five parameter values for the single diode equation at effective irradiance and cell temperature using the De Soto et al.

pvsystem.calcparams_pvsyst(…[, R_sh_exp, …])

Calculates five parameter values for the single diode equation at effective irradiance and cell temperature using the PVsyst v6 model described in [1,2,3].

pvsystem.i_from_v(resistance_shunt, …[, …])

Device current at the given device voltage for the single diode model.

pvsystem.singlediode(photocurrent, …[, …])

Solve the single-diode model to obtain a photovoltaic IV curve.

pvsystem.v_from_i(resistance_shunt, …[, …])

Device voltage at the given device current for the single diode model.

pvsystem.max_power_point(photocurrent, …)

Given the single diode equation coefficients, calculates the maximum power point (MPP).

Low-level functions for solving the single diode equation.

singlediode.estimate_voc(photocurrent, …)

Rough estimate of open circuit voltage useful for bounding searches for i of v when using singlediode().

singlediode.bishop88(diode_voltage, …[, …])

Explicit calculation of points on the IV curve described by the single diode equation [1]_.

singlediode.bishop88_i_from_v(voltage, …)

Find current given any voltage.

singlediode.bishop88_v_from_i(current, …)

Find voltage given any current.

singlediode.bishop88_mpp(photocurrent, …)

Find max power point.

SAPM model

Functions relevant for the SAPM model.

pvsystem.sapm(effective_irradiance, …)

The Sandia PV Array Performance Model (SAPM) generates 5 points on a PV module’s I-V curve (Voc, Isc, Ix, Ixx, Vmp/Imp) according to SAND2004-3535.

pvsystem.sapm_effective_irradiance(…[, …])

Calculates the SAPM effective irradiance using the SAPM spectral loss and SAPM angle of incidence loss functions.

pvsystem.sapm_spectral_loss(…)

Calculates the SAPM spectral loss coefficient, F1.

pvsystem.sapm_aoi_loss(aoi, module[, upper])

Calculates the SAPM angle of incidence loss coefficient, F2.

pvsystem.snlinverter(v_dc, p_dc, inverter)

Converts DC power and voltage to AC power using Sandia’s Grid-Connected PV Inverter model.

temperature.sapm_cell(poa_global, temp_air, …)

Calculate cell temperature per the Sandia PV Array Performance Model [1].

Pvsyst model

Functions relevant for the Pvsyst model.

temperature.pvsyst_cell(poa_global, temp_air)

Calculate cell temperature using an empirical heat loss factor model as implemented in PVsyst.

PVWatts model

pvsystem.pvwatts_dc(g_poa_effective, …[, …])

Implements NREL’s PVWatts DC power model [1]_:

pvsystem.pvwatts_ac(pdc, pdc0[, …])

Implements NREL’s PVWatts inverter model [1]_.

pvsystem.pvwatts_losses([soiling, shading, …])

Implements NREL’s PVWatts system loss model [1]_:

pvsystem.pvwatts_losses([soiling, shading, …])

Implements NREL’s PVWatts system loss model [1]_:

Functions for fitting PV models

ivtools.fit_sde_sandia(voltage, current[, …])

Fits the single diode equation (SDE) to an IV curve.

ivtools.fit_sdm_cec_sam(celltype, v_mp, …)

Estimates parameters for the CEC single diode model (SDM) using the SAM SDK.

Other

pvsystem.retrieve_sam([name, path])

Retrieve latest module and inverter info from a local file or the SAM website.

pvsystem.systemdef(meta, surface_tilt, …)

Generates a dict of system parameters used throughout a simulation.

pvsystem.scale_voltage_current_power(data[, …])

Scales the voltage, current, and power of the DataFrames returned by singlediode() and sapm().

Tracking

SingleAxisTracker

The SingleAxisTracker inherits from PVSystem.

tracking.SingleAxisTracker([axis_tilt, …])

Inherits the PV modeling methods from PVSystem.

tracking.SingleAxisTracker.singleaxis(…)

Get tracking data.

tracking.SingleAxisTracker.get_irradiance(…)

Uses the irradiance.get_total_irradiance() function to calculate the plane of array irradiance components on a tilted surface defined by the input data and self.albedo.

tracking.SingleAxisTracker.localize([…])

Creates a LocalizedSingleAxisTracker object using this object and location data.

tracking.LocalizedSingleAxisTracker([…])

The LocalizedSingleAxisTracker class defines a standard set of installed PV system attributes and modeling functions.

Functions

tracking.singleaxis(apparent_zenith, …[, …])

Determine the rotation angle of a single axis tracker using the equations in [1] when given a particular sun zenith and azimuth angle.

IO Tools

Functions for reading and writing data from a variety of file formats relevant to solar energy modeling.

iotools.read_tmy2(filename)

Read a TMY2 file in to a DataFrame.

iotools.read_tmy3([filename, coerce_year, …])

Read a TMY3 file in to a pandas dataframe.

iotools.read_epw(filename[, coerce_year])

Read an EPW file in to a pandas dataframe.

iotools.read_srml(filename)

Read University of Oregon SRML[1] 1min .tsv file into pandas dataframe.

iotools.read_srml_month_from_solardat(…[, …])

Request a month of SRML[1] data from solardat and read it into a Dataframe.

iotools.read_surfrad(filename[, map_variables])

Read in a daily NOAA SURFRAD[1] file.

iotools.read_midc(filename[, variable_map, …])

Read in National Renewable Energy Laboratory Measurement and Instrumentation Data Center [1]_ weather data.

iotools.read_midc_raw_data_from_nrel(site, …)

Request and read MIDC data directly from the raw data api.

iotools.read_ecmwf_macc(filename, latitude, …)

Read data from ECMWF MACC reanalysis netCDF4 file.

iotools.get_ecmwf_macc(filename, params, …)

Download data from ECMWF MACC Reanalysis API.

iotools.read_crn(filename)

Read NOAA USCRN [1]_ [2]_ fixed-width file into pandas dataframe.

iotools.read_solrad(filename)

Read NOAA SOLRAD [1]_ [2]_ fixed-width file into pandas dataframe.

iotools.get_psm3(latitude, longitude, …[, …])

Get PSM3 data

A Location object may be created from metadata in some files.

location.Location.from_tmy(tmy_metadata[, …])

Create an object based on a metadata dictionary from tmy2 or tmy3 data readers.

TMY

Warning

The pvlib.tmy module is deprecated; it will be removed in pvlib 0.7. Please see the pvlib.iotools package.

Methods and functions for reading data from TMY files.

location.Location.from_tmy(tmy_metadata[, …])

Create an object based on a metadata dictionary from tmy2 or tmy3 data readers.

tmy.readtmy2

tmy.readtmy3

Forecasting

Forecast models

forecast.GFS([resolution, set_type])

Subclass of the ForecastModel class representing GFS forecast model.

forecast.NAM([set_type])

Subclass of the ForecastModel class representing NAM forecast model.

forecast.RAP([resolution, set_type])

Subclass of the ForecastModel class representing RAP forecast model.

forecast.HRRR([set_type])

Subclass of the ForecastModel class representing HRRR forecast model.

forecast.HRRR_ESRL([set_type])

Subclass of the ForecastModel class representing NOAA/GSD/ESRL’s HRRR forecast model.

forecast.NDFD([set_type])

Subclass of the ForecastModel class representing NDFD forecast model.

Getting data

forecast.ForecastModel.get_data(latitude, …)

Submits a query to the UNIDATA servers using Siphon NCSS and converts the netcdf data to a pandas DataFrame.

forecast.ForecastModel.get_processed_data(…)

Get and process forecast data.

Processing data

forecast.ForecastModel.process_data(data, …)

Defines the steps needed to convert raw forecast data into processed forecast data.

forecast.ForecastModel.rename(data[, variables])

Renames the columns according the variable mapping.

forecast.ForecastModel.cloud_cover_to_ghi_linear(…)

Convert cloud cover to GHI using a linear relationship.

forecast.ForecastModel.cloud_cover_to_irradiance_clearsky_scaling(…)

Estimates irradiance from cloud cover in the following steps:

forecast.ForecastModel.cloud_cover_to_transmittance_linear(…)

Convert cloud cover to atmospheric transmittance using a linear model.

forecast.ForecastModel.cloud_cover_to_irradiance_liujordan(…)

Estimates irradiance from cloud cover in the following steps:

forecast.ForecastModel.cloud_cover_to_irradiance(…)

Convert cloud cover to irradiance.

forecast.ForecastModel.kelvin_to_celsius(…)

Converts Kelvin to celsius.

forecast.ForecastModel.isobaric_to_ambient_temperature(data)

Calculates temperature from isobaric temperature.

forecast.ForecastModel.uv_to_speed(data)

Computes wind speed from wind components.

forecast.ForecastModel.gust_to_speed(data[, …])

Computes standard wind speed from gust.

IO support

These are public for now, but use at your own risk.

forecast.ForecastModel.set_dataset()

Retrieves the designated dataset, creates NCSS object, and creates a NCSS query object.

forecast.ForecastModel.set_query_latlon()

Sets the NCSS query location latitude and longitude.

forecast.ForecastModel.set_location(time, …)

Sets the location for the query.

forecast.ForecastModel.set_time(time)

Converts time data into a pandas date object.

ModelChain

Creating a ModelChain object.

modelchain.ModelChain(system, location[, …])

The ModelChain class to provides a standardized, high-level interface for all of the modeling steps necessary for calculating PV power from a time series of weather inputs.

Running

Running a ModelChain.

modelchain.ModelChain.run_model(weather[, times])

Run the model.

modelchain.ModelChain.complete_irradiance(weather)

Determine the missing irradiation columns.

modelchain.ModelChain.prepare_inputs(weather)

Prepare the solar position, irradiance, and weather inputs to the model.

Attributes

Simple ModelChain attributes:

system, location, clearsky_model, transposition_model, solar_position_method, airmass_model

Functions

Functions for power modeling.

modelchain.basic_chain(times, latitude, …)

An experimental function that computes all of the modeling steps necessary for calculating power or energy for a PV system at a given location.

modelchain.get_orientation(strategy, **kwargs)

Determine a PV system’s surface tilt and surface azimuth using a named strategy.

Bifacial

Methods for calculating back surface irradiance

bifacial.pvfactors_timeseries(solar_azimuth, …)

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.

Comparison with PVLIB MATLAB

PVLIB was originally developed as a library for MATLAB at Sandia National Lab, and Sandia remains the official maintainer of the MATLAB library. Sandia supported the initial Python port and then released further project maintenance and development to the pvlib-python maintainers.

The pvlib-python maintainers collaborate with the PVLIB MATLAB maintainers but operate independently. We’d all like to keep the core functionality of the Python and MATLAB projects synchronized, but this will require the efforts of the larger pvlib-python community, not just the maintainers. Therefore, do not expect feature parity between the libaries, only similarity.

The PV_LIB Matlab help webpage is a good reference for this comparison.

Missing functions

See pvlib-python GitHub issue #2 for a list of functions missing from the Python version of the library.

Major differences

  • pvlib-python uses git version control to track all changes to the code. A summary of changes is included in the whatsnew file for each release. PVLIB MATLAB documents changes in Changelog.docx

  • pvlib-python has a comprehensive test suite, whereas PVLIB MATLAB does not have a test suite at all. Specifically, pvlib-python

    • Uses TravisCI for automated testing on Linux.

    • Uses Appveyor for automated testing on Windows.

    • Uses Coveralls to measure test coverage.

  • Using readthedocs for automated documentation building and hosting.

  • Removed pvl_ from module/function names.

  • Consolidated similar functions into topical modules. For example, functions from pvl_clearsky_ineichen.m and pvl_clearsky_haurwitz.m have been consolidated into clearsky.py.

  • PVLIB MATLAB uses location structs as the input to some functions. pvlib-python just uses the lat, lon, etc. as inputs to the functions. Furthermore, pvlib-python replaces the structs with classes, and these classes have methods, such as get_solarposition(), that automatically reference the appropriate data. See Modeling paradigms for more information.

  • pvlib-python implements a handful of class designed to simplify the PV modeling process. These include Location, PVSystem, LocalizedPVSystem, SingleAxisTracker, and ModelChain.

Other differences

  • Very few tests of input validitity exist in the Python code. We believe that the vast majority of these tests were not necessary. We also make use of Python’s robust support for raising and catching exceptions.

  • Removed unnecessary and sometimes undesired behavior such as setting maximum zenith=90 or airmass=0. Instead, we make extensive use of nan values in returned arrays.

  • Implemented the NREL solar position calculation algorithm. Also added a PyEphem option to solar position calculations.

  • Specify time zones using a string from the standard IANA Time Zone Database naming conventions or using a pytz.timezone instead of an integer GMT offset.

  • clearsky.ineichen supports interpolating monthly Linke Turbidities to daily resolution.

  • Instead of requiring effective irradiance as an input, pvsystem.sapm calculates and returns it based on input POA irradiance, AM, and AOI.

  • pvlib-python does not come with as much example data.

  • pvlib-python does not currently implement as many algorithms as PVLIB MATLAB.

Documentation

  • Using Sphinx to build the documentation, including dynamically created inline examples.

  • Additional Jupyter tutorials in /docs/tutorials.

Variables and Symbols

There is a convention on consistent variable names throughout the library:

List of used Variables and Parameters

variable

description

tz

timezone

latitude

latitude

longitude

longitude

dni

direct normal irradiance

dni_extra

direct normal irradiance at top of atmosphere (extraterrestrial)

dhi

diffuse horizontal irradiance

ghi

global horizontal irradiance

aoi

angle of incidence between \(90\deg\) and \(90\deg\)

aoi_projection

cos(aoi)

airmass

airmass

airmass_relative

relative airmass

airmass_absolute

absolute airmass

poa_ground_diffuse

in plane ground reflected irradiation

poa_direct

direct/beam irradiation in plane

poa_diffuse

total diffuse irradiation in plane. sum of ground and sky diffuse.

poa_global

global irradiation in plane. sum of diffuse and beam projection.

poa_sky_diffuse

diffuse irradiation in plane from scattered light in the atmosphere (without ground reflected irradiation)

g_poa_effective

broadband plane of array effective irradiance.

surface_tilt

tilt angle of the surface

surface_azimuth

azimuth angle of the surface

solar_zenith

zenith angle of the sun in degrees

apparent_zenith

refraction-corrected solar zenith angle in degrees

solar_azimuth

azimuth angle of the sun in degrees East of North

temp_cell

temperature of the cell

temp_module

temperature of the module

temp_air

temperature of the air

temp_dew

dewpoint temperature

relative_humidity

relative humidity

v_mp, i_mp, p_mp

module voltage, current, power at the maximum power point

v_oc

open circuit module voltage

i_sc

short circuit module current

i_x, i_xx

Sandia Array Performance Model IV curve parameters

effective_irradiance

effective irradiance

photocurrent

photocurrent

saturation_current

diode saturation current

resistance_series

series resistance

resistance_shunt

shunt resistance

transposition_factor

the gain ratio of the radiation on inclined plane to global horizontal irradiation: \(\frac{poa\_global}{ghi}\)

pdc0

nameplate DC rating

pdc, dc

dc power

gamma_pdc

module temperature coefficient. Typically in units of 1/C.

pac, ac

ac powe.

eta_inv

inverter efficiency

eta_inv_ref

reference inverter efficiency

eta_inv_nom

nominal inverter efficiency

For a definition and further explanation on the variables, common symbols and units refer to the following sources:

Note

These further references might not use the same terminology as pvlib. But the physical process referred to is the same.

Single Diode Equation

This section reviews the solutions to the single diode equation used in pvlib-python to generate an IV curve of a PV module.

pvlib-python supports two ways to solve the single diode equation:

  1. Lambert W-Function

  2. Bishop’s Algorithm

The pvlib.pvsystem.singlediode() function allows the user to choose the method using the method keyword.

Lambert W-Function

When method='lambertw', the Lambert W-function is used as previously shown by Jain, Kapoor [1, 2] and Hansen [3]. The following algorithm can be found on Wikipedia: Theory of Solar Cells, given the basic single diode model equation.

\[I = I_L - I_0 \left(\exp \left(\frac{V + I R_s}{n Ns V_{th}} \right) - 1 \right) - \frac{V + I R_s}{R_{sh}}\]

Lambert W-function is the inverse of the function \(f \left( w \right) = w \exp \left( w \right)\) or \(w = f^{-1} \left( w \exp \left( w \right) \right)\) also given as \(w = W \left( w \exp \left( w \right) \right)\). Defining the following parameter, \(z\), is necessary to transform the single diode equation into a form that can be expressed as a Lambert W-function.

\[z = \frac{R_s I_0}{n Ns V_{th} \left(1 + \frac{R_s}{R_{sh}} \right)} \exp \left( \frac{R_s \left( I_L + I_0 \right) + V}{n Ns V_{th} \left(1 + \frac{R_s}{R_{sh}}\right)} \right)\]

Then the module current can be solved using the Lambert W-function, \(W \left(z \right)\).

\[I = \frac{I_L + I_0 - \frac{V}{R_{sh}}}{1 + \frac{R_s}{R_{sh}}} - \frac{n Ns V_{th}}{R_s} W \left(z \right)\]

Bishop’s Algorithm

The function pvlib.singlediode.bishop88() uses an explicit solution [4] that finds points on the IV curve by first solving for pairs \((V_d, I)\) where \(V_d\) is the diode voltage \(V_d = V + I*Rs\). Then the voltage is backed out from \(V_d\). Points with specific voltage, such as open circuit, are located using the bisection search method, brentq, bounded by a zero diode voltage and an estimate of open circuit voltage given by

\[V_{oc, est} = n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1 \right)\]

We know that \(V_d = 0\) corresponds to a voltage less than zero, and we can also show that when \(V_d = V_{oc, est}\), the resulting current is also negative, meaning that the corresponding voltage must be in the 4th quadrant and therefore greater than the open circuit voltage (see proof below). Therefore the entire forward-bias 1st quadrant IV-curve is bounded because \(V_{oc} < V_{oc, est}\), and so a bisection search between 0 and \(V_{oc, est}\) will always find any desired condition in the 1st quadrant including \(V_{oc}\).

\[ \begin{align}\begin{aligned}I = I_L - I_0 \left(\exp \left(\frac{V_{oc, est}}{n Ns V_{th}} \right) - 1 \right) - \frac{V_{oc, est}}{R_{sh}} \newline\\I = I_L - I_0 \left(\exp \left(\frac{n Ns V_{th} \log \left(\frac{I_L}{I_0} + 1 \right)}{n Ns V_{th}} \right) - 1 \right) - \frac{n Ns V_{th} \log \left(\frac{I_L}{I_0} + 1 \right)}{R_{sh}} \newline\\I = I_L - I_0 \left(\exp \left(\log \left(\frac{I_L}{I_0} + 1 \right) \right) - 1 \right) - \frac{n Ns V_{th} \log \left(\frac{I_L}{I_0} + 1 \right)}{R_{sh}} \newline\\I = I_L - I_0 \left(\frac{I_L}{I_0} + 1 - 1 \right) - \frac{n Ns V_{th} \log \left(\frac{I_L}{I_0} + 1 \right)}{R_{sh}} \newline\\I = I_L - I_0 \left(\frac{I_L}{I_0} \right) - \frac{n Ns V_{th} \log \left(\frac{I_L}{I_0} + 1 \right)}{R_{sh}} \newline\\I = I_L - I_L - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1 \right)}{R_{sh}} \newline\\I = - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1 \right)}{R_{sh}}\end{aligned}\end{align} \]

References

[1] “Exact analytical solutions of the parameters of real solar cells using Lambert W-function,” A. Jain, A. Kapoor, Solar Energy Materials and Solar Cells, 81, (2004) pp 269-277. DOI: 10.1016/j.solmat.2003.11.018

[2] “A new method to determine the diode ideality factor of real solar cell using Lambert W-function,” A. Jain, A. Kapoor, Solar Energy Materials and Solar Cells, 85, (2005) 391-396. DOI: 10.1016/j.solmat.2004.05.022

[3] “Parameter Estimation for Single Diode Models of Photovoltaic Modules,” Clifford W. Hansen, Sandia Report SAND2015-2065, 2015 DOI: 10.13140/RG.2.1.4336.7842

[4] “Computer simulation of the effects of electrical mismatches in photovoltaic cell interconnection circuits” JW Bishop, Solar Cell (1988) DOI: 10.1016/0379-6787(88)90059-2

Indices and tables