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 flexibility 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 has three components:

  • a PVSystem object, representing a collection of modules and inverters

  • a Location object, representing a location on the planet

  • values for attributes that specify the model to be used for for each step in the PV modeling process.

Modeling with a ModelChain typically involves 3 steps:

  1. Creating an instance of ModelChain.

  2. Executing a ModelChain.run_model method with weather data as input. See Running for a list of run_model methods.

  3. Examining the model results that are stored in the ModelChain’s ModelChain.results attribute.

A simple ModelChain example

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

In [1]: import pandas as pd

In [2]: import numpy as np

# pvlib imports
In [3]: import pvlib

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

In [5]: from pvlib.location import Location

In [6]: from pvlib.modelchain import ModelChain

In [7]: from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS

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

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

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

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

In [12]: cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_']

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

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

In [14]: system = PVSystem(surface_tilt=20, surface_azimuth=200,
   ....:                   module_parameters=sandia_module,
   ....:                   inverter_parameters=cec_inverter,
   ....:                   temperature_model_parameters=temperature_model_parameters)
   ....: 

In [15]: mc = ModelChain(system, location)

Printing a ModelChain object will display its models.

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

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

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

In [18]: mc.run_model(weather)
Out[18]: 
ModelChain: 
  name: None
  clearsky_model: ineichen
  transposition_model: haydavies
  solar_position_method: nrel_numpy
  airmass_model: kastenyoung1989
  dc_model: sapm
  ac_model: sandia_inverter
  aoi_model: sapm_aoi_loss
  spectral_model: sapm_spectral_loss
  temperature_model: sapm_temp
  losses_model: no_extra_losses

ModelChain stores the modeling results in the results attribute. The results attribute is an instance of ModelChainResult. A few examples of attributes of ModelChainResult are shown below.

In [19]: mc.results.aoi
Out[19]: 
2017-04-01 12:00:00-07:00    15.929553
Name: aoi, dtype: float64
In [20]: mc.results.cell_temperature
Out[20]: 
2017-04-01 12:00:00-07:00    58.087879
dtype: float64
In [21]: mc.results.dc
Out[21]: 
                               i_sc      i_mp  ...       i_x      i_xx
2017-04-01 12:00:00-07:00  5.485953  4.860313  ...  5.363074  3.401312

[1 rows x 7 columns]
In [22]: mc.results.ac
Out[22]: 
2017-04-01 12:00:00-07:00    189.990907
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 [23]: location = Location(32.2, -110.9)

In [24]: poorly_specified_system = PVSystem()

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

In [26]: print(poorly_specified_system)
PVSystem:
  name: None
  Array:
    name: None
    mount: FixedMount(surface_tilt=0, surface_azimuth=180, racking_model=None, module_height=None)
    module: None
    albedo: 0.25
    module_type: None
    temperature_model_parameters: {}
    strings: 1
    modules_per_string: 1
  inverter: None

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

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

~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/stable/pvlib/modelchain.py in __init__(self, system, location, clearsky_model, transposition_model, solar_position_method, airmass_model, dc_model, ac_model, aoi_model, spectral_model, temperature_model, dc_ohmic_model, losses_model, name)
    481 
    482         # calls setters
--> 483         self.dc_model = dc_model
    484         self.ac_model = ac_model
    485         self.aoi_model = aoi_model

~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/stable/pvlib/modelchain.py in __setattr__(self, key, value)
    511             setattr(self.results, key, value)
    512         else:
--> 513             super().__setattr__(key, value)
    514 
    515     @classmethod

~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/stable/pvlib/modelchain.py in dc_model(self, model)
    688         # guess at model if None
    689         if model is None:
--> 690             self._dc_model, model = self.infer_dc_model()
    691 
    692         # Set model and validate parameters

