pvlib-python

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 the most appropriate of:

  • 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.

Specific released versions of pvlib-python can be cited using their Zenodo DOI.

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.

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.DatetimeIndex(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_']

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

In [11]: 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 [12]: system = {'module': module, 'inverter': inverter,
   ....:           'surface_azimuth': 180}
   ....: 

In [13]: energies = {}

In [14]: 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.extraradiation(times)
   ....:     dni_extra = pd.Series(dni_extra, index=times)
   ....:     airmass = pvlib.atmosphere.relativeairmass(solpos['apparent_zenith'])
   ....:     pressure = pvlib.atmosphere.alt2pres(altitude)
   ....:     am_abs = pvlib.atmosphere.absoluteairmass(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.total_irrad(system['surface_tilt'],
   ....:                                                system['surface_azimuth'],
   ....:                                                solpos['apparent_zenith'],
   ....:                                                solpos['azimuth'],
   ....:                                                cs['dni'], cs['ghi'], cs['dhi'],
   ....:                                                dni_extra=dni_extra,
   ....:                                                model='haydavies')
   ....:     temps = pvlib.pvsystem.sapm_celltemp(total_irrad['poa_global'],
   ....:                                          wind_speed, temp_air)
   ....:     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, temps['temp_cell'], module)
   ....:     ac = pvlib.pvsystem.snlinverter(dc['v_mp'], dc['p_mp'], inverter)
   ....:     annual_energy = ac.sum()
   ....:     energies[name] = annual_energy
   ....: 

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

# based on the parameters specified above, these are in W*hrs
In [16]: print(energies.round(0))
Albuquerque      511711.0
Berlin           398047.0
San Francisco    454729.0
Tucson           479080.0
dtype: float64

In [17]: energies.plot(kind='bar', rot=0)
Out[17]: <matplotlib.axes._subplots.AxesSubplot at 0x7f07872ac550>

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

pvlib-python provides a basic_chain() function that implements much of the code above. Use this function with a full understanding of what it is doing internally!

In [19]: from pvlib.modelchain import basic_chain

In [20]: energies = {}

In [21]: for latitude, longitude, name, altitude, timezone in coordinates:
   ....:     dc, ac = basic_chain(naive_times.tz_localize(timezone),
   ....:                          latitude, longitude,
   ....:                          module, inverter,
   ....:                          altitude=altitude,
   ....:                          orientation_strategy='south_at_latitude_tilt')
   ....:     annual_energy = ac.sum()
   ....:     energies[name] = annual_energy
   ....: 

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

# based on the parameters specified above, these are in W*hrs
In [23]: print(energies.round(0))
Albuquerque      511627.0
Berlin           398044.0
San Francisco    454728.0
Tucson           479040.0
dtype: float64

In [24]: energies.plot(kind='bar', rot=0)
Out[24]: <matplotlib.axes._subplots.AxesSubplot at 0x7f07a79c8e80>

In [25]: plt.ylabel('Yearly energy yield (W hr)')
Out[25]: Text(0,0.5,'Yearly energy yield (W hr)')
_images/basic-chain-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.

The following code demonstrates how to use Location, PVSystem, and ModelChain objects to accomplish our system modeling goal:

In [26]: from pvlib.pvsystem import PVSystem

In [27]: from pvlib.location import Location

In [28]: from pvlib.modelchain import ModelChain

In [29]: system = PVSystem(module_parameters=module,
   ....:                   inverter_parameters=inverter)
   ....: 

In [30]: energies = {}

In [31]: for latitude, longitude, name, altitude, timezone in coordinates:
   ....:     location = Location(latitude, longitude, name=name, altitude=altitude,
   ....:                         tz=timezone)
   ....:     mc = ModelChain(system, location,
   ....:                     orientation_strategy='south_at_latitude_tilt')
   ....:     mc.run_model(naive_times.tz_localize(timezone))
   ....:     annual_energy = mc.ac.sum()
   ....:     energies[name] = annual_energy
   ....: 

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

# based on the parameters specified above, these are in W*hrs
In [33]: print(energies.round(0))
Albuquerque      511627.0
Berlin           398044.0
San Francisco    454728.0
Tucson           479040.0
dtype: float64

In [34]: energies.plot(kind='bar', rot=0)
Out[34]: <matplotlib.axes._subplots.AxesSubplot at 0x7f0786f07710>

In [35]: plt.ylabel('Yearly energy yield (W hr)')
Out[35]: 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 following code demonstrates how to use a LocalizedPVSystem object to accomplish our modeling goal:

In [36]: from pvlib.pvsystem import LocalizedPVSystem

In [37]: energies = {}

In [38]: for latitude, longitude, name, altitude, timezone in coordinates:
   ....:     localized_system = LocalizedPVSystem(module_parameters=module,
   ....:                                          inverter_parameters=inverter,
   ....:                                          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'])
   ....:     temps = localized_system.sapm_celltemp(total_irrad['poa_global'],
   ....:                                            wind_speed, temp_air)
   ....:     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, temps['temp_cell'])
   ....:     ac = localized_system.snlinverter(dc['v_mp'], dc['p_mp'])
   ....:     annual_energy = ac.sum()
   ....:     energies[name] = annual_energy
   ....: 

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

# based on the parameters specified above, these are in W*hrs
In [40]: print(energies.round(0))
Albuquerque      511627.0
Berlin           398044.0
San Francisco    454728.0
Tucson           479040.0
dtype: float64

In [41]: energies.plot(kind='bar', rot=0)
Out[41]: <matplotlib.axes._subplots.AxesSubplot at 0x7f07a7b58860>

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

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 our wiki 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 DOE EERE Postdoctoral Fellowship program for support from 2014-2016. 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.

What’s New

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

v0.6.0 (___, 2018)

API Changes

Enhancements

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

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)

