diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 633a9d8cc0..d62782bfdf 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -572,6 +572,8 @@ Creating a ModelChain object. modelchain.ModelChain.with_pvwatts modelchain.ModelChain.with_sapm +.. _modelchain_runmodel: + Running ------- diff --git a/docs/sphinx/source/introtutorial.rst b/docs/sphinx/source/introtutorial.rst index 495b0c6e40..b3a9b9a7b6 100644 --- a/docs/sphinx/source/introtutorial.rst +++ b/docs/sphinx/source/introtutorial.rst @@ -209,8 +209,8 @@ by examining the parameters defined for the module. ) mc = ModelChain(system, location) - results = mc.run_model(weather) - annual_energy = results.ac.sum() + mc.run_model(weather) + annual_energy = mc.results.ac.sum() energies[name] = annual_energy energies = pd.Series(energies) diff --git a/docs/sphinx/source/modelchain.rst b/docs/sphinx/source/modelchain.rst index 716e37b396..00b1d2c6fd 100644 --- a/docs/sphinx/source/modelchain.rst +++ b/docs/sphinx/source/modelchain.rst @@ -4,28 +4,26 @@ ModelChain The :py:class:`~.modelchain.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 +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 :py:class:`~.modelchain.ModelChain` is composed of a -:py:class:`~.pvsystem.PVSystem` object and a -:py:class:`~.location.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. +A :py:class:`~.modelchain.ModelChain` has three components: + +* a :py:class:`~.pvsystem.PVSystem` object, representing a collection of modules and inverters +* a :py:class:`~.location.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 :py:class:`~.ModelChain` typically involves 3 steps: -1. Creating the :py:class:`~.ModelChain`. -2. Executing the :py:meth:`ModelChain.run_model() <.ModelChain.run_model>` - method with prepared weather data. -3. Examining the model results that :py:meth:`~.ModelChain.run_model` - stored in attributes of the :py:class:`~.ModelChain`. +1. Creating an instance of :py:class:`~pvlib.modelchain.ModelChain`. +2. Executing a ModelChain.run_model method with weather data as input. See + :ref:`modelchain_runmodel` for a list of run_model methods. +3. Examining the model results that are stored in the ModelChain's + :py:class:`ModelChain.results ` attribute. A simple ModelChain example --------------------------- @@ -81,26 +79,28 @@ Next, we run a model with some simple weather data. columns=['ghi', 'dni', 'dhi', 'temp_air', 'wind_speed'], index=[pd.Timestamp('20170401 1200', tz='US/Arizona')]) - mc.run_model(weather); + mc.run_model(weather) -ModelChain stores the modeling results on a series of attributes. A few -examples are shown below. +ModelChain stores the modeling results in the ``results`` attribute. The +``results`` attribute is an instance of :py:class:`~pvlib.modelchain.ModelChainResult`. +A few examples of attributes of :py:class:`~pvlib.modelchain.ModelChainResult` +are shown below. .. ipython:: python - mc.aoi + mc.results.aoi .. ipython:: python - mc.cell_temperature + mc.results.cell_temperature .. ipython:: python - mc.dc + mc.results.dc .. ipython:: python - mc.ac + mc.results.ac The remainder of this guide examines the ModelChain functionality and explores common pitfalls. @@ -158,12 +158,12 @@ model, AC model, AOI loss model, and spectral loss model. .. ipython:: python mc.run_model(weather); - mc.ac + mc.results.ac 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 +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. @@ -181,7 +181,7 @@ information to determine which of those models to choose. .. ipython:: python mc.run_model(weather); - mc.ac + mc.results.ac User-supplied keyword arguments override ModelChain’s inspection methods. For example, we can tell ModelChain to use different loss @@ -199,11 +199,30 @@ functions for a PVSystem that contains SAPM-specific parameters. .. ipython:: python mc.run_model(weather); - mc.ac + mc.results.ac Of course, these choices can also lead to failure when executing :py:meth:`~pvlib.modelchain.ModelChain.run_model` if your system objects -do not contain the required parameters for running the model. +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: + +* :py:meth:`~pvlib.modelchain.ModelChain.with_pvwatts` +* :py:meth:`~pvlib.modelchain.ModelChain.with_sapm` + +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: + +.. ipython:: python + + mc = mc.with_sapm(sapm_system, location) + print(mc) + + mc.run_model(weather) + mc.results.dc + Demystifying ModelChain internals --------------------------------- @@ -213,57 +232,64 @@ users' code as simple as possible. The key parts of ModelChain are: - 1. The :py:meth:`ModelChain.run_model() <.ModelChain.run_model>` method + 1. The ModelChain.run_model methods. 2. A set of methods that wrap and call the PVSystem methods. - 3. A set of methods that inspect user-supplied objects to determine - the appropriate default models. - -run_model -~~~~~~~~~ + 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: + +* :py:meth:`~pvlib.modelchain.ModelChain.run_model`, use when ``weather`` + contains global horizontal, direct and diffuse horizontal irradiance + (``'ghi'``, ``'dni'`` and ``'dhi'``). +* :py:meth:`~pvlib.modelchain.ModelChain.run_model_from_poa`, use when + ``weather`` broadband direct, diffuse and total irradiance in the plane of array + (``'poa_global'``, ``'poa_direct'``, ``'poa_diffuse'``). +* :py:meth:`~pvlib.modelchain.ModelChain.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. +:py:meth:`~pvlib.modelchain.ModelChain.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 :py:meth:`~pvlib.modelchain.ModelChain.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 :ref:`dniestmodels` +for methods and functions that can help fully define the irradiance inputs. -Most users will only interact with the -:py:meth:`~pvlib.modelchain.ModelChain.run_model` method. The -:py:meth:`~pvlib.modelchain.ModelChain.run_model` method, shown below, +The :py:meth:`~pvlib.modelchain.ModelChain.run_model` method, shown below, calls a series of methods to complete the modeling steps. The first method, :py:meth:`~pvlib.modelchain.ModelChain.prepare_inputs`, computes parameters such as solar position, airmass, angle of incidence, and -plane of array irradiance. The -:py:meth:`~pvlib.modelchain.ModelChain.prepare_inputs` method also -assigns default values for temperature (20 C) -and wind speed (0 m/s) if these inputs are not provided. -:py:meth:`~pvlib.modelchain.ModelChain.prepare_inputs` requires all irradiance -components (GHI, DNI, and DHI). See -:py:meth:`~pvlib.modelchain.ModelChain.complete_irradiance` and -:ref:`dniestmodels` for methods and functions that can help fully define -the irradiance inputs. - -Next, :py:meth:`~pvlib.modelchain.ModelChain.run_model` calls the +plane of array irradiance. Next, :py:meth:`~pvlib.modelchain.ModelChain.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 :py:meth:`~pvlib.modelchain.ModelChain.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``. +assigned to generic names, as described in the next section. .. ipython:: python mc.run_model?? -Finally, the :py:meth:`~pvlib.modelchain.ModelChain.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 :py:meth:`~pvlib.modelchain.ModelChain.run_model`. +The methods called by :py:meth:`~pvlib.modelchain.ModelChain.run_model` +store their results in the ``results`` attribute, which is an instance of +:py:class:`~pvlib.modelchain.ModelChainResult`. For example, :py:class:`~.ModelChainResult` +includes the following attributes: ``solar_position``, ``effective_irradiance``, +``cell_temperature``, ``dc``, ``ac``. See :py:class:`~pvlib.modelchain.ModelChainResult` +for a full list of results attributes. + 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 +Readers may notice that the source code of the :py:meth:`~pvlib.modelchain.ModelChain.run_model` +method is model-agnostic. :py:meth:`~pvlib.modelchain.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 +``pvwatts_dc``. So how does :py:meth:`~pvlib.modelchain.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. @@ -271,18 +297,19 @@ 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 +the :py:meth:`~pvlib.modelchain.ModelChain.pvwatts_dc` method is shown below. Its only argument is ``self``, and it sets the ``dc`` attribute. .. ipython:: python mc.pvwatts_dc?? -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. +The :py:meth:`~pvlib.modelchain.ModelChain.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 :py:meth:`~pvlib.modelchain.ModelChain.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. .. ipython:: python @@ -296,25 +323,29 @@ shows a simple example of this. # 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.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')]) + mc.results.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')]) + mc.results.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')]) # run ModelChain.pvwatts_dc and look at the result mc.pvwatts_dc(); - mc.dc - -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. + mc.results.dc + +The :py:meth:`~pvlib.modelchain.ModelChain.sapm` method works in a manner similar +to the :py:meth:`~pvlib.modelchain.ModelChain.pvwatts_dc` +method. It calls the :py:meth:`~pvlib.pvsystem.PVSystem.sapm` method using stored data, then +assigns the result to the ``dc`` attribute of ``ModelChain.results``. +The :py:meth:`~pvlib.modelchain.ModelChain.sapm` method differs from the +:py:meth:`~pvlib.modelchain.ModelChain.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., +:py:meth:`~pvlib.modelchain.ModelChain.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 +:py:meth:`~pvlib.pvsystem.PVSystem.scale_voltage_current_power` method to scale +DC quantities to the output of the full PVSystem. .. ipython:: python @@ -331,12 +362,12 @@ PVSystem.scale_voltage_current_power method. # 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.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')]) + mc.results.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')]) + mc.results.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')]) # run ModelChain.sapm and look at the result mc.sapm(); - mc.dc + mc.results.dc We’ve established that the ``ModelChain.pvwatts_dc`` and ``ModelChain.sapm`` have the same API: they take the same arugments @@ -345,12 +376,13 @@ 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? +Again, so how does :py:meth:`~pvlib.modelchain.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 +(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. .. ipython:: python @@ -374,13 +406,17 @@ 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. +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., :py:meth:`~pvlib.modelchain.ModelChain.infer_dc_model`, +:py:meth:`~pvlib.modelchain.ModelChain.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 +:py:meth:`~pvlib.modelchain.ModelChain.sapm` or :py:meth:`~pvlib.modelchain.ModelChain.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. .. ipython:: python @@ -389,6 +425,77 @@ shown below. .. ipython:: python mc.infer_ac_model?? + pvlib.modelchain._snl_params?? + pvlib.modelchain._adr_params?? + pvlib.modelchain._pvwatts_params?? + +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 :ref:`multiarray`). 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``. + +.. ipython:: python + + from pvlib.pvsystem import Array + location = Location(latitude=32.2, longitude=-110.9) + inverter_parameters = {'pdc0': 10000, 'eta_inv_nom': 0.96} + module_parameters = {'pdc0': 250, 'gamma_pdc': -0.004} + array_one = Array(surface_tilt=20, surface_azimuth=200, + module_parameters=module_parameters, + temperature_model_parameters=temperature_model_parameters, + modules_per_string=10, strings=2) + array_two = Array(surface_tilt=20, surface_azimuth=160, + module_parameters=module_parameters, + temperature_model_parameters=temperature_model_parameters, + modules_per_string=10, strings=2) + system_two_arrays = PVSystem(arrays=[array_one, array_two], + inverter_parameters={'pdc0': 8000}) + mc = ModelChain(system_two_arrays, location, aoi_model='no_loss', + spectral_model='no_loss') + + mc.run_model(weather) + + mc.results.dc + mc.results.dc[0] + +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 :py:meth:`~pvlib.modelchain.ModelChain.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 ------------------- @@ -419,27 +526,43 @@ function if you wanted to. def pvusa_mc_wrapper(mc): - # calculate the dc power and assign it to mc.dc - # in the future, need to explicitly iterate over system.arrays - # https://github.com/pvlib/pvlib-python/issues/1115 - mc.dc = pvusa(mc.results.total_irrad['poa_global'], - mc.results.weather['wind_speed'], mc.results.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 + """ + 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 def pvusa_ac_mc(mc): # keep it simple - mc.ac = mc.dc + mc.results.ac = mc.results.dc return mc def no_loss_temperature(mc): # keep it simple - mc.cell_temperature = mc.results.weather['temp_air'] + mc.results.cell_temperature = mc.results.weather['temp_air'] return mc @@ -464,5 +587,5 @@ The end result is that ModelChain.run_model works as expected! .. ipython:: python - mc.run_model(weather); - mc.dc + mc = mc.run_model(weather) + mc.results.dc