~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/stable/pvlib/modelchain.py in infer_dc_model(self)
    737         else:
    738             raise ValueError(
--> 739                 'Could not infer DC model from the module_parameters '
    740                 'attributes of system.arrays. Check the module_parameters '
    741                 'attributes or explicitly set the model with the dc_model '

ValueError: Could not infer DC model from the module_parameters attributes of system.arrays. Check the module_parameters attributes or explicitly set the model with the dc_model keyword argument.

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

In [28]: sapm_system = PVSystem(
   ....:     module_parameters=sandia_module,
   ....:     inverter_parameters=cec_inverter,
   ....:     temperature_model_parameters=temperature_model_parameters)
   ....: 

In [29]: mc = ModelChain(sapm_system, location)

In [30]: print(mc)
ModelChain: 
  name: None
  clearsky_model: ineichen
  transposition_model: haydavies
  solar_position_method: nrel_numpy
  airmass_model: kastenyoung1989
  dc_model: sapm
  ac_model: sandia_inverter
  aoi_model: sapm_aoi_loss
  spectral_model: sapm_spectral_loss
  temperature_model: sapm_temp
  losses_model: no_extra_losses
In [31]: mc.run_model(weather);

In [32]: mc.results.ac
Out[32]: 
2017-04-01 12:00:00-07:00    176.649413
dtype: float64

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

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

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

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

In [37]: mc.results.ac
Out[37]: 
2017-04-01 12:00:00-07:00    198.519201
Name: p_mp, dtype: float64

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

In [38]: sapm_system = PVSystem(
   ....:     module_parameters=sandia_module,
   ....:     inverter_parameters=cec_inverter,
   ....:     temperature_model_parameters=temperature_model_parameters)
   ....: 

In [39]: mc = ModelChain(sapm_system, location, aoi_model='physical', spectral_model='no_loss')

In [40]: print(mc)
ModelChain: 
  name: None
  clearsky_model: ineichen
  transposition_model: haydavies
  solar_position_method: nrel_numpy
  airmass_model: kastenyoung1989
  dc_model: sapm
  ac_model: sandia_inverter
  aoi_model: physical_aoi_loss
  spectral_model: no_spectral_loss
  temperature_model: sapm_temp
  losses_model: no_extra_losses
In [41]: mc.run_model(weather);

In [42]: mc.results.ac
Out[42]: 
2017-04-01 12:00:00-07:00    177.381377
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 chain.

As a convenience, ModelChain includes two class methods that return a ModelChain with models selected to be consistent with named PV system models:

Each “with” method returns a ModelChain using a Location and PVSystem. Parameters used to define the PVSystem need to be consistent with the models specified by the “with” method. Using location and sapm_system defined above:

In [43]: mc = mc.with_sapm(sapm_system, location)

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

In [45]: mc.run_model(weather)
Out[45]: 
ModelChain: 
  name: None
  clearsky_model: ineichen
  transposition_model: haydavies
  solar_position_method: nrel_numpy
  airmass_model: kastenyoung1989
  dc_model: sapm
  ac_model: sandia_inverter
  aoi_model: sapm_aoi_loss
  spectral_model: sapm_spectral_loss
  temperature_model: sapm_temp
  losses_model: no_extra_losses

In [46]: mc.results.dc
Out[46]: 
                             i_sc      i_mp  ...       i_x     i_xx
2017-04-01 12:00:00-07:00  5.0333  4.466561  ...  4.919042  3.16318

[1 rows x 7 columns]

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

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

  3. A set of methods that can inspect user-supplied objects to infer the appropriate model when a model isn’t specified by the user.

run_model methods

ModelChain provides three methods for executing the chain of models. The methods allow for simulating the output of the PVSystem with different input data:

  • run_model(), use when weather contains global horizontal, direct and diffuse horizontal irradiance ('ghi', 'dni' and 'dhi').

  • run_model_from_poa(), use when weather broadband direct, diffuse and total irradiance in the plane of array ('poa_global', 'poa_direct', 'poa_diffuse').

  • run_model_from_effective_irradiance(), use when weather contains spectrally- and reflection-adjusted total irradiance in the plane of array (‘effective_irradiance’).

To illustrate the use of the run_model method, assume that a user has GHI and DHI. prepare_inputs() requires all three irradiance components (GHI, DNI, and DHI). In this case, the user needs to calculate DNI before using run_model. 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. See also DNI estimation models for methods and functions that can help fully define the irradiance inputs.

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. 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 generic names, as described in the next section.

In [47]: mc.run_model??
Signature: mc.run_model(weather)
Source:   
    def run_model(self, weather):
        """
        Run the model chain starting with broadband global, diffuse and/or
        direct irradiance.

        Parameters
        ----------
        weather : DataFrame, or tuple or list of DataFrame
            Irradiance column names must include ``'dni'``, ``'ghi'``, and
            ``'dhi'``. If optional columns ``'temp_air'`` and ``'wind_speed'``
            are not provided, air temperature of 20 C and wind speed of 0 m/s
            are added to the DataFrame. If optional column
            ``'cell_temperature'`` is provided, these values are used instead
            of `temperature_model`. If optional column `module_temperature`
            is provided, `temperature_model` must be ``'sapm'``.

            If list or tuple, must be of the same length and order as the
            Arrays of the ModelChain's PVSystem.

        Returns
        -------
        self

        Raises
        ------
        ValueError
            If the number of DataFrames in `data` is different than the number
            of Arrays in the PVSystem.
        ValueError
            If the DataFrames in `data` have different indexes.

        Notes
        -----
        Assigns attributes to ``results``: ``times``, ``weather``,
        ``solar_position``, ``airmass``, ``total_irrad``, ``aoi``,
        ``aoi_modifier``, ``spectral_modifier``, and
        ``effective_irradiance``, ``cell_temperature``, ``dc``, ``ac``,
        ``losses``, ``diode_params`` (if dc_model is a single diode
        model).

        See also
        --------
        pvlib.modelchain.ModelChain.run_model_from_poa
        pvlib.modelchain.ModelChain.run_model_from_effective_irradiance
        """
        weather = _to_tuple(weather)
        self.prepare_inputs(weather)
        self.aoi_model()
        self.spectral_model()
        self.effective_irradiance_model()

        self._run_from_effective_irrad(weather)

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

The methods called by run_model() store their results in the results attribute, which is an instance of ModelChainResult. For example, ModelChainResult includes the following attributes: solar_position, effective_irradiance, cell_temperature, dc, ac. See ModelChainResult for a full list of results attributes.

Wrapping methods into a unified API

Readers may notice that the source code of the run_model() method is model-agnostic. run_model() calls generic methods such as self.dc_model rather than a specific model such as pvwatts_dc. So how does 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 pvwatts_dc() method is shown below. Its only argument is self, and it sets the dc attribute.

In [48]: mc.pvwatts_dc??
Signature: mc.pvwatts_dc()
Source:   
    def pvwatts_dc(self):
        """Calculate DC power using the PVWatts model.

        Results are stored in ModelChain.results.dc. DC power is computed
        from PVSystem.arrays[i].module_parameters['pdc0'] and then scaled by
        PVSystem.modules_per_string and PVSystem.strings_per_inverter.

        Returns
        -------
        self

        See also
        --------
        pvlib.pvsystem.PVSystem.pvwatts_dc
        pvlib.pvsystem.PVSystem.scale_voltage_current_power
        """
        dc = self.system.pvwatts_dc(
            self.results.effective_irradiance,
            self.results.cell_temperature,
            unwrap=False
        )
        p_mp = tuple(pd.DataFrame(s, columns=['p_mp']) for s in dc)
        scaled = self.system.scale_voltage_current_power(p_mp)
        self.results.dc = _tuple_from_dfs(scaled, "p_mp")
        return self
File:      ~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      method

The pvwatts_dc() method calls the pvwatts_dc method of the PVSystem object that we supplied when we created the ModelChain instance, using data that is stored in the ModelChain effective_irradiance and cell_temperature attributes. The pvwatts_dc() method assigns its result to the dc attribute of the ModelChain’s results object. The code below shows a simple example of this.

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

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

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

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

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

In [54]: mc.results.dc
Out[54]: 
2017-04-01 12:00:00-07:00    216.0
Name: p_mp, dtype: float64

The sapm() method works in a manner similar to the pvwatts_dc() method. It calls the sapm() method using stored data, then assigns the result to the dc attribute of ModelChain.results. The sapm() method differs from the pvwatts_dc() method in a notable way: the PVSystem.sapm method returns a DataFrame with current, voltage, and power results, rather than a simple Series of power. The ModelChain methods for single diode models (e.g., desoto()) also return a DataFrame with current, voltage and power, and a second DataFrame with the single diode equation parameter values.

All ModelChain methods for DC output use the scale_voltage_current_power() method to scale DC quantities to the output of the full PVSystem.

In [55]: mc.sapm??
Signature: mc.sapm()
Docstring: <no docstring>
Source:   
    def sapm(self):
        dc = self.system.sapm(self.results.effective_irradiance,
                              self.results.cell_temperature)
        self.results.dc = self.system.scale_voltage_current_power(dc)
        return self
File:      ~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      method
# make the objects
In [56]: sapm_system = PVSystem(
   ....:     module_parameters=sandia_module,
   ....:     inverter_parameters=cec_inverter,
   ....:     temperature_model_parameters=temperature_model_parameters)
   ....: 

In [57]: 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.
In [58]: mc.results.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')])

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

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

In [61]: mc.results.dc
Out[61]: 
                              i_sc      i_mp  ...       i_x      i_xx
2017-04-01 12:00:00-07:00  5.14168  4.566863  ...  5.025377  3.219662

[1 rows x 7 columns]

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 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) either with the value assigned to the dc_model parameter at construction, or by inference as described in the next section.

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

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