Documentation

Testing

Contributors

  • Will Holmgren
  • Yu Cao

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.

If you don’t have Python

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

Anaconda installs cleanly into a single directory, does not require Administrator or root privileges, does not affect other Python installs on your system, or interfere with OSX Frameworks. – The Anaconda Documentation
  1. Install the full Anaconda Scientific Python distribution available at Continuum.io

See the Anaconda FAQ for more information.

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

Install standard release

To install the most recent stable release of pvlib-python in a non-editable way, use conda (recommended if you use the Anaconda Python distribution) or pip (works with any Python distribution):

conda install -c pvlib pvlib

pip install 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: source activate pvlibdev
  3. Install additional packages into your development environment: conda install jupyter ipython matplotlib seaborn 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 /path/to/your/pvlib-python. You remember this path from the clone step, right? It’s probably something like 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 2, 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 source activate pvlibdev (or whatever you named your environment) when you start a new shell or terminal.

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 .

Compatibility

pvlib-python is compatible with Python versions 2.7, 3.4, 3.5 and Pandas versions 0.13.1 or newer.

There have been several problems with Continuum’s Anaconda packages that have impacted pvlib users. The current problems that we’re aware of are listed below:

  1. For Windows + Python 2.7 users: Continuum’s Python 2.7 SciPy 0.16.1, 0.17.0, 0.17.1 packages are not compiled properly and will crash your Python interpreter if you use our Linke turbidity lookup function. See Anaconda issue 650 for more.

Note that our Numba-accelerated solar position algorithms have more specific version requirements that will be resolved by the Numba installer.

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.

Easy ways to contribute

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

  • Make GitHub issues and contribute to the conversation about how to resolve them.
  • Read issues and pull requests that other people created and contribute to the conversation about how to resolve them.
  • 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

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.

Note that 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. This gives everybody an easy way to comment on the code and can make the process more efficient.

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.

The maintainers will follow same procedures, rather than making direct commits to the main repo. Exceptions may be made for extremely minor changes, such as fixing documentation typos.

Testing

pvlib’s unit tests can easily be run by executing py.test on the pvlib directory:

py.test pvlib

or, for a single module:

py.test pvlib/test/test_clearsky.py

While copy/paste coding should generally be avoided, it’s a great way to learn how to write unit tests!

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.

This documentation

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

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
import numpy as np

# pvlib imports
import pvlib

from pvlib.pvsystem import PVSystem
from pvlib.location import Location
from pvlib.modelchain import ModelChain

# load some module and inverter specifications
sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod')
cec_inverters = pvlib.pvsystem.retrieve_sam('cecinverter')

sandia_module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']
cec_inverter = cec_inverters['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 [2]:
location = Location(latitude=32.2, longitude=-110.9)
system = PVSystem(surface_tilt=20, surface_azimuth=200,
                  module_parameters=sandia_module,
                  inverter_parameters=cec_inverter)
mc = ModelChain(system, location)

Printing a ModelChain object will display its models.

In [3]:
print(mc)
ModelChain:
  name: None
  orientation_strategy: None
  clearsky_model: ineichen
  transposition_model: haydavies
  solar_position_method: nrel_numpy
  airmass_model: kastenyoung1989
  dc_model: sapm
  ac_model: snlinverter
  aoi_model: sapm_aoi_loss
  spectral_model: sapm_spectral_loss
  temp_model: sapm_temp
  losses_model: no_extra_losses

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

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

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 [5]:
mc.aoi
Out[5]:
2017-04-01 12:00:00-07:00    15.929176
Name: aoi, dtype: float64
In [6]:
mc.dc
Out[6]:
i_sc i_mp v_oc v_mp p_mp i_x i_xx
2017-04-01 12:00:00-07:00 5.485958 4.860317 52.319047 40.585752 197.259628 5.363079 3.401315
In [7]:
mc.ac
Out[7]:
2017-04-01 12:00:00-07:00    189.915445
dtype: float64

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 [8]:
location = Location(32.2, -110.9)
poorly_specified_system = PVSystem()
print(location)
print(poorly_specified_system)
Location:
  name: None
  latitude: 32.2
  longitude: -110.9
  altitude: 0
  tz: UTC
PVSystem:
  name: None
  surface_tilt: 0
  surface_azimuth: 180
  module: None
  inverter: None
  albedo: 0.25
  racking_model: open_rack_cell_glassback

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 [9]:
ModelChain(poorly_specified_system, location)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-4e9151e6ff63> in <module>()
----> 1 ModelChain(poorly_specified_system, location)

~/checkouts/readthedocs.org/user_builds/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/site-packages/pvlib-0.5.0+38.g38fddbb-py3.6.egg/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, temp_model, losses_model, name, **kwargs)
    315
    316         # calls setters
--> 317         self.dc_model = dc_model
    318         self.ac_model = ac_model
    319         self.aoi_model = aoi_model

~/checkouts/readthedocs.org/user_builds/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/site-packages/pvlib-0.5.0+38.g38fddbb-py3.6.egg/pvlib/modelchain.py in dc_model(self, model)
    369     def dc_model(self, model):
    370         if model is None:
--> 371             self._dc_model = self.infer_dc_model()
    372         elif isinstance(model, str):
    373             model = model.lower()

~/checkouts/readthedocs.org/user_builds/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/site-packages/pvlib-0.5.0+38.g38fddbb-py3.6.egg/pvlib/modelchain.py in infer_dc_model(self)
    392             return self.pvwatts_dc
    393         else:
