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:
- Creating the
ModelChain
. - Executing the
ModelChain.run_model()
method with prepared weather data. - Examining the model results that
run_model()
stored in attributes of theModelChain
.
A simple ModelChain example¶
Before delving into the intricacies of ModelChain, we provide a brief example of the modeling steps using ModelChain. First, we import pvlib’s objects, module data, and inverter data.
In [1]: import pandas as pd
In [2]: import numpy as np
# pvlib imports
In [3]: import pvlib
In [4]: from pvlib.pvsystem import PVSystem
In [5]: from pvlib.location import Location
In [6]: from pvlib.modelchain import ModelChain
In [7]: from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
In [8]: temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']
# load some module and inverter specifications
In [9]: sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod')
In [10]: cec_inverters = pvlib.pvsystem.retrieve_sam('cecinverter')
In [11]: sandia_module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']
In [12]: cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_']
Now we create a Location object, a PVSystem object, and a ModelChain object.
In [13]: location = Location(latitude=32.2, longitude=-110.9)
In [14]: system = PVSystem(surface_tilt=20, surface_azimuth=200,
....: module_parameters=sandia_module,
....: inverter_parameters=cec_inverter,
....: temperature_model_parameters=temperature_model_parameters)
....:
In [15]: mc = ModelChain(system, location)
Printing a ModelChain object will display its models.
In [16]: 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
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);
ModelChain stores the modeling results on a series of attributes. A few examples are shown below.
In [19]: mc.aoi
Out[19]:
2017-04-01 12:00:00-07:00 15.929553
Name: aoi, dtype: float64
In [20]: mc.cell_temperature
Out[20]:
2017-04-01 12:00:00-07:00 58.087879
dtype: float64
In [21]: mc.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.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:
- The properties of its
PVSystem
andLocation
objects - The keyword arguments passed to it at construction
ModelChain uses the keyword arguments passed to it to determine the models for the simulation. The documentation describes the allowed values for each keyword argument. If a keyword argument is not supplied, ModelChain will attempt to infer the correct set of models by inspecting the Location and PVSystem attributes.
Below, we show some examples of how to define a ModelChain.
Let’s make the most basic Location and PVSystem objects and build from there.
In [23]: location = Location(32.2, -110.9)
In [24]: poorly_specified_system = PVSystem()
In [25]: print(location)
Location:
name: None
latitude: 32.2
longitude: -110.9
altitude: 0
tz: UTC
In [26]: print(poorly_specified_system)
PVSystem:
name: None
surface_tilt: 0
surface_azimuth: 180
module: None
inverter: None
albedo: 0.25
racking_model: None
module_type: None
temperature_model_parameters: {}
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/v0.8.0/pvlib/modelchain.py in __init__(self, system, location, orientation_strategy, clearsky_model, transposition_model, solar_position_method, airmass_model, dc_model, ac_model, aoi_model, spectral_model, temperature_model, losses_model, name, **kwargs)
356
357 # calls setters
--> 358 self.dc_model = dc_model
359 self.ac_model = ac_model
360 self.aoi_model = aoi_model
~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/v0.8.0/pvlib/modelchain.py in dc_model(self, model)
583 # guess at model if None
584 if model is None:
--> 585 self._dc_model, model = self.infer_dc_model()
586
587 # Set model and validate parameters
~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/v0.8.0/pvlib/modelchain.py in infer_dc_model(self)
627 return self.pvwatts_dc, 'pvwatts'
628 else:
--> 629 raise ValueError('could not infer DC model from '
630 'system.module_parameters. Check '
631 'system.module_parameters or explicitly '
ValueError: could not infer DC model from system.module_parameters. Check system.module_parameters or explicitly set the model with the dc_model kwarg.
Next, we define a PVSystem with a module from the SAPM database and an inverter from the CEC database. ModelChain will examine the PVSystem object’s properties and determine that it should choose the SAPM DC model, AC model, AOI loss model, and spectral loss model.
In [28]: sapm_system = PVSystem(
....: module_parameters=sandia_module,
....: inverter_parameters=cec_inverter,
....: temperature_model_parameters=temperature_model_parameters)
....:
In [29]: mc = ModelChain(sapm_system, location)
In [30]: 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
temperature_model: sapm_temp
losses_model: no_extra_losses
In [31]: mc.run_model(weather);
In [32]: mc.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 PVWatts data to
the PVSystem. ModelChain will automatically determine that it should
choose PVWatts DC and AC models. ModelChain still needs us to specify
aoi_model
and spectral_model
keyword arguments because the
system.module_parameters
dictionary does not contain enough
information to determine which of those models to choose.
In [33]: pvwatts_system = PVSystem(
....: module_parameters={'pdc0': 240, 'gamma_pdc': -0.004},
....: inverter_parameters={'pdc0': 240},
....: temperature_model_parameters=temperature_model_parameters)
....:
In [34]: mc = ModelChain(pvwatts_system, location,
....: aoi_model='physical', spectral_model='no_loss')
....:
In [35]: print(mc)
ModelChain:
name: None
orientation_strategy: None
clearsky_model: ineichen
transposition_model: haydavies
solar_position_method: nrel_numpy
airmass_model: kastenyoung1989
dc_model: pvwatts_dc
ac_model: pvwatts_inverter
aoi_model: physical_aoi_loss
spectral_model: no_spectral_loss
temperature_model: sapm_temp
losses_model: no_extra_losses
In [36]: mc.run_model(weather);
In [37]: mc.ac
Out[37]:
2017-04-01 12:00:00-07:00 198.519201
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
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
temperature_model: sapm_temp
losses_model: no_extra_losses
In [41]: mc.run_model(weather);
In [42]: mc.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.
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:
- The
ModelChain.run_model()
method- A set of methods that wrap and call the PVSystem methods.
- A set of methods that inspect user-supplied objects to determine the appropriate default models.
run_model¶
Most users will only interact with the
run_model()
method. The
run_model()
method, shown below,
calls a series of methods to complete the modeling steps. The first
method, prepare_inputs()
, computes
parameters such as solar position, airmass, angle of incidence, and
plane of array irradiance. The
prepare_inputs()
method also
assigns default values for temperature (20 C)
and wind speed (0 m/s) if these inputs are not provided.
prepare_inputs()
requires all irradiance
components (GHI, DNI, and DHI). See
complete_irradiance()
and
DNI estimation models for methods and functions that can help fully define
the irradiance inputs.
Next, run_model()
calls the
wrapper methods for AOI loss, spectral loss, effective irradiance, cell
temperature, DC power, AC power, and other losses. These methods are
assigned to standard names, as described in the next section.
The methods called by run_model()
store their results in a series of ModelChain attributes: times
,
solar_position
, airmass
, irradiance
, total_irrad
,
effective_irradiance
, weather
, temps
, aoi
,
aoi_modifier
, spectral_modifier
, dc
, ac
, losses
.
In [43]: mc.run_model??
Signature: mc.run_model(weather)
Source:
def run_model(self, weather):
"""
Run the model chain starting with broadband global, diffuse and/or
direct irradiance.
Parameters
----------
weather : 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'``.
Returns
-------
self
Notes
-----
Assigns attributes: ``solar_position``, ``airmass``, ``weather``,
``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
"""
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/v0.8.0/pvlib/modelchain.py
Type: method
Finally, the complete_irradiance()
method is available for calculating the full set of GHI, DNI, or DHI if
only two of these three series are provided. The completed dataset can
then be passed to run_model()
.
Wrapping methods into a unified API¶
Readers may notice that the source code of the ModelChain.run_model
method is model-agnostic. ModelChain.run_model calls generic methods
such as self.dc_model
rather than a specific model such as
singlediode
. So how does the ModelChain.run_model know what models
it’s supposed to run? The answer comes in two parts, and allows us to
explore more of the ModelChain API along the way.
First, ModelChain has a set of methods that wrap the PVSystem methods
that perform the calculations (or further wrap the pvsystem.py module’s
functions). Each of these methods takes the same arguments (self
)
and sets the same attributes, thus creating a uniform API. For example,
the ModelChain.pvwatts_dc method is shown below. Its only argument is
self
, and it sets the dc
attribute.
In [44]: mc.pvwatts_dc??
Signature: mc.pvwatts_dc()
Docstring: <no docstring>
Source:
def pvwatts_dc(self):
self.dc = self.system.pvwatts_dc(self.effective_irradiance,
self.cell_temperature)
return self
File: ~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/v0.8.0/pvlib/modelchain.py
Type: method
The ModelChain.pvwatts_dc method calls the pvwatts_dc method of the
PVSystem object that we supplied using data that is stored in its own
effective_irradiance
and cell_temperature
attributes. Then it assigns the
result to the dc
attribute of the ModelChain object. The code below
shows a simple example of this.
# make the objects
In [45]: pvwatts_system = PVSystem(
....: module_parameters={'pdc0': 240, 'gamma_pdc': -0.004},
....: inverter_parameters={'pdc0': 240},
....: temperature_model_parameters=temperature_model_parameters)
....:
In [46]: mc = ModelChain(pvwatts_system, location,
....: aoi_model='no_loss', spectral_model='no_loss')
....:
# manually assign data to the attributes that ModelChain.pvwatts_dc will need.
# for standard workflows, run_model would assign these attributes.
In [47]: mc.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')])
In [48]: mc.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')])
# run ModelChain.pvwatts_dc and look at the result
In [49]: mc.pvwatts_dc();
In [50]: mc.dc
Out[50]:
2017-04-01 12:00:00-07:00 216.0
dtype: float64
The ModelChain.sapm method works similarly to the ModelChain.pvwatts_dc
method. It calls the PVSystem.sapm method using stored data, then
assigns the result to the dc
attribute. The ModelChain.sapm method
differs from the ModelChain.pvwatts_dc method in three notable ways.
First, the PVSystem.sapm method expects different units for effective
irradiance, so ModelChain handles the conversion for us. Second, the
PVSystem.sapm method (and the PVSystem.singlediode method) returns a
DataFrame with current, voltage, and power parameters rather than a
simple Series of power. Finally, this current and voltage information
allows the SAPM and single diode model paths to support the concept of
modules in series and parallel, which is handled by the
PVSystem.scale_voltage_current_power method.
In [51]: mc.sapm??
Signature: mc.sapm()
Docstring: <no docstring>
Source:
def sapm(self):
self.dc = self.system.sapm(self.effective_irradiance,
self.cell_temperature)
self.dc = self.system.scale_voltage_current_power(self.dc)
return self
File: ~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/v0.8.0/pvlib/modelchain.py
Type: method
# make the objects
In [52]: sapm_system = PVSystem(
....: module_parameters=sandia_module,
....: inverter_parameters=cec_inverter,
....: temperature_model_parameters=temperature_model_parameters)
....:
In [53]: 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 [54]: mc.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')])
In [55]: mc.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')])
# run ModelChain.sapm and look at the result
In [56]: mc.sapm();
In [57]: mc.dc
Out[57]:
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 the ModelChain.run_model know which models it’s supposed to run?
At object construction, ModelChain assigns the desired model’s method
(e.g. ModelChain.pvwatts_dc
) to the corresponding generic attribute
(e.g. ModelChain.dc_model
) using a method described in the next
section.
In [58]: pvwatts_system = PVSystem(
....: module_parameters={'pdc0': 240, 'gamma_pdc': -0.004},
....: inverter_parameters={'pdc0': 240},
....: temperature_model_parameters=temperature_model_parameters)
....:
In [59]: mc = ModelChain(pvwatts_system, location,
....: aoi_model='no_loss', spectral_model='no_loss')
....:
In [60]: mc.dc_model.__func__
Out[60]: <function pvlib.modelchain.ModelChain.pvwatts_dc(self)>
The ModelChain.run_model method can ignorantly call self.dc_module
because the API is the same for all methods that may be assigned to this
attribute.
* some readers may object that the API is not actually the same
because the type of the dc
attribute is different (Series
vs. DataFrame)!
Inferring models¶
How does ModelChain infer the appropriate model types? ModelChain uses a series of methods (ModelChain.infer_dc_model, ModelChain.infer_ac_model, etc.) that examine the user-supplied PVSystem object. The inference methods use set logic to assign one of the model-specific methods, such as ModelChain.sapm or ModelChain.snlinverter, to the universal method names ModelChain.dc_model and ModelChain.ac_model. A few examples are shown below.
In [61]: mc.infer_dc_model??
Signature: mc.infer_dc_model()
Source:
def infer_dc_model(self):
"""Infer DC power model from system attributes."""
params = set(self.system.module_parameters.keys())
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 '
'system.module_parameters. Check '
'system.module_parameters or explicitly '
'set the model with the dc_model kwarg.')
File: ~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/v0.8.0/pvlib/modelchain.py
Type: method
In [62]: mc.infer_ac_model??
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 {'C0', 'C1', 'C2'} <= inverter_params:
return self.snlinverter
elif {'ADRCoefficients'} <= inverter_params:
return self.adrinverter
elif {'pdc0'} <= inverter_params:
return self.pvwatts_inverter
else:
raise ValueError('could not infer AC model from '
'system.inverter_parameters. Check '
'system.inverter_parameters or explicitly '
'set the model with the ac_model kwarg.')
File: ~/checkouts/readthedocs.org/user_builds/pvlib-python/checkouts/v0.8.0/pvlib/modelchain.py
Type: method
User-defined models¶
Users may also write their own functions and pass them as arguments to ModelChain. The first argument of the function must be a ModelChain instance. For example, the functions below implement the PVUSA model and a wrapper function appropriate for use with ModelChain. This follows the pattern of implementing the core models using the simplest possible functions, and then implementing wrappers to make them easier to use in specific applications. Of course, you could implement it in a single function if you wanted to.
In [63]: def pvusa(poa_global, wind_speed, temp_air, a, b, c, d):
....: """
....: Calculates system power according to the PVUSA equation
....: P = I * (a + b*I + c*W + d*T)
....: where
....: P is the output power,
....: I is the plane of array irradiance,
....: W is the wind speed, and
....: T is the temperature
....: a, b, c, d are empirically derived parameters.
....: """
....: return poa_global * (a + b*poa_global + c*wind_speed + d*temp_air)
....:
In [64]: def pvusa_mc_wrapper(mc):
....: mc.dc = pvusa(mc.total_irrad['poa_global'], mc.weather['wind_speed'], mc.weather['temp_air'],
....: mc.system.module_parameters['a'], mc.system.module_parameters['b'],
....: mc.system.module_parameters['c'], mc.system.module_parameters['d'])
....:
# returning mc is optional, but enables method chaining
In [65]: def pvusa_ac_mc(mc):
....: mc.ac = mc.dc
....: return mc
....:
In [66]: def no_loss_temperature(mc):
....: mc.cell_temperature = mc.weather['temp_air']
....: return mc
....:
In [67]: module_parameters = {'a': 0.2, 'b': 0.00001, 'c': 0.001, 'd': -0.00005}
In [68]: pvusa_system = PVSystem(module_parameters=module_parameters)
In [69]: mc = ModelChain(pvusa_system, location,
....: dc_model=pvusa_mc_wrapper, ac_model=pvusa_ac_mc,
....: temperature_model=no_loss_temperature,
....: aoi_model='no_loss', spectral_model='no_loss')
....:
A ModelChain object uses Python’s functools.partial function to assign itself as the argument to the user-supplied functions.
In [70]: mc.dc_model.func
Out[70]: <function __main__.pvusa_mc_wrapper(mc)>
The end result is that ModelChain.run_model works as expected!
In [71]: mc.run_model(weather);
In [72]: mc.dc
Out[72]:
2017-04-01 12:00:00-07:00 209.518773
dtype: float64