In [64]: mc.dc_model.__func__
Out[64]: <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

When ModelChain’s attributes are not assigned when the instance is created, ModelChain can infer the appropriate model from data stored on the PVSystem object. ModelChain uses a set of methods (e.g., infer_dc_model(), infer_ac_model(), etc.) that examine the parameters on the user-supplied PVSystem object. The inference methods use set logic to assign one of the model-specific methods, such as sapm() or sandia_inverter(), to the universal method names ModelChain.dc_model and ModelChain.ac_model, respectively. A few examples are shown below. Inference methods generally work by inspecting the parameters for all required parameters for a corresponding method.

In [65]: mc.infer_dc_model??
Signature: mc.infer_dc_model()
Source:   
    def infer_dc_model(self):
        """Infer DC power model from Array module parameters."""
        params = _common_keys(
            tuple(array.module_parameters for array in self.system.arrays))
        if {'A0', 'A1', 'C7'} <= params:
            return self.sapm, 'sapm'
        elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s',
              'Adjust'} <= params:
            return self.cec, 'cec'
        elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s'} <= params:
            return self.desoto, 'desoto'
        elif {'gamma_ref', 'mu_gamma', 'I_L_ref', 'I_o_ref', 'R_sh_ref',
              'R_sh_0', 'R_sh_exp', 'R_s'} <= params:
            return self.pvsyst, 'pvsyst'
        elif {'pdc0', 'gamma_pdc'} <= params:
            return self.pvwatts_dc, 'pvwatts'
        else:
            raise ValueError(
                'Could not infer DC model from the module_parameters '
                'attributes of system.arrays. Check the module_parameters '
                'attributes or explicitly set the model with the dc_model '
                'keyword argument.')