--> 394             raise ValueError('could not infer DC model from '
    395                              'system.module_parameters')
    396

ValueError: could not infer DC model from system.module_parameters

If our goal is simply to get the object constructed, we can specify the models that the ModelChain should use. We’ll have to fill in missing data on the PVSystem object later, but maybe that’s desirable in some workflows.

In [10]:
mc = ModelChain(poorly_specified_system, location,
                dc_model='singlediode', ac_model='snlinverter',
                aoi_model='physical', spectral_model='no_loss')
print(mc)
ModelChain:
  name: None
  orientation_strategy: None
  clearsky_model: ineichen
  transposition_model: haydavies
  solar_position_method: nrel_numpy
  airmass_model: kastenyoung1989
  dc_model: singlediode
  ac_model: snlinverter
  aoi_model: physical_aoi_loss
  spectral_model: no_spectral_loss
  temp_model: sapm_temp
  losses_model: no_extra_losses

As expected, without additional information, the run_model() method fails at run time.

In [11]:
mc.run_model(times=weather.index, weather=weather)
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-11-669fff3a00c3> in <module>()
----> 1 mc.run_model(times=weather.index, weather=weather)

~/checkouts/readthedocs.org/user_builds/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/site-packages/pvlib-0.5.0+38.g38fddbb-py3.6.egg/pvlib/modelchain.py in run_model(self, times, irradiance, weather)
    847         self.effective_irradiance_model()
    848         self.temp_model()
--> 849         self.dc_model()
    850         self.ac_model()
    851         self.losses_model()

