Intro Tutorial#

This page contains introductory examples of pvlib python usage.

Modeling paradigms#

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

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

In [1]: import pvlib

In [2]: import pandas as pd

In [3]: import matplotlib.pyplot as plt

# latitude, longitude, name, altitude, timezone
In [4]: coordinates = [
   ...:     (32.2, -111.0, 'Tucson', 700, 'Etc/GMT+7'),
   ...:     (35.1, -106.6, 'Albuquerque', 1500, 'Etc/GMT+7'),
   ...:     (37.8, -122.4, 'San Francisco', 10, 'Etc/GMT+8'),
   ...:     (52.5, 13.4, 'Berlin', 34, 'Etc/GMT-1'),
   ...: ]
   ...: 

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

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

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

In [8]: inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_']

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

In order to retrieve meteorological data for the simulation, we can make use of the IO Tools module. In this example we will be using PVGIS, one of the data sources available, to retrieve a Typical Meteorological Year (TMY) which includes irradiation, temperature and wind speed.

In [10]: tmys = []

In [11]: for location in coordinates:
   ....:     latitude, longitude, name, altitude, timezone = location
   ....:     weather = pvlib.iotools.get_pvgis_tmy(latitude, longitude,
   ....:                                           map_variables=True)[0]
   ....:     weather.index.name = "utc_time"
   ....:     tmys.append(weather)
   ....: 
---------------------------------------------------------------------------
timeout                                   Traceback (most recent call last)
~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/urllib3/connectionpool.py in _make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    448                     # Otherwise it looks like a bug in the code.
--> 449                     six.raise_from(e, None)
    450         except (SocketTimeout, BaseSSLError, SocketError) as e:

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/urllib3/packages/six.py in raise_from(value, from_value)

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/urllib3/connectionpool.py in _make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    443                 try:
--> 444                     httplib_response = conn.getresponse()
    445                 except BaseException as e:

~/.asdf/installs/python/3.7.15/lib/python3.7/http/client.py in getresponse(self)
   1372             try:
-> 1373                 response.begin()
   1374             except ConnectionError:

~/.asdf/installs/python/3.7.15/lib/python3.7/http/client.py in begin(self)
    318         while True:
--> 319             version, status, reason = self._read_status()
    320             if status != CONTINUE:

~/.asdf/installs/python/3.7.15/lib/python3.7/http/client.py in _read_status(self)
    279     def _read_status(self):
--> 280         line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
    281         if len(line) > _MAXLINE:

~/.asdf/installs/python/3.7.15/lib/python3.7/socket.py in readinto(self, b)
    588             try:
--> 589                 return self._sock.recv_into(b)
    590             except timeout:

~/.asdf/installs/python/3.7.15/lib/python3.7/ssl.py in recv_into(self, buffer, nbytes, flags)
   1070                   self.__class__)
-> 1071             return self.read(nbytes, buffer)
   1072         else:

~/.asdf/installs/python/3.7.15/lib/python3.7/ssl.py in read(self, len, buffer)
    928             if buffer is not None:
--> 929                 return self._sslobj.read(len, buffer)
    930             else:

timeout: The read operation timed out

During handling of the above exception, another exception occurred:

ReadTimeoutError                          Traceback (most recent call last)
~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/requests/adapters.py in send(self, request, stream, timeout, verify, cert, proxies)
    498                     retries=self.max_retries,
--> 499                     timeout=timeout,
    500                 )

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/urllib3/connectionpool.py in urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    787             retries = retries.increment(
--> 788                 method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2]
    789             )

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/urllib3/util/retry.py in increment(self, method, url, response, error, _pool, _stacktrace)
    549             if read is False or not self._is_method_retryable(method):
--> 550                 raise six.reraise(type(error), error, _stacktrace)
    551             elif read is not None:

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/urllib3/packages/six.py in reraise(tp, value, tb)
    769                 raise value.with_traceback(tb)
--> 770             raise value
    771         finally:

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/urllib3/connectionpool.py in urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    709                 headers=headers,
--> 710                 chunked=chunked,
    711             )

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/urllib3/connectionpool.py in _make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    450         except (SocketTimeout, BaseSSLError, SocketError) as e:
--> 451             self._raise_timeout(err=e, url=url, timeout_value=read_timeout)
    452             raise

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/urllib3/connectionpool.py in _raise_timeout(self, err, url, timeout_value)
    340             raise ReadTimeoutError(
--> 341                 self, url, "Read timed out. (read timeout=%s)" % timeout_value
    342             )