File:      ~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      method
In [66]: mc.infer_ac_model??
   ....: pvlib.modelchain._snl_params??
   ....: pvlib.modelchain._adr_params??
   ....: pvlib.modelchain._pvwatts_params??
   ....: 
Signature: mc.infer_ac_model()
Source:   
    def infer_ac_model(self):
        """Infer AC power model from system attributes."""
        inverter_params = set(self.system.inverter_parameters.keys())
        if _snl_params(inverter_params):
            return self.sandia_inverter
        if _adr_params(inverter_params):
            if self.system.num_arrays > 1:
                raise ValueError(
                    'The adr inverter function cannot be used for an inverter',
                    ' with multiple MPPT inputs')
            else:
                return self.adr_inverter
        if _pvwatts_params(inverter_params):
            return self.pvwatts_inverter
        raise ValueError('could not infer AC model from '
                         'system.inverter_parameters. Check '
                         'system.inverter_parameters or explicitly '
                         'set the model with the ac_model kwarg.')
File:      ~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      method

Signature: pvlib.modelchain._snl_params(inverter_params)
Source:   
def _snl_params(inverter_params):
    """Return True if `inverter_params` includes parameters for the
    Sandia inverter model."""
    return {'C0', 'C1', 'C2'} <= inverter_params
File:      ~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      function

Signature: pvlib.modelchain._adr_params(inverter_params)
Source:   
def _adr_params(inverter_params):
    """Return True if `inverter_params` includes parameters for the ADR
    inverter model."""
    return {'ADRCoefficients'} <= inverter_params
File:      ~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      function

Signature: pvlib.modelchain._pvwatts_params(inverter_params)
Source:   
def _pvwatts_params(inverter_params):
    """Return True if `inverter_params` includes parameters for the
    PVWatts inverter model."""
    return {'pdc0'} <= inverter_params
File:      ~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/stable/pvlib/modelchain.py
Type:      function

ModelChain for a PVSystem with multiple Arrays

The PVSystem can represent a PV system with a single array of modules, or with multiple arrays (see PVSystem and Arrays). The same models are applied to all PVSystem.array objects, so each Array must contain the appropriate model parameters. For example, if ModelChain.dc_model='pvwatts', then each Array.module_parameters must contain 'pdc0'.

When the PVSystem contains multiple arrays, ModelChain.results attributes are tuples with length equal to the number of Arrays. Each tuple’s elements are in the same order as in PVSystem.arrays.

In [67]: from pvlib.pvsystem import Array

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

In [69]: inverter_parameters = {'pdc0': 10000, 'eta_inv_nom': 0.96}

In [70]: module_parameters = {'pdc0': 250, 'gamma_pdc': -0.004}