~/checkouts/readthedocs.org/user_builds/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/site-packages/pvlib-0.5.0+38.g38fddbb-py3.6.egg/pvlib/modelchain.py in singlediode(self)
    407          resistance_shunt, nNsVth) = (
    408             self.system.calcparams_desoto(self.effective_irradiance,
--> 409                                           self.temps['temp_cell']))
    410
    411         self.desoto = (photocurrent, saturation_current, resistance_series,

~/checkouts/readthedocs.org/user_builds/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/site-packages/pvlib-0.5.0+38.g38fddbb-py3.6.egg/pvlib/pvsystem.py in calcparams_desoto(self, poa_global, temp_cell, **kwargs)
    302         """
    303         return calcparams_desoto(poa_global, temp_cell,
--> 304                                  self.module_parameters['alpha_sc'],
    305                                  self.module_parameters,
    306                                  self.module_parameters['EgRef'],

KeyError: 'alpha_sc'

The ModelChain attempted to execute the PVSystem object’s singlediode() method, and the method failed because the object’s module_parameters did not include the data necessary to run the model.

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 [12]:
sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter)
mc = ModelChain(system, location)
print(mc)
ModelChain:
  name: None
  orientation_strategy: None
  clearsky_model: ineichen
  transposition_model: haydavies
  solar_position_method: nrel_numpy
  airmass_model: kastenyoung1989
  dc_model: sapm
  ac_model: snlinverter
  aoi_model: sapm_aoi_loss
  spectral_model: sapm_spectral_loss
  temp_model: sapm_temp
  losses_model: no_extra_losses
In [13]:
mc.run_model(times=weather.index, weather=weather)
mc.ac
Out[13]:
2017-04-01 12:00:00-07:00    189.915445
dtype: float64

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 [14]:
pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004})
mc = ModelChain(pvwatts_system, location,
                aoi_model='physical', spectral_model='no_loss')
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
  temp_model: sapm_temp
  losses_model: no_extra_losses
In [15]:
mc.run_model(times=weather.index, weather=weather)
mc.ac
Out[15]:
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 [16]:
sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter)
mc = ModelChain(system, location, aoi_model='physical', spectral_model='no_loss')
print(mc)
ModelChain:
  name: None
  orientation_strategy: None
  clearsky_model: ineichen
  transposition_model: haydavies
  solar_position_method: nrel_numpy
  airmass_model: kastenyoung1989
  dc_model: sapm
  ac_model: snlinverter
  aoi_model: physical_aoi_loss
  spectral_model: no_spectral_loss
  temp_model: sapm_temp
  losses_model: no_extra_losses
In [17]:
mc.run_model(times=weather.index, weather=weather)
mc.ac
Out[17]:
2017-04-01 12:00:00-07:00    191.991429
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 1. A set of methods that wrap and call the PVSystem methods. 1. 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 irradiance (clear sky), temperature (20 C), and wind speed (0 m/s) if these inputs are not provided.

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 [18]:
np.source(mc.run_model)
In file: /home/docs/checkouts/readthedocs.org/user_builds/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/site-packages/pvlib-0.5.0+38.g38fddbb-py3.6.egg/pvlib/modelchain.py

    def run_model(self, times=None, irradiance=None, weather=None):
        """
        Run the model.

        Parameters
        ----------
        times : None or DatetimeIndex, default None
            Times at which to evaluate the model. Can be None if
            attribute `times` is already set.
        irradiance : None or DataFrame
            This parameter is deprecated. Please use `weather` instead.
        weather : None or DataFrame, default None
            If None, assumes air temperature is 20 C, wind speed is 0
            m/s and irradiation calculated from clear sky data. Column
            names must be 'wind_speed', 'temp_air', 'dni', 'ghi', 'dhi'.
            Do not pass incomplete irradiation data. Use method
            :py:meth:`~pvlib.modelchain.ModelChain.complete_irradiance`
            instead.

        Returns
        -------
        self

        Assigns attributes: times, solar_position, airmass, irradiance,
        total_irrad, effective_irradiance, weather, temps, aoi,
        aoi_modifier, spectral_modifier, dc, ac, losses.
        """

        self.prepare_inputs(times, irradiance, weather)
        self.aoi_model()
        self.spectral_model()
        self.effective_irradiance_model()
        self.temp_model()
        self.dc_model()
        self.ac_model()
        self.losses_model()

        return self

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 [19]:
np.source(mc.pvwatts_dc)
In file: /home/docs/checkouts/readthedocs.org/user_builds/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/site-packages/pvlib-0.5.0+38.g38fddbb-py3.6.egg/pvlib/modelchain.py

    def pvwatts_dc(self):
        self.dc = self.system.pvwatts_dc(self.effective_irradiance,
                                         self.temps['temp_cell'])
        return self

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 temps attributes. Then it assigns the result to the dc attribute of the ModelChain object. The code below shows a simple example of this.

In [20]:
# make the objects
pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004})
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.
mc.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')])
mc.temps = pd.DataFrame({'temp_cell': 50, 'temp_module': 50}, index=[pd.Timestamp('20170401 1200-0700')])

# run ModelChain.pvwatts_dc and look at the result
mc.pvwatts_dc()
mc.dc
Out[20]:
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 [21]:
np.source(mc.sapm)
In file: /home/docs/checkouts/readthedocs.org/user_builds/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/site-packages/pvlib-0.5.0+38.g38fddbb-py3.6.egg/pvlib/modelchain.py

    def sapm(self):
        self.dc = self.system.sapm(self.effective_irradiance/1000.,
                                   self.temps['temp_cell'])

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

        return self

In [22]:
# make the objects
sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter)
mc = ModelChain(sapm_system, location)

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

# run ModelChain.sapm and look at the result
mc.sapm()
mc.dc
Out[22]:
i_sc i_mp v_oc v_mp p_mp i_x i_xx
2017-04-01 12:00:00-07:00 5.14168 4.566863 53.8368 42.4284 193.764685 5.025377 3.219662

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 [23]:
pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004})
mc = ModelChain(pvwatts_system, location,
                aoi_model='no_loss', spectral_model='no_loss')
mc.dc_model.__func__
Out[23]:
<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 [24]:
np.source(mc.infer_dc_model)
In file: /home/docs/checkouts/readthedocs.org/user_builds/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/site-packages/pvlib-0.5.0+38.g38fddbb-py3.6.egg/pvlib/modelchain.py

    def infer_dc_model(self):
        params = set(self.system.module_parameters.keys())
        if set(['A0', 'A1', 'C7']) <= params:
            return self.sapm
        elif set(['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s']) <= params:
            return self.singlediode
        elif set(['pdc0', 'gamma_pdc']) <= params:
            return self.pvwatts_dc
        else:
            raise ValueError('could not infer DC model from '
                             'system.module_parameters')

In [25]:
np.source(mc.infer_ac_model)
In file: /home/docs/checkouts/readthedocs.org/user_builds/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/site-packages/pvlib-0.5.0+38.g38fddbb-py3.6.egg/pvlib/modelchain.py

    def infer_ac_model(self):
        inverter_params = set(self.system.inverter_parameters.keys())
        module_params = set(self.system.module_parameters.keys())
        if set(['C0', 'C1', 'C2']) <= inverter_params:
            return self.snlinverter
        elif set(['ADRCoefficients']) <= inverter_params:
            return self.adrinverter
        elif set(['pdc0']) <= module_params:
            return self.pvwatts_inverter
        else:
            raise ValueError('could not infer AC model from '
                             'system.inverter_parameters')

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 [26]:
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)


def pvusa_mc_wrapper(mc):
    # calculate the dc power and assign it to mc.dc
    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
    return mc


def pvusa_ac_mc_wrapper(mc):
    # keep it simple
    mc.ac = mc.dc
    return mc
In [27]:
module_parameters = {'a': 0.2, 'b': 0.00001, 'c': 0.001, 'd': -0.00005}
pvusa_system = PVSystem(module_parameters=module_parameters)

mc = ModelChain(pvusa_system, location,
                dc_model=pvusa_mc_wrapper, ac_model=pvusa_ac_mc_wrapper,
                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 [28]:
mc.dc_model.func
Out[28]:
<function __main__.pvusa_mc_wrapper(mc)>

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

In [29]:
mc.run_model(times=weather.index, weather=weather)
mc.dc
Out[29]:
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]: 591

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/Ujung_Pandang',
 'Atlantic/South_Georgia',
 'Australia/South',
 'Chile/Continental',
 'Etc/GMT+8',
 'Etc/UTC',
 'Europe/Helsinki',
 'Europe/Podgorica',
 'Europe/Vilnius',
 'Indian/Comoro',
 'NZ-CHAT',
 'Pacific/Johnston',
 'Pacific/Tahiti',
 'US/Hawaii']

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/tslib.pyx in pandas._libs.tslib.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/wholmgren-pvlib-python-new/conda/apichangesdoconly/lib/python3.6/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.tmy.readtmy3(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.

The bird_hulstrom80_aod_bb(), and kasten96_lt() functions are useful for calculating inputs to the clear sky functions.

We’ll need these imports for the examples below.

In [1]: import itertools

In [2]: import matplotlib.pyplot as plt

In [3]: import pandas as pd

In [4]: import pvlib

In [5]: from pvlib import clearsky, atmosphere

In [6]: from pvlib.location import Location

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 [7]: tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson')

In [8]: times = pd.DatetimeIndex(start='2016-07-01', end='2016-07-04', freq='1min', tz=tus.tz)

In [9]: cs = tus.get_clearsky(times)  # ineichen with climatology table by default

In [10]: cs.plot();

In [11]: plt.ylabel('Irradiance $W/m^2$');

In [12]: 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 [13]: cs = tus.get_clearsky(times, model='ineichen', linke_turbidity=3)

In [14]: cs.plot();

In [15]: plt.title('Ineichen, linke_turbidity=3');

In [16]: plt.ylabel('Irradiance $W/m^2$');
_images/location-ineichen.png
In [17]: cs = tus.get_clearsky(times, model='simplified_solis', aod700=0.2, precipitable_water=3)

In [18]: cs.plot();

In [19]: plt.title('Simplfied Solis, aod700=0.2, precipitable_water=3');

In [20]: 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 [21]: import calendar

In [22]: import os

In [23]: import tables

In [24]: pvlib_path = os.path.dirname(os.path.abspath(pvlib.clearsky.__file__))

In [25]: filepath = os.path.join(pvlib_path, 'data', 'LinkeTurbidities.h5')

In [26]: 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 [27]: plot_turbidity_map(1)

In [28]: 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 [29]: times = pd.DatetimeIndex(start='2015-01-01', end='2016-01-01', freq='1D')

In [30]: sites = [(32, -111, 'Tucson1'), (32.2, -110.9, 'Tucson2'),
   ....:          (33.5, -112.1, 'Phoenix'), (35.1, -106.6, 'Albuquerque')]
   ....: 

In [31]: plt.figure();

In [32]: for lat, lon, name in sites:
   ....:     turbidity = pvlib.clearsky.lookup_linke_turbidity(times, lat, lon, interp_turbidity=False)
   ....:     turbidity.plot(label=name)
   ....: 

In [33]: plt.legend();

In [34]: plt.title('Raw data (no interpolation)');

In [35]: plt.ylabel('Linke Turbidity');

In [36]: plt.figure();

In [37]: for lat, lon, name in sites:
   ....:     turbidity = pvlib.clearsky.lookup_linke_turbidity(times, lat, lon)
   ....:     turbidity.plot(label=name)
   ....: 

In [38]: plt.legend();

In [39]: plt.title('Interpolated to the day');

In [40]: plt.ylabel('Linke Turbidity');
_images/turbidity-no-interp.png _images/turbidity-yes-interp.png

Examples

A clear sky time series using only basic pvlib functions.

In [41]: latitude, longitude, tz, altitude, name = 32.2, -111, 'US/Arizona', 700, 'Tucson'

In [42]: times = pd.date_range(start='2014-01-01', end='2014-01-02', freq='1Min', tz=tz)

In [43]: solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude)

In [44]: apparent_zenith = solpos['apparent_zenith']

In [45]: airmass = pvlib.atmosphere.relativeairmass(apparent_zenith)

In [46]: pressure = pvlib.atmosphere.alt2pres(altitude)

In [47]: airmass = pvlib.atmosphere.absoluteairmass(airmass, pressure)

In [48]: linke_turbidity = pvlib.clearsky.lookup_linke_turbidity(times, latitude, longitude)

In [49]: dni_extra = pvlib.irradiance.extraradiation(times)

# an input is a pandas Series, so solis is a DataFrame
In [50]: ineichen = clearsky.ineichen(apparent_zenith, airmass, linke_turbidity, altitude, dni_extra)

In [51]: plt.figure();

In [52]: ax = ineichen.plot()

In [53]: ax.set_ylabel('Irradiance $W/m^2$');

In [54]: ax.set_title('Ineichen Clear Sky Model');

In [55]: ax.legend(loc=2);

In [56]: plt.show();
_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 [57]: times = pd.date_range(start='2014-09-01', end='2014-09-02', freq='1Min', tz=tz)

In [58]: solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude)

In [59]: apparent_zenith = solpos['apparent_zenith']

In [60]: airmass = pvlib.atmosphere.relativeairmass(apparent_zenith)

In [61]: pressure = pvlib.atmosphere.alt2pres(altitude)

In [62]: airmass = pvlib.atmosphere.absoluteairmass(airmass, pressure)

In [63]: linke_turbidity = pvlib.clearsky.lookup_linke_turbidity(times, latitude, longitude)

In [64]: print('climatological linke_turbidity = {}'.format(linke_turbidity.mean()))
climatological linke_turbidity = 3.329496820286459

In [65]: dni_extra = pvlib.irradiance.extraradiation(times)

In [66]: linke_turbidities = [linke_turbidity.mean(), 2, 4]

In [67]: fig, axes = plt.subplots(ncols=3, nrows=1, sharex=True, sharey=True, squeeze=True, figsize=(12, 4))

In [68]: axes = axes.flatten()

In [69]: 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 [70]: ax.legend(loc=1);

In [71]: plt.show();
_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 is a function of wavelength, and the Simplified Solis model requires AOD at 700 nm. Models exist to convert AOD between different wavelengths, as well as convert Linke turbidity to AOD and PW [Ine08con], [Ine16].

Examples

A clear sky time series using only basic pvlib functions.

In [72]: latitude, longitude, tz, altitude, name = 32.2, -111, 'US/Arizona', 700, 'Tucson'

In [73]: times = pd.date_range(start='2014-01-01', end='2014-01-02', freq='1Min', tz=tz)

In [74]: solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude)

In [75]: apparent_elevation = solpos['apparent_elevation']

In [76]: aod700 = 0.1

In [77]: precipitable_water = 1

In [78]: pressure = pvlib.atmosphere.alt2pres(altitude)

In [79]: dni_extra = pvlib.irradiance.extraradiation(times)

# an input is a Series, so solis is a DataFrame
In [80]: solis = clearsky.simplified_solis(apparent_elevation, aod700, precipitable_water,
   ....:                                   pressure, dni_extra)
   ....: 

In [81]: ax = solis.plot();

In [82]: ax.set_ylabel('Irradiance $W/m^2$');

In [83]: ax.set_title('Simplified Solis Clear Sky Model');

In [84]: ax.legend(loc=2);

In [85]: plt.show();
_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 [86]: apparent_elevation = pd.Series(np.linspace(-10, 90, 101))

In [87]: aod700 = 0.1

In [88]: precipitable_water = 1

In [89]: pressure = 101325

In [90]: dni_extra = 1364

In [91]: solis = clearsky.simplified_solis(apparent_elevation, aod700,
   ....:                                   precipitable_water, pressure, dni_extra)
   ....: 

In [92]: ax = solis.plot();

In [93]: ax.set_xlabel('Apparent elevation (deg)');

In [94]: ax.set_ylabel('Irradiance $W/m^2$');

In [95]: ax.set_title('Irradiance vs Solar Elevation')
Out[95]: Text(0.5,1,'Irradiance vs Solar Elevation')

In [96]: ax.legend(loc=2);
_images/solis-vs-elevation.png

Grid with a clear sky irradiance for a few PW and AOD values.

In [97]: times = pd.date_range(start='2014-09-01', end='2014-09-02', freq='1Min', tz=tz)

In [98]: solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude)

In [99]: apparent_elevation = solpos['apparent_elevation']

In [100]: pressure = pvlib.atmosphere.alt2pres(altitude)

In [101]: dni_extra = pvlib.irradiance.extraradiation(times)

In [102]: aod700 = [0.01, 0.1]

In [103]: precipitable_water = [0.5, 5]

In [104]: fig, axes = plt.subplots(ncols=2, nrows=2, sharex=True, sharey=True, squeeze=True)

In [105]: axes = axes.flatten()

In [106]: 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))
   .....: 

In [107]: plt.show();
_images/solis-grid.png

Contour plots of irradiance as a function of both PW and AOD.

In [108]: aod700 = np.linspace(0, 0.5, 101)

In [109]: precipitable_water = np.linspace(0, 10, 101)

In [110]: apparent_elevation = 70

In [111]: pressure = 101325

In [112]: dni_extra = 1364

In [113]: aod700, precipitable_water = np.meshgrid(aod700, precipitable_water)

# inputs are arrays, so solis is an OrderedDict
In [114]: solis = clearsky.simplified_solis(apparent_elevation, aod700,
   .....:                                   precipitable_water, pressure,
   .....:                                   dni_extra)
   .....: 

In [115]: n = 15

In [116]: vmin = None

In [117]: vmax = None

In [118]: 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 [119]: plot_solis('ghi')

In [120]: plt.show()

In [121]: plot_solis('dni')

In [122]: plt.show()

In [123]: plot_solis('dhi')

In [124]: plt.show()
_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 [125]: abq = Location(35.04, -106.62, altitude=1619)

In [126]: times = pd.DatetimeIndex(start='2012-04-01 10:30:00', tz='Etc/GMT+7', periods=30, freq='1min')

In [127]: cs = abq.get_clearsky(times)

# scale clear sky data to account for possibility of different turbidity
In [128]: ghi = cs['ghi']*.953

# add a cloud event
In [129]: ghi['2012-04-01 10:42:00':'2012-04-01 10:44:00'] = [500, 300, 400]

# add an overirradiance event
In [130]: ghi['2012-04-01 10:56:00'] = 950

In [131]: fig, ax = plt.subplots()

In [132]: ghi.plot(label='input');

In [133]: cs['ghi'].plot(label='ineichen clear');

In [134]: ax.set_ylabel('Irradiance $W/m^2$');

In [135]: plt.legend(loc=4);

In [136]: plt.show();
_images/detect-clear-ghi.png

Now we run the synthetic data and clear sky estimate through the detect_clearsky() function.

In [137]: clear_samples = clearsky.detect_clearsky(ghi, cs['ghi'], cs.index, 10)

In [138]: fig, ax = plt.subplots()

In [139]: clear_samples.astype(int).plot();

In [140]: 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.

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())
                           Downward_Short-Wave_Radiation_Flux_surface_Mixed_intervals_Average  \
2018-05-25 09:00:00-07:00                                                0.0                    
2018-05-25 12:00:00-07:00                                                0.0                    
2018-05-25 15:00:00-07:00                                              210.0                    
2018-05-25 18:00:00-07:00                                              509.0                    
2018-05-25 21:00:00-07:00                                             1050.0                    

                           Temperature_surface  \
2018-05-25 09:00:00-07:00           288.895081   
2018-05-25 12:00:00-07:00           287.628387   
2018-05-25 15:00:00-07:00           305.414948   
2018-05-25 18:00:00-07:00           323.406494   
2018-05-25 21:00:00-07:00           324.976196   

                           Total_cloud_cover_boundary_layer_cloud_Mixed_intervals_Average  \
2018-05-25 09:00:00-07:00                                                0.0                
2018-05-25 12:00:00-07:00                                                0.0                
2018-05-25 15:00:00-07:00                                                0.0                
2018-05-25 18:00:00-07:00                                                0.0                
2018-05-25 21:00:00-07:00                                                0.0                

                           Total_cloud_cover_convective_cloud  \
2018-05-25 09:00:00-07:00                                 0.0   
2018-05-25 12:00:00-07:00                                 0.0   
2018-05-25 15:00:00-07:00                                 0.0   
2018-05-25 18:00:00-07:00                                 0.0   
2018-05-25 21:00:00-07:00                                 0.0   

                           Total_cloud_cover_entire_atmosphere_Mixed_intervals_Average  \
2018-05-25 09:00:00-07:00                                                2.0             
2018-05-25 12:00:00-07:00                                                1.0             
2018-05-25 15:00:00-07:00                                                0.0             
2018-05-25 18:00:00-07:00                                                1.0             
2018-05-25 21:00:00-07:00                                               30.0             

                           Total_cloud_cover_high_cloud_Mixed_intervals_Average  \
2018-05-25 09:00:00-07:00                                                2.0      
2018-05-25 12:00:00-07:00                                                1.0      
2018-05-25 15:00:00-07:00                                                0.0      
2018-05-25 18:00:00-07:00                                                1.0      
2018-05-25 21:00:00-07:00                                               30.0      

                           Total_cloud_cover_low_cloud_Mixed_intervals_Average  \
2018-05-25 09:00:00-07:00                                                0.0     
2018-05-25 12:00:00-07:00                                                0.0     
2018-05-25 15:00:00-07:00                                                0.0     
2018-05-25 18:00:00-07:00                                                0.0     
2018-05-25 21:00:00-07:00                                                0.0     

                           Total_cloud_cover_middle_cloud_Mixed_intervals_Average  \
2018-05-25 09:00:00-07:00                                                0.0        
2018-05-25 12:00:00-07:00                                                0.0        
2018-05-25 15:00:00-07:00                                                0.0        
2018-05-25 18:00:00-07:00                                                0.0        
2018-05-25 21:00:00-07:00                                                0.0        

                           Wind_speed_gust_surface  \
2018-05-25 09:00:00-07:00                 2.439871   
2018-05-25 12:00:00-07:00                 4.001238   
2018-05-25 15:00:00-07:00                 3.657876   
2018-05-25 18:00:00-07:00                 5.362531   
2018-05-25 21:00:00-07:00                 6.559022   

                           u-component_of_wind_isobaric  \
2018-05-25 09:00:00-07:00                      0.381057   
2018-05-25 12:00:00-07:00                     -0.804331   
2018-05-25 15:00:00-07:00                     -0.379626   
2018-05-25 18:00:00-07:00                      3.340979   
2018-05-25 21:00:00-07:00                      5.422766   

                           v-component_of_wind_isobaric  
2018-05-25 09:00:00-07:00                      2.415139  
2018-05-25 12:00:00-07:00                      3.870647  
2018-05-25 15:00:00-07:00                      2.986392  
2018-05-25 18:00:00-07:00                      2.416624  
2018-05-25 21:00:00-07:00                      3.127595  

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          ghi         dni  \
2018-05-25 09:00:00-07:00  15.745087    2.445016   689.639052  831.381974   
2018-05-25 12:00:00-07:00  14.478394    3.953335  1010.349982  805.027263   
2018-05-25 15:00:00-07:00  32.264954    3.010424   812.095345  852.994377   
2018-05-25 18:00:00-07:00  50.256500    4.123374   220.650851  671.792620   
2018-05-25 21:00:00-07:00  51.826202    6.260051     0.000000    0.000000   

                                  dhi  total_clouds  low_clouds  mid_clouds  \
2018-05-25 09:00:00-07:00  109.843710           2.0         0.0         0.0   
2018-05-25 12:00:00-07:00  223.103697           1.0         0.0         0.0   
2018-05-25 15:00:00-07:00  131.686480           0.0         0.0         0.0   
2018-05-25 18:00:00-07:00   44.319521           1.0         0.0         0.0   
2018-05-25 21:00:00-07:00    0.000000          30.0         0.0         0.0   

                           high_clouds  
2018-05-25 09:00:00-07:00          2.0  
2018-05-25 12:00:00-07:00          1.0  
2018-05-25 15:00:00-07:00          0.0  
2018-05-25 18:00:00-07:00          1.0  
2018-05-25 21:00:00-07:00         30.0  

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          ghi         dni  \
2018-05-25 09:00:00-07:00  15.745087    2.445016   689.639052  831.381974   
2018-05-25 12:00:00-07:00  14.478394    3.953335  1010.349982  805.027263   
2018-05-25 15:00:00-07:00  32.264954    3.010424   812.095345  852.994377   
2018-05-25 18:00:00-07:00  50.256500    4.123374   220.650851  671.792620   
2018-05-25 21:00:00-07:00  51.826202    6.260051     0.000000    0.000000   

                                  dhi  total_clouds  low_clouds  mid_clouds  \
2018-05-25 09:00:00-07:00  109.843710           2.0         0.0         0.0   
2018-05-25 12:00:00-07:00  223.103697           1.0         0.0         0.0   
2018-05-25 15:00:00-07:00  131.686480           0.0         0.0         0.0   
2018-05-25 18:00:00-07:00   44.319521           1.0         0.0         0.0   
2018-05-25 21:00:00-07:00    0.000000          30.0         0.0         0.0   

                           high_clouds  
2018-05-25 09:00:00-07:00          2.0  
2018-05-25 12:00:00-07:00          1.0  
2018-05-25 15:00:00-07:00          0.0  
2018-05-25 18:00:00-07:00          1.0  
2018-05-25 21:00:00-07:00         30.0  

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          ghi         dni  \
2018-05-25 09:00:00-07:00  15.745087    2.445016   689.639052  831.381974   
2018-05-25 12:00:00-07:00  14.478394    3.953335  1010.349982  805.027263   
2018-05-25 15:00:00-07:00  32.264954    3.010424   812.095345  852.994377   
2018-05-25 18:00:00-07:00  50.256500    4.123374   220.650851  671.792620   
2018-05-25 21:00:00-07:00  51.826202    6.260051     0.000000    0.000000   

                                  dhi  total_clouds  low_clouds  mid_clouds  \
2018-05-25 09:00:00-07:00  109.843710           2.0         0.0         0.0   
2018-05-25 12:00:00-07:00  223.103697           1.0         0.0         0.0   
2018-05-25 15:00:00-07:00  131.686480           0.0         0.0         0.0   
2018-05-25 18:00:00-07:00   44.319521           1.0         0.0         0.0   
2018-05-25 21:00:00-07:00    0.000000          30.0         0.0         0.0   

                           high_clouds  
2018-05-25 09:00:00-07:00          2.0  
2018-05-25 12:00:00-07:00          1.0  
2018-05-25 15:00:00-07:00          0.0  
2018-05-25 18:00:00-07:00          1.0  
2018-05-25 21:00:00-07:00         30.0  

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));
   ....: 

In [77]: plt.legend();
_images/ndfd_irrad.png

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 [78]: from pvlib.pvsystem import PVSystem, retrieve_sam

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_']

# model a big tracker for more fun
In [85]: system = SingleAxisTracker(module_parameters=module,
   ....:                            inverter_parameters=inverter,
   ....:                            modules_per_string=15,
   ....:                            strings_per_inverter=300)
   ....: 

# fx is a common abbreviation for forecast
In [86]: fx_model = GFS()

In [87]: fx_data = fx_model.get_processed_data(latitude, longitude, start, end)

# use a ModelChain object to calculate modeling intermediates
In [88]: mc = ModelChain(system, fx_model.location)

# extract relevant data for model chain
In [89]: 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 [90]: mc.total_irrad.plot();

In [91]: plt.ylabel('Plane of array irradiance ($W/m^2$)');

In [92]: plt.legend(loc='best');
_images/poa_irrad.png

…the cell and module temperature…

In [93]: mc.temps.plot();

In [94]: plt.ylabel('Temperature (C)');
_images/pv_temps.png

…and finally AC power…

In [95]: mc.ac.fillna(0).plot();

In [96]: plt.ylim(0, None);

In [97]: 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 :ref:PVSystem:.
modelchain.ModelChain(system, location[, …]) An experimental class that represents all of the modeling steps necessary for calculating power or energy for a PV system at a given location using the SAPM.
pvsystem.LocalizedPVSystem([pvsystem, location]) The LocalizedPVSystem class defines a standard set of installed PV system attributes and modeling functions.
tracking.LocalizedSingleAxisTracker([…]) Highly experimental.

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 [R696e8080435f-1].
spa.calculate_deltat(year, month) Calculate the difference between Terrestrial Dynamical Time (TD) and Universal Time (UT).

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.

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 [Ra31b6ebd1385-1].
clearsky.haurwitz(apparent_zenith) Determine clear sky GHI from 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.absolute_airmass
atmosphere.get_relative_airmass
atmosphere.pres2alt(pressure) Determine altitude from site pressure.
atmosphere.alt2pres(altitude) Determine site pressure from altitude.
atmosphere.pw_gueymard94
atmosphere.spectral_correction_first_solar
atmosphere.spectral_corrrection_sapm
atmosphere.aod_bb_bird_hulstrom80
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, …) 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.total_irrad() 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.total_irrad() 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
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 solar vector and the surface normal.
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
irradiance.get_ground_diffuse

Transposition models

irradiance.get_total_irradiance
irradiance.get_sky_diffuse
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, zenith, datetime_or_doy) Estimate Direct Normal Irradiance from Global Horizontal Irradiance using the DISC model.
irradiance.dirint(ghi, 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, which is a modification of the DIRINT model with information from a clear sky model.
irradiance.erbs(ghi, zenith, 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.

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.iam_physical
pvsystem.iam_ashrae
pvsystem.iam_sapm

Single diode model

Functions relevant for the single diode model.

pvsystem.calcparams_desoto(poa_global, …) Applies the temperature and irradiance corrections to inputs for singlediode.
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.

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.effective_irradiance_sapm
pvsystem.celltemp_sapm
pvsystem.spectral_loss_sapm
pvsystem.iam_sapm
pvsystem.inverter_sapm

PVWatts model

pvsystem.pvwatts_dc(g_poa_effective, …[, …]) Implements NREL’s PVWatts DC power model [R549cc3f4d5f7-1]:
pvsystem.pvwatts_ac(pdc, pdc0[, …]) Implements NREL’s PVWatts inverter model [R8eb68e324bb7-1].
pvsystem.pvwatts_losses([soiling, shading, …]) Implements NREL’s PVWatts system loss model [R7a6c04e4f7d1-1]:

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 :ref:PVSystem:.
tracking.SingleAxisTracker.singleaxis(…)
tracking.SingleAxisTracker.get_irradiance(…) Uses the irradiance.total_irrad() 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([…]) Highly experimental.

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.

TMY

Methods and functions for reading data from TMY files.

Move to IO subpackage

location.Location.from_tmy(tmy_metadata[, …]) Create an object based on a metadata dictionary from tmy2 or tmy3 data readers.
tmy.readtmy2(filename) Read a TMY2 file in to a DataFrame.
tmy.readtmy3([filename, coerce_year, recolumn]) Read a TMY3 file in to a pandas dataframe.

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[, …]) An experimental class that represents all of the modeling steps necessary for calculating power or energy for a PV system at a given location using the SAPM.

Running

Running a ModelChain.

modelchain.ModelChain.run_model([times, …]) Run the model.
modelchain.ModelChain.complete_irradiance([…]) Determine the missing irradiation columns.
modelchain.ModelChain.prepare_inputs([…]) 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.

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.

Indices and tables