ReadTimeoutError: HTTPSConnectionPool(host='re.jrc.ec.europa.eu', port=443): Read timed out. (read timeout=30)

During handling of the above exception, another exception occurred:

ReadTimeout                               Traceback (most recent call last)
<ipython-input-11-30c9501a9f2f> in <module>
      2     latitude, longitude, name, altitude, timezone = location
      3     weather = pvlib.iotools.get_pvgis_tmy(latitude, longitude,
----> 4                                           map_variables=True)[0]
      5     weather.index.name = "utc_time"
      6     tmys.append(weather)

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/checkouts/stable/pvlib/iotools/pvgis.py in get_pvgis_tmy(latitude, longitude, outputformat, usehorizon, userhorizon, startyear, endyear, url, map_variables, timeout)
    460     if endyear is not None:
    461         params['endyear'] = endyear
--> 462     res = requests.get(url + 'tmy', params=params, timeout=timeout)
    463     # PVGIS returns really well formatted error messages in JSON for HTTP/1.1
    464     # 400 BAD REQUEST so try to return that if possible, otherwise raise the

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/requests/api.py in get(url, params, **kwargs)
     71     """
     72 
---> 73     return request("get", url, params=params, **kwargs)
     74 
     75 

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/requests/api.py in request(method, url, **kwargs)
     57     # cases, and look like a memory leak in others.
     58     with sessions.Session() as session:
---> 59         return session.request(method=method, url=url, **kwargs)
     60 
     61 

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/requests/sessions.py in request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    585         }
    586         send_kwargs.update(settings)
--> 587         resp = self.send(prep, **send_kwargs)
    588 
    589         return resp

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/requests/sessions.py in send(self, request, **kwargs)
    699 
    700         # Send the request
--> 701         r = adapter.send(request, **kwargs)
    702 
    703         # Total elapsed time of the request (approximately)

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/requests/adapters.py in send(self, request, stream, timeout, verify, cert, proxies)
    576                 raise SSLError(e, request=request)
    577             elif isinstance(e, ReadTimeoutError):
--> 578                 raise ReadTimeout(e, request=request)
    579             elif isinstance(e, _InvalidHeader):
    580                 raise InvalidHeader(e, request=request)

ReadTimeout: HTTPSConnectionPool(host='re.jrc.ec.europa.eu', port=443): Read timed out. (read timeout=30)

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 location, weather in zip(coordinates, tmys):
   ....:     latitude, longitude, name, altitude, timezone = location
   ....:     system['surface_tilt'] = latitude
   ....:     solpos = pvlib.solarposition.get_solarposition(
   ....:         time=weather.index,
   ....:         latitude=latitude,
   ....:         longitude=longitude,
   ....:         altitude=altitude,
   ....:         temperature=weather["temp_air"],
   ....:         pressure=pvlib.atmosphere.alt2pres(altitude),
   ....:     )
   ....:     dni_extra = pvlib.irradiance.get_extra_radiation(weather.index)
   ....:     airmass = pvlib.atmosphere.get_relative_airmass(solpos['apparent_zenith'])
   ....:     pressure = pvlib.atmosphere.alt2pres(altitude)
   ....:     am_abs = pvlib.atmosphere.get_absolute_airmass(airmass, pressure)
   ....:     aoi = pvlib.irradiance.aoi(
   ....:         system['surface_tilt'],
   ....:         system['surface_azimuth'],
   ....:         solpos["apparent_zenith"],
   ....:         solpos["azimuth"],
   ....:     )
   ....:     total_irradiance = pvlib.irradiance.get_total_irradiance(
   ....:         system['surface_tilt'],
   ....:         system['surface_azimuth'],
   ....:         solpos['apparent_zenith'],
   ....:         solpos['azimuth'],
   ....:         weather['dni'],
   ....:         weather['ghi'],
   ....:         weather['dhi'],
   ....:         dni_extra=dni_extra,
   ....:         model='haydavies',
   ....:     )
   ....:     cell_temperature = pvlib.temperature.sapm_cell(
   ....:         total_irradiance['poa_global'],
   ....:         weather["temp_air"],
   ....:         weather["wind_speed"],
   ....:         **temperature_model_parameters,
   ....:     )
   ....:     effective_irradiance = pvlib.pvsystem.sapm_effective_irradiance(
   ....:         total_irradiance['poa_direct'],
   ....:         total_irradiance['poa_diffuse'],
   ....:         am_abs,
   ....:         aoi,
   ....:         module,
   ....:     )
   ....:     dc = pvlib.pvsystem.sapm(effective_irradiance, cell_temperature, module)
   ....:     ac = pvlib.inverter.sandia(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)
Series([], dtype: float64)

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

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_core.py in __call__(self, *args, **kwargs)
    970                     data.columns = label_name
    971 
--> 972         return plot_backend.plot(data, kind=kind, **kwargs)
    973 
    974     __call__.__doc__ = __doc__

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/__init__.py in plot(data, kind, **kwargs)
     69             kwargs["ax"] = getattr(ax, "left_ax", ax)
     70     plot_obj = PLOT_CLASSES[kind](data, **kwargs)
---> 71     plot_obj.generate()
     72     plot_obj.draw()
     73     return plot_obj.result

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in generate(self)
    293         for ax in self.axes:
    294             self._post_plot_logic_common(ax, self.data)
--> 295             self._post_plot_logic(ax, self.data)
    296 
    297     def _args_adjust(self):

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in _post_plot_logic(self, ax, data)
   1526         name = self._get_index_name()
   1527 
-> 1528         s_edge = self.ax_pos[0] - 0.25 + self.lim_offset
   1529         e_edge = self.ax_pos[-1] + 0.25 + self.bar_width + self.lim_offset
   1530 

IndexError: index 0 is out of bounds for axis 0 with size 0

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

Object oriented (Location, Mount, Array, PVSystem, ModelChain)#

The object oriented paradigm uses a model with three main concepts:

  1. System design (modules, inverters etc.) is represented by PVSystem, Array, and FixedMount /SingleAxisTrackerMount objects.

  2. A particular place on the planet is represented by a Location object.

  3. The modeling chain used to calculate power output for a particular system and location is represented by a ModelChain object.

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. ModelChain objects provide convenience methods that can provide default selections for models and can also fill necessary input with modeled data. For example, no air temperature or wind speed data is provided in the input weather DataFrame, so the ModelChain object defaults to 20 C and 0 m/s. Also, no irradiance transposition model is specified (keyword argument transposition_model for ModelChain) so the ModelChain defaults to the haydavies model. In this example, ModelChain infers the DC power model from the module provided by examining the parameters defined for the module.

In [19]: from pvlib.pvsystem import PVSystem, Array, FixedMount

In [20]: from pvlib.location import Location

In [21]: from pvlib.modelchain import ModelChain

In [22]: energies = {}

In [23]: for location, weather in zip(coordinates, tmys):
   ....:     latitude, longitude, name, altitude, timezone = location
   ....:     location = Location(
   ....:         latitude,
   ....:         longitude,
   ....:         name=name,
   ....:         altitude=altitude,
   ....:         tz=timezone,
   ....:     )
   ....:     mount = FixedMount(surface_tilt=latitude, surface_azimuth=180)
   ....:     array = Array(
   ....:         mount=mount,
   ....:         module_parameters=module,
   ....:         temperature_model_parameters=temperature_model_parameters,
   ....:     )
   ....:     system = PVSystem(arrays=[array], inverter_parameters=inverter)
   ....:     mc = ModelChain(system, location)
   ....:     mc.run_model(weather)
   ....:     annual_energy = mc.results.ac.sum()
   ....:     energies[name] = annual_energy
   ....: 

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

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

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

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_core.py in __call__(self, *args, **kwargs)
    970                     data.columns = label_name
    971 
--> 972         return plot_backend.plot(data, kind=kind, **kwargs)
    973 
    974     __call__.__doc__ = __doc__

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/__init__.py in plot(data, kind, **kwargs)
     69             kwargs["ax"] = getattr(ax, "left_ax", ax)
     70     plot_obj = PLOT_CLASSES[kind](data, **kwargs)
---> 71     plot_obj.generate()
     72     plot_obj.draw()
     73     return plot_obj.result

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in generate(self)
    293         for ax in self.axes:
    294             self._post_plot_logic_common(ax, self.data)
--> 295             self._post_plot_logic(ax, self.data)
    296 
    297     def _args_adjust(self):

~/checkouts/readthedocs.org/user_builds/mikofski-pvlib-python/envs/stable/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in _post_plot_logic(self, ax, data)
   1526         name = self._get_index_name()
   1527 
-> 1528         s_edge = self.ax_pos[0] - 0.25 + self.lim_offset
   1529         e_edge = self.ax_pos[-1] + 0.25 + self.bar_width + self.lim_offset
   1530 

IndexError: index 0 is out of bounds for axis 0 with size 0

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