In [71]: array_one = Array(mount=FixedMount(surface_tilt=20, surface_azimuth=200),
   ....:                   module_parameters=module_parameters,
   ....:                   temperature_model_parameters=temperature_model_parameters,
   ....:                   modules_per_string=10, strings=2)
   ....: 

In [72]: array_two = Array(mount=FixedMount(surface_tilt=20, surface_azimuth=160),
   ....:                   module_parameters=module_parameters,
   ....:                   temperature_model_parameters=temperature_model_parameters,
   ....:                   modules_per_string=10, strings=2)
   ....: 

In [73]: system_two_arrays = PVSystem(arrays=[array_one, array_two],
   ....:                              inverter_parameters={'pdc0': 8000})
   ....: 

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

In [75]: mc.run_model(weather)
Out[75]: 
ModelChain: 
  name: 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: no_aoi_loss
  spectral_model: no_spectral_loss
  temperature_model: sapm_temp
  losses_model: no_extra_losses

In [76]: mc.results.dc
Out[76]: 
(2017-04-01 12:00:00-07:00    4664.84898
 Name: p_mp, dtype: float64,
 2017-04-01 12:00:00-07:00    4777.7143
 Name: p_mp, dtype: float64)

In [77]: mc.results.dc[0]
Out[77]: 
2017-04-01 12:00:00-07:00    4664.84898
Name: p_mp, dtype: float64

When weather is a single DataFrame, these data are broadcast and used for all arrays. Weather data can be specified for each array, in which case weather needs to be a tuple or list of DataFrames in the same order as the arrays of the PVSystem. To specify data separately for each array, provide a tuple for weather where each element is a DataFrame containing the required data.

Air, module and cell temperatures

The different run_model methods allow the ModelChain to be run starting with different irradiance data. Similarly, ModelChain run_model methods can be used with different temperature data as long as cell temperature can be determined. Temperature data are passed in the weather DataFrame and can include:

  • cell temperature ('cell_temperature'). If passed in weather no cell temperature model is run.

  • module temperature ('module_temperature'), typically measured on the rear surface. If found in weather and ModelChain.temperature_model='sapm' (either set directly or inferred), the sapm_temp() method is used to calculate cell temperature. If ModelChain.temperature_model is set to any other model, 'module_temperature' is ignored.

  • ambient air temperature ('temp_air'). In this case ModelChain.temperature_model is used to calculate cell temeprature.

Cell temperature models also can use irradiance as input. All cell temperature models expect POA irradiance ('poa_global') as input. When weather contains 'effective_irradiance' but not 'poa_global', 'effective_irradiance' is substituted for calculating cell temperature.

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

In [79]: def pvusa_mc_wrapper(mc):
   ....:     """
   ....:     Calculate the dc power and assign it to mc.results.dc
   ....:     Set up to iterate over arrays and total_irrad. mc.system.arrays is
   ....:     always a tuple. However, when there is a single array
   ....:     mc.results.total_irrad will be a Series (if multiple arrays,
   ....:     total_irrad will be a tuple). In this case we put total_irrad
   ....:     in a list so that we can iterate. If we didn't put total_irrad
   ....:     in a list, iteration will access each value of the Series, one
   ....:     at a time.
   ....:     The iteration returns a tuple. If there is a single array, the
   ....:     tuple is of length 1. As a convenience, pvlib unwraps tuples of length 1
   ....:     that are assigned to ModelChain.results attributes.
   ....:     Returning mc is optional, but enables method chaining.
   ....:     """
   ....:     if mc.system.num_arrays == 1:
   ....:         total_irrads = [mc.results.total_irrad]
   ....:     else:
   ....:         total_irrads = mc.results.total_irrad
   ....:     mc.results.dc = tuple(
   ....:         pvusa(total_irrad['poa_global'], mc.results.weather['wind_speed'],
   ....:               mc.results.weather['temp_air'], array.module_parameters['a'],
   ....:               array.module_parameters['b'], array.module_parameters['c'],
   ....:               array.module_parameters['d'])
   ....:         for total_irrad, array
   ....:         in zip(total_irrads, mc.system.arrays))
   ....:     return mc
   ....: 

In [80]: def pvusa_ac_mc(mc):
   ....:     mc.results.ac = mc.results.dc
   ....:     return mc
   ....: 

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

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

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

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

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

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

In [86]: mc = mc.run_model(weather)

In [87]: mc.results.dc
Out[87]: 
(2017-04-01 12:00:00-07:00    209.518773
 dtype: float64,)