diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..7e3cb863b9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +pvlib python pull request guidelines +==================================== + +Thank you for your contribution to pvlib python! + +You may submit a pull request with your code at any stage of completion, however, before the code can be merged the following items must be addressed: + + - [ ] Closes issue #xxxx + - [ ] Fully tested. Added and/or modified tests to ensure correct behavior for all reasonable inputs. Tests must pass on the TravisCI and Appveyor testing services. + - [ ] Code quality and style is sufficient. Passes ``git diff upstream/master -u -- "*.py" | flake8 --diff`` and/or landscape.io linting service. + - [ ] New code is fully documented. Includes sphinx/numpydoc compliant docstrings and comments in the code where necessary. + - [ ] Updates entries to `docs/sphinx/source/api.rst` for API changes. + - [ ] Adds description and name entries in the appropriate `docs/sphinx/source/whatsnew` file for all changes. + +Please don't hesitate to ask for help if you're unsure of how to accomplish any of the above. You may delete all of these instructions except for the list above. + +Brief description of the problem and proposed solution (if not already fully described in the issue linked to above): diff --git a/docs/environment.yml b/docs/environment.yml index d1be3cb1e2..998f699a81 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -22,3 +22,4 @@ dependencies: - hdf4=4.2.12 - sphinx_rtd_theme - docutils + - nbsphinx diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 5fd65b8733..8ddbd40f59 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -73,6 +73,7 @@ calculations. :toctree: generated/ solarposition.solar_zenith_analytical + solarposition.solar_azimuth_analytical solarposition.declination_spencer71 solarposition.declination_cooper69 solarposition.equation_of_time_spencer71 diff --git a/docs/sphinx/source/conf.py b/docs/sphinx/source/conf.py index 891386ec1b..8483da4c44 100644 --- a/docs/sphinx/source/conf.py +++ b/docs/sphinx/source/conf.py @@ -59,6 +59,7 @@ def __getattr__(cls, name): 'sphinx.ext.autosummary', 'IPython.sphinxext.ipython_directive', 'IPython.sphinxext.ipython_console_highlighting', + 'nbsphinx' ] # Add any paths that contain templates here, relative to this directory. @@ -100,7 +101,7 @@ def __getattr__(cls, name): # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['whatsnew/*'] +exclude_patterns = ['whatsnew/*', '**.ipynb_checkpoints'] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -316,3 +317,4 @@ def setup(app): 'numpy': ('http://docs.scipy.org/doc/numpy/', None), } +nbsphinx_allow_errors = True diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index bb6529c61b..a5ad0d3e4e 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -74,6 +74,7 @@ Contents whatsnew installation contributing + modelchain timetimezones clearsky forecasts diff --git a/docs/sphinx/source/modelchain.ipynb b/docs/sphinx/source/modelchain.ipynb new file mode 100644 index 0000000000..8dff7dc2fc --- /dev/null +++ b/docs/sphinx/source/modelchain.ipynb @@ -0,0 +1,738 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ModelChain" + ] + }, + { + "cell_type": "raw", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "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 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.\n", + "\n", + "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.\n", + "\n", + "Modeling with a :py:class:`~.ModelChain` typically involves 3 steps:\n", + "\n", + "1. Creating the :py:class:`~.ModelChain`.\n", + "2. Executing the :py:meth:`ModelChain.run_model() <.ModelChain.run_model>` method with prepared weather data.\n", + "3. Examining the model results that :py:meth:`~.ModelChain.run_model` stored in attributes of the :py:class:`~.ModelChain`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## A simple ModelChain example\n", + "\n", + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "# pvlib imports\n", + "import pvlib\n", + "\n", + "from pvlib.pvsystem import PVSystem\n", + "from pvlib.location import Location\n", + "from pvlib.modelchain import ModelChain\n", + "\n", + "# load some module and inverter specifications\n", + "sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod')\n", + "cec_inverters = pvlib.pvsystem.retrieve_sam('cecinverter')\n", + "\n", + "sandia_module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']\n", + "cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we create a Location object, a PVSystem object, and a ModelChain object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "location = Location(latitude=32.2, longitude=-110.9)\n", + "system = PVSystem(surface_tilt=20, surface_azimuth=200, \n", + " module_parameters=sandia_module,\n", + " inverter_parameters=cec_inverter)\n", + "mc = ModelChain(system, location)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Printing a ModelChain object will display its models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "print(mc)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we run a model with some simple weather data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "weather = pd.DataFrame([[1050, 1000, 100, 30, 5]], \n", + " columns=['ghi', 'dni', 'dhi', 'temp_air', 'wind_speed'], \n", + " index=[pd.Timestamp('20170401 1200', tz='US/Arizona')])\n", + "\n", + "mc.run_model(times=weather.index, weather=weather);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "ModelChain stores the modeling results on a series of attributes. A few examples are shown below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mc.aoi" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mc.dc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mc.ac" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The remainder of this guide examines the ModelChain functionality and explores common pitfalls." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining a ModelChain" + ] + }, + { + "cell_type": "raw", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "A :py:class:`~pvlib.modelchain.ModelChain` object is defined by:\n", + "\n", + "1. The properties of its :py:class:`~pvlib.pvsystem.PVSystem` and :py:class:`~pvlib.location.Location` objects\n", + "2. The keyword arguments passed to it at construction\n", + "\n", + "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. \n", + "\n", + "Below, we show some examples of how to define a ModelChain." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's make the most basic Location and PVSystem objects and build from there." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "location = Location(32.2, -110.9)\n", + "poorly_specified_system = PVSystem()\n", + "print(location)\n", + "print(poorly_specified_system)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "ModelChain(poorly_specified_system, location)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If our goal is simply to get the object constructed, we can specify the models that the ModelChain should use. We'll have to fill in missing data on the PVSystem object later, but maybe that's desirable in some workflows." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mc = ModelChain(poorly_specified_system, location, \n", + " dc_model='singlediode', ac_model='snlinverter', \n", + " aoi_model='physical', spectral_model='no_loss')\n", + "print(mc)" + ] + }, + { + "cell_type": "raw", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "As expected, without additional information, the :py:meth:`~.ModelChain.run_model` method fails at run time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mc.run_model(times=weather.index, weather=weather)" + ] + }, + { + "cell_type": "raw", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "The ModelChain attempted to execute the PVSystem object's :py:meth:`~pvlib.pvsystem.PVSystem.singlediode` method, and the method failed because the object's ``module_parameters`` did not include the data necessary to run the model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter)\n", + "mc = ModelChain(system, location)\n", + "print(mc)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mc.run_model(times=weather.index, weather=weather)\n", + "mc.ac" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004})\n", + "mc = ModelChain(pvwatts_system, location, \n", + " aoi_model='physical', spectral_model='no_loss')\n", + "print(mc)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mc.run_model(times=weather.index, weather=weather)\n", + "mc.ac" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter)\n", + "mc = ModelChain(system, location, aoi_model='physical', spectral_model='no_loss')\n", + "print(mc)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mc.run_model(times=weather.index, weather=weather)\n", + "mc.ac" + ] + }, + { + "cell_type": "raw", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "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." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demystifying ModelChain internals" + ] + }, + { + "cell_type": "raw", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "The ModelChain class has a lot going in inside it in order to make users' code as simple as possible.\n", + "\n", + "The key parts of ModelChain are:\n", + "\n", + "1. The :py:meth:`ModelChain.run_model() <.ModelChain.run_model>` method\n", + "1. A set of methods that wrap and call the PVSystem methods.\n", + "1. A set of methods that inspect user-supplied objects to determine the appropriate default models." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### run_model" + ] + }, + { + "cell_type": "raw", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "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, 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 irradiance (clear sky), temperature (20 C), and wind speed (0 m/s) if these inputs are not provided.\n", + "\n", + "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.\n", + "\n", + "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``." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "np.source(mc.run_model)" + ] + }, + { + "cell_type": "raw", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "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`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Wrapping methods into a unified API\n", + "\n", + "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.\n", + "\n", + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "np.source(mc.pvwatts_dc)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ModelChain.pvwatts_dc method calls the pvwatts_dc method of the PVSystem object that we supplied using data that is stored in its own ``effective_irradiance`` and ``temps`` attributes. Then it assigns the result to the ``dc`` attribute of the ModelChain object. The code below shows a simple example of this." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# make the objects\n", + "pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004})\n", + "mc = ModelChain(pvwatts_system, location, \n", + " aoi_model='no_loss', spectral_model='no_loss')\n", + "\n", + "# manually assign data to the attributes that ModelChain.pvwatts_dc will need.\n", + "# for standard workflows, run_model would assign these attributes.\n", + "mc.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')])\n", + "mc.temps = pd.DataFrame({'temp_cell': 50, 'temp_module': 50}, index=[pd.Timestamp('20170401 1200-0700')])\n", + "\n", + "# run ModelChain.pvwatts_dc and look at the result\n", + "mc.pvwatts_dc()\n", + "mc.dc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "np.source(mc.sapm)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# make the objects\n", + "sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter)\n", + "mc = ModelChain(sapm_system, location)\n", + "\n", + "# manually assign data to the attributes that ModelChain.sapm will need.\n", + "# for standard workflows, run_model would assign these attributes.\n", + "mc.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')])\n", + "mc.temps = pd.DataFrame({'temp_cell': 50, 'temp_module': 50}, index=[pd.Timestamp('20170401 1200-0700')])\n", + "\n", + "# run ModelChain.sapm and look at the result\n", + "mc.sapm()\n", + "mc.dc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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.\n", + "\n", + "Again, so how does the ModelChain.run_model know which models it's supposed to run?\n", + "\n", + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004})\n", + "mc = ModelChain(pvwatts_system, location, \n", + " aoi_model='no_loss', spectral_model='no_loss')\n", + "mc.dc_model.__func__" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\\* some readers may object that the API is *not* actually the same because the type of the ``dc`` attribute is different (Series vs. DataFrame)!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Inferring models\n", + "\n", + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "np.source(mc.infer_dc_model)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "np.source(mc.infer_ac_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## User-defined models\n", + "\n", + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def pvusa(poa_global, wind_speed, temp_air, a, b, c, d):\n", + " \"\"\"\n", + " Calculates system power according to the PVUSA equation\n", + " \n", + " P = I * (a + b*I + c*W + d*T)\n", + " \n", + " where\n", + " P is the output power,\n", + " I is the plane of array irradiance,\n", + " W is the wind speed, and\n", + " T is the temperature\n", + " a, b, c, d are empirically derived parameters.\n", + " \"\"\"\n", + " return poa_global * (a + b*poa_global + c*wind_speed + d*temp_air)\n", + "\n", + "\n", + "def pvusa_mc_wrapper(mc):\n", + " # calculate the dc power and assign it to mc.dc\n", + " mc.dc = pvusa(mc.total_irrad['poa_global'], mc.weather['wind_speed'], mc.weather['temp_air'],\n", + " mc.system.module_parameters['a'], mc.system.module_parameters['b'],\n", + " mc.system.module_parameters['c'], mc.system.module_parameters['d'])\n", + " \n", + " # returning mc is optional, but enables method chaining\n", + " return mc\n", + "\n", + "\n", + "def pvusa_ac_mc_wrapper(mc):\n", + " # keep it simple\n", + " mc.ac = mc.dc\n", + " return mc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "module_parameters = {'a': 0.2, 'b': 0.00001, 'c': 0.001, 'd': -0.00005}\n", + "pvusa_system = PVSystem(module_parameters=module_parameters)\n", + "\n", + "mc = ModelChain(pvusa_system, location, \n", + " dc_model=pvusa_mc_wrapper, ac_model=pvusa_ac_mc_wrapper,\n", + " aoi_model='no_loss', spectral_model='no_loss')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A ModelChain object uses Python's functools.partial function to assign itself as the argument to the user-supplied functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mc.dc_model.func" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The end result is that ModelChain.run_model works as expected!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mc.run_model(times=weather.index, weather=weather)\n", + "mc.dc" + ] + } + ], + "metadata": { + "celltoolbar": "Raw Cell Format", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index 8c5ecefa2a..78738125b2 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -149,6 +149,8 @@ a full understanding of what it is doing internally! plt.ylabel('Yearly energy yield (W hr)') +.. _object-oriented: + Object oriented (Location, PVSystem, ModelChain) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -200,10 +202,6 @@ objects to accomplish our system modeling goal: @savefig modelchain-energies.png width=6in plt.ylabel('Yearly energy yield (W hr)') -See Will Holmgren's -`ModelChain gist `_ -for more discussion about new features in ModelChain. - Object oriented (LocalizedPVSystem) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/sphinx/source/whatsnew.rst b/docs/sphinx/source/whatsnew.rst index daff232eb7..37870b40ec 100644 --- a/docs/sphinx/source/whatsnew.rst +++ b/docs/sphinx/source/whatsnew.rst @@ -6,7 +6,8 @@ What's New These are new features and improvements of note in each release. -.. include:: whatsnew/v0.4.6.rst +.. include:: whatsnew/v0.5.1.rst +.. include:: whatsnew/v0.5.0.rst .. include:: whatsnew/v0.4.5.txt .. include:: whatsnew/v0.4.4.txt .. include:: whatsnew/v0.4.3.txt diff --git a/docs/sphinx/source/whatsnew/v0.4.6.rst b/docs/sphinx/source/whatsnew/v0.4.6.rst deleted file mode 100644 index 063a2b9019..0000000000 --- a/docs/sphinx/source/whatsnew/v0.4.6.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. _whatsnew_0460: - -v0.4.6 () ---------- - - -Bug fixes -~~~~~~~~~ - -* Method of multi-inheritance has changed to make it possible to use kwargs in - the parent classes of LocalizedPVSystem and LocalizedSingleAxisTracker - (:issue:`330`) - - -Enhancements -~~~~~~~~~~~~ -* Added default values to docstrings of all functions (:issue:`336`) - -API Changes -~~~~~~~~~~~ -* Removed parameter w from _calc_d (:issue:`344`) - -Documentation -~~~~~~~~~~~~~ - - -Testing -~~~~~~~ - -* Added explicit tests for aoi and aoi_projection functions. - - -Contributors -~~~~~~~~~~~~ - -* Will Holmgren -* Uwe Krien -* Alaina Kafkes -* Birgit Schachler diff --git a/docs/sphinx/source/whatsnew/v0.5.0.rst b/docs/sphinx/source/whatsnew/v0.5.0.rst new file mode 100644 index 0000000000..0ade84c4d3 --- /dev/null +++ b/docs/sphinx/source/whatsnew/v0.5.0.rst @@ -0,0 +1,55 @@ +.. _whatsnew_0500: + +v0.5.0 (August 11, 2017) +------------------------ + +API Changes +~~~~~~~~~~~ +* Removed parameter w from _calc_d (:issue:`344`) +* SingleAxisTracker.get_aoi and SingleAxisTracker.get_irradiance + now require surface_zenith and surface_azimuth (:issue:`351`) +* Changes calculation of the Incidence Angle Modifier to return 0 + instead of np.nan for angles >= 90°. This improves the calculation of + effective irradiance close to sunrise and sunset. (:issue:`338`) +* Change the default ModelChain orientation strategy from + 'south_at_latitude_tilt' to ``None``. (:issue:`290`) + +Bug fixes +~~~~~~~~~ +* Method of multi-inheritance has changed to make it possible to use kwargs in + the parent classes of LocalizedPVSystem and LocalizedSingleAxisTracker + (:issue:`330`) +* Fix the `__repr__` method of `ModelChain`, crashing when + `orientation_strategy` is set to `'None'` (:issue:`352`) +* Fix the `ModelChain`'s angle of incidence calculation for + SingleAxisTracker objects (:issue:`351`) +* Fix issue with ForecastModel.cloud_cover_to_transmittance_linear method of + forecast.py ignoring 'offset' parameter. (:issue:`343`) + +Enhancements +~~~~~~~~~~~~ +* Added default values to docstrings of all functions (:issue:`336`) +* Added analytical method that calculates solar azimuth angle (:issue:`291`) + +Documentation +~~~~~~~~~~~~~ +* Added ModelChain documentation page +* Added nbsphinx to documentation build configuration. +* Added a pull request template file (:issue:`354`) + +Testing +~~~~~~~ +* Added explicit tests for aoi and aoi_projection functions. +* Update test of `ModelChain.__repr__` to take in account :issue:`352` +* Added a test for solar_azimuth_analytical function. + +Contributors +~~~~~~~~~~~~ +* Johannes Kaufmann +* Will Holmgren +* Uwe Krien +* Alaina Kafkes +* Birgit Schachler +* Jonathan Gaffiot +* Siyan (Veronica) Guo +* KonstantinTr diff --git a/docs/sphinx/source/whatsnew/v0.5.1.rst b/docs/sphinx/source/whatsnew/v0.5.1.rst new file mode 100644 index 0000000000..8ebde38228 --- /dev/null +++ b/docs/sphinx/source/whatsnew/v0.5.1.rst @@ -0,0 +1,28 @@ +.. _whatsnew_0500: + +v0.5.0 (August 11, 2017) +------------------------ + +API Changes +~~~~~~~~~~~ +* + +Bug fixes +~~~~~~~~~ +* Remove condition causing Overflow warning from clearsky.haurwitz + +Enhancements +~~~~~~~~~~~~ +* + +Documentation +~~~~~~~~~~~~~ +* + +Testing +~~~~~~~ +* Changed test for clearsky.haurwitz to operate on zenith angles + +Contributors +~~~~~~~~~~~~ +* Cliff Hansen diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index 8d56751449..4617d8d832 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -294,9 +294,9 @@ def haurwitz(apparent_zenith): Implements the Haurwitz clear sky model for global horizontal irradiance (GHI) as presented in [1, 2]. A report on clear - sky models found the Haurwitz model to have the best performance of - models which require only zenith angle [3]. Extreme care should - be taken in the interpretation of this result! + sky models found the Haurwitz model to have the best performance + in terms of average monthly error among models which require only + zenith angle [3]. Parameters ---------- @@ -306,7 +306,7 @@ def haurwitz(apparent_zenith): Returns ------- - pd.Series + pd.DataFrame The modeled global horizonal irradiance in W/m^2 provided by the Haurwitz clear-sky model. @@ -326,13 +326,14 @@ def haurwitz(apparent_zenith): Laboratories, SAND2012-2389, 2012. ''' - cos_zenith = tools.cosd(apparent_zenith) + cos_zenith = tools.cosd(apparent_zenith.values) + clearsky_ghi = np.zeros_like(apparent_zenith.values) + clearsky_ghi[cos_zenith>0] = 1098.0 * cos_zenith[cos_zenith>0] * \ + np.exp(-0.059/cos_zenith[cos_zenith>0]) - clearsky_ghi = 1098.0 * cos_zenith * np.exp(-0.059/cos_zenith) - - clearsky_ghi[clearsky_ghi < 0] = 0 - - df_out = pd.DataFrame({'ghi': clearsky_ghi}) + df_out = pd.DataFrame(index=apparent_zenith.index, + data=clearsky_ghi, + columns=['ghi']) return df_out diff --git a/pvlib/data/variables_style_rules.csv b/pvlib/data/variables_style_rules.csv index 236d5f6ccc..0e0758beaa 100644 --- a/pvlib/data/variables_style_rules.csv +++ b/pvlib/data/variables_style_rules.csv @@ -6,7 +6,7 @@ dni;direct normal irradiance dni_extra;direct normal irradiance at top of atmosphere (extraterrestrial) dhi;diffuse horizontal irradiance ghi;global horizontal irradiance -aoi;angle of incidence +aoi;angle of incidence between :math:`90\deg` and :math:`90\deg` aoi_projection;cos(aoi) airmass;airmass airmass_relative;relative airmass diff --git a/pvlib/forecast.py b/pvlib/forecast.py index b313379be7..cfa44be0c1 100644 --- a/pvlib/forecast.py +++ b/pvlib/forecast.py @@ -473,7 +473,7 @@ def cloud_cover_to_transmittance_linear(self, cloud_cover, offset=0.75, ghi : numeric Estimated GHI. """ - transmittance = ((100.0 - cloud_cover) / 100.0) * 0.75 + transmittance = ((100.0 - cloud_cover) / 100.0) * offset return transmittance diff --git a/pvlib/location.py b/pvlib/location.py index 58557a7d33..e64ce52d53 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -82,7 +82,7 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0, def __repr__(self): attrs = ['name', 'latitude', 'longitude', 'altitude', 'tz'] return ('Location: \n ' + '\n '.join( - (attr + ': ' + str(getattr(self, attr)) for attr in attrs))) + ('{}: {}'.format(attr, getattr(self, attr)) for attr in attrs))) @classmethod def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): @@ -226,8 +226,8 @@ def get_clearsky(self, times, model='ineichen', solar_position=None, apparent_elevation, pressure=pressure, dni_extra=dni_extra, **kwargs) else: - raise ValueError(('{} is not a valid clear sky model. Must be ' + - 'one of ineichen, simplified_solis, haurwitz') + raise ValueError('{} is not a valid clear sky model. Must be ' + 'one of ineichen, simplified_solis, haurwitz' .format(model)) return cs diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 567a70b6bb..8406aba80f 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -112,7 +112,7 @@ def basic_chain(times, latitude, longitude, surface_tilt, surface_azimuth = \ get_orientation(orientation_strategy, latitude=latitude) else: - raise ValueError('orientation_strategy or surface_tilt and ' + + raise ValueError('orientation_strategy or surface_tilt and ' 'surface_azimuth must be provided') times = times @@ -208,7 +208,7 @@ def get_orientation(strategy, **kwargs): surface_azimuth = 180 surface_tilt = 0 else: - raise ValueError('invalid orientation strategy. strategy must ' + + raise ValueError('invalid orientation strategy. strategy must ' 'be one of south_at_latitude, flat,') return surface_tilt, surface_azimuth @@ -294,7 +294,7 @@ class ModelChain(object): """ def __init__(self, system, location, - orientation_strategy='south_at_latitude_tilt', + orientation_strategy=None, clearsky_model='ineichen', transposition_model='haydavies', solar_position_method='nrel_numpy', @@ -344,7 +344,7 @@ def getmcattr(self, attr): return out return ('ModelChain: \n ' + '\n '.join( - (attr + ': ' + getmcattr(self, attr) for attr in attrs))) + ('{}: {}'.format(attr, getmcattr(self, attr)) for attr in attrs))) @property def orientation_strategy(self): @@ -391,7 +391,7 @@ def infer_dc_model(self): elif set(['pdc0', 'gamma_pdc']) <= params: return self.pvwatts_dc else: - raise ValueError('could not infer DC model from ' + + raise ValueError('could not infer DC model from ' 'system.module_parameters') def sapm(self): @@ -455,7 +455,7 @@ def infer_ac_model(self): elif set(['pdc0']) <= module_params: return self.pvwatts_inverter else: - raise ValueError('could not infer AC model from ' + + raise ValueError('could not infer AC model from ' 'system.inverter_parameters') def snlinverter(self): @@ -502,7 +502,7 @@ def infer_aoi_model(self): elif set(['b']) <= params: return self.ashrae_aoi_loss else: - raise ValueError('could not infer AOI model from ' + + raise ValueError('could not infer AOI model from ' 'system.module_parameters') def ashrae_aoi_loss(self): @@ -547,7 +547,7 @@ def infer_spectral_model(self): if set(['A4', 'A3', 'A2', 'A1', 'A0']) <= params: return self.sapm_spectral_loss else: - raise ValueError('could not infer spectral model from ' + + raise ValueError('could not infer spectral model from ' 'system.module_parameters') def first_solar_spectral_loss(self): @@ -757,9 +757,6 @@ def prepare_inputs(self, times=None, irradiance=None, weather=None): self.airmass = self.location.get_airmass( solar_position=self.solar_position, model=self.airmass_model) - self.aoi = self.system.get_aoi(self.solar_position['apparent_zenith'], - self.solar_position['azimuth']) - if not any([x in ['ghi', 'dni', 'dhi'] for x in self.weather.columns]): self.weather[['ghi', 'dni', 'dhi']] = self.location.get_clearsky( self.solar_position.index, self.clearsky_model, @@ -773,7 +770,8 @@ def prepare_inputs(self, times=None, irradiance=None, weather=None): "Detected data: {0}".format(list(self.weather.columns))) # PVSystem.get_irradiance and SingleAxisTracker.get_irradiance - # have different method signatures, so use partial to handle + # and PVSystem.get_aoi and SingleAxisTracker.get_aoi + # have different method signatures. Use partial to handle # the differences. if isinstance(self.system, SingleAxisTracker): self.tracking = self.system.singleaxis( @@ -785,13 +783,17 @@ def prepare_inputs(self, times=None, irradiance=None, weather=None): self.tracking['surface_azimuth'] = ( self.tracking['surface_azimuth'] .fillna(self.system.axis_azimuth)) + self.aoi = self.tracking['aoi'] get_irradiance = partial( self.system.get_irradiance, - surface_tilt=self.tracking['surface_tilt'], - surface_azimuth=self.tracking['surface_azimuth'], - solar_zenith=self.solar_position['apparent_zenith'], - solar_azimuth=self.solar_position['azimuth']) + self.tracking['surface_tilt'], + self.tracking['surface_azimuth'], + self.solar_position['apparent_zenith'], + self.solar_position['azimuth']) else: + self.aoi = self.system.get_aoi( + self.solar_position['apparent_zenith'], + self.solar_position['azimuth']) get_irradiance = partial( self.system.get_irradiance, self.solar_position['apparent_zenith'], diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 886681114a..fdd7e2d9be 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -156,7 +156,7 @@ def __repr__(self): attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module', 'inverter', 'albedo', 'racking_model'] return ('PVSystem: \n ' + '\n '.join( - (attr + ': ' + str(getattr(self, attr)) for attr in attrs))) + ('{}: {}'.format(attr, getattr(self, attr)) for attr in attrs))) def get_aoi(self, solar_zenith, solar_azimuth): """Get the angle of incidence on the system. @@ -593,7 +593,7 @@ def __repr__(self): 'surface_azimuth', 'module', 'inverter', 'albedo', 'racking_model' ] return ('LocalizedPVSystem: \n ' + '\n '.join( - (attr + ': ' + str(getattr(self, attr)) for attr in attrs))) + ('{}: {}'.format(attr, getattr(self, attr)) for attr in attrs))) def systemdef(meta, surface_tilt, surface_azimuth, albedo, modules_per_string, @@ -700,7 +700,7 @@ def ashraeiam(aoi, b=0.05): ---------- aoi : numeric The angle of incidence between the module normal vector and the - sun-beam vector in degrees. + sun-beam vector in degrees. Angles of nan will result in nan. b : float, default 0.05 A parameter to adjust the modifier as a function of angle of @@ -712,7 +712,7 @@ def ashraeiam(aoi, b=0.05): The incident angle modifier calculated as 1-b*(sec(aoi)-1) as described in [2,3]. - Returns nan for all abs(aoi) >= 90 and for all IAM values that + Returns zeros for all abs(aoi) >= 90 and for all IAM values that would be less than 0. References @@ -735,7 +735,7 @@ def ashraeiam(aoi, b=0.05): iam = 1 - b*((1/np.cos(np.radians(aoi)) - 1)) - iam = np.where(np.abs(aoi) >= 90, np.nan, iam) + iam = np.where(np.abs(aoi) >= 90, 0, iam) iam = np.maximum(0, iam) if isinstance(iam, pd.Series): @@ -764,7 +764,8 @@ def physicaliam(aoi, n=1.526, K=4., L=0.002): ---------- aoi : numeric The angle of incidence between the module normal vector and the - sun-beam vector in degrees. + sun-beam vector in degrees. Angles of 0 are replaced with 1e-06 + to ensure non-nan results. Angles of nan will result in nan. n : numeric, default 1.526 The effective index of refraction (unitless). Reference [1] @@ -814,27 +815,40 @@ def physicaliam(aoi, n=1.526, K=4., L=0.002): spa ashraeiam ''' + zeroang = 1e-06 + + aoi = np.where(aoi == 0, zeroang, aoi) + + # angle of reflection thetar_deg = tools.asind(1.0 / n*(tools.sind(aoi))) - tau = (np.exp(- 1.0 * (K*L / tools.cosd(thetar_deg))) * - ((1 - 0.5*((((tools.sind(thetar_deg - aoi)) ** 2) / - ((tools.sind(thetar_deg + aoi)) ** 2) + - ((tools.tand(thetar_deg - aoi)) ** 2) / - ((tools.tand(thetar_deg + aoi)) ** 2)))))) + # reflectance and transmittance for normal incidence light + rho_zero = ((1-n) / (1+n)) ** 2 + tau_zero = np.exp(-K*L) - zeroang = 1e-06 + # reflectance for parallel and perpendicular polarized light + rho_para = (tools.tand(thetar_deg - aoi) / + tools.tand(thetar_deg + aoi)) ** 2 + rho_perp = (tools.sind(thetar_deg - aoi) / + tools.sind(thetar_deg + aoi)) ** 2 + + # transmittance for non-normal light + tau = np.exp(-K*L / tools.cosd(thetar_deg)) - thetar_deg0 = tools.asind(1.0 / n*(tools.sind(zeroang))) + # iam is ratio of non-normal to normal incidence transmitted light + # after deducting the reflected portion of each + iam = ((1 - (rho_para + rho_perp) / 2) / (1 - rho_zero) * tau / tau_zero) - tau0 = (np.exp(- 1.0 * (K*L / tools.cosd(thetar_deg0))) * - ((1 - 0.5*((((tools.sind(thetar_deg0 - zeroang)) ** 2) / - ((tools.sind(thetar_deg0 + zeroang)) ** 2) + - ((tools.tand(thetar_deg0 - zeroang)) ** 2) / - ((tools.tand(thetar_deg0 + zeroang)) ** 2)))))) + # angles near zero produce nan, but iam is defined as one + small_angle = 1e-06 + iam = np.where(np.abs(aoi) < small_angle, 1.0, iam) - iam = tau / tau0 + # angles at 90 degrees can produce tiny negative values, which should be zero + # this is a result of calculation precision rather than the physical model + iam = np.where(iam < 0, 0, iam) - iam = np.where((np.abs(aoi) >= 90) | (iam < 0), np.nan, iam) + # for light coming from behind the plane, none can enter the module + iam = np.where(aoi > 90, 0, iam) if isinstance(aoi, pd.Series): iam = pd.Series(iam, index=aoi.index) @@ -1465,7 +1479,7 @@ def sapm_aoi_loss(aoi, module, upper=None): ---------- aoi : numeric Angle of incidence in degrees. Negative input angles will return - nan values. + zeros. module : dict-like A dict, Series, or DataFrame defining the SAPM performance @@ -1507,7 +1521,7 @@ def sapm_aoi_loss(aoi, module, upper=None): aoi_loss = np.polyval(aoi_coeff, aoi) aoi_loss = np.clip(aoi_loss, 0, upper) - aoi_loss = np.where(aoi < 0, np.nan, aoi_loss) + aoi_loss = np.where(aoi < 0, 0, aoi_loss) if isinstance(aoi, pd.Series): aoi_loss = pd.Series(aoi_loss, aoi.index) @@ -2084,8 +2098,8 @@ def adrinverter(v_dc, p_dc, inverter, vtol=0.10): See Notes for required keys. vtol : numeric, default 0.1 - A unit-less fraction that determines how far the efficiency model is - allowed to extrapolate beyond the inverter's normal input voltage + A unit-less fraction that determines how far the efficiency model is + allowed to extrapolate beyond the inverter's normal input voltage operating range. 0.0 <= vtol <= 1.0 Returns @@ -2109,21 +2123,21 @@ def adrinverter(v_dc, p_dc, inverter, vtol=0.10): Column Description ======= ============================================================ p_nom The nominal power value used to normalize all power values, - typically the DC power needed to produce maximum AC power + typically the DC power needed to produce maximum AC power output, (W). - v_nom The nominal DC voltage value used to normalize DC voltage - values, typically the level at which the highest efficiency + v_nom The nominal DC voltage value used to normalize DC voltage + values, typically the level at which the highest efficiency is achieved, (V). - pac_max The maximum AC output power value, used to clip the output + pac_max The maximum AC output power value, used to clip the output if needed, (W). ce_list This is a list of 9 coefficients that capture the influence of input voltage and power on inverter losses, and thereby efficiency. - p_nt ac-power consumed by inverter at night (night tare) to + p_nt ac-power consumed by inverter at night (night tare) to maintain circuitry required to sense PV array voltage, (W). ======= ============================================================ diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 7b035323e9..562f74192a 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1052,6 +1052,53 @@ def declination_cooper69(dayofyear): return np.deg2rad(23.45 * np.sin(day_angle + (2.0 * np.pi / 365.0) * 285.0)) +def solar_azimuth_analytical(latitude, hour_angle, declination, zenith): + """ + Analytical expression of solar azimuth angle based on spherical + trigonometry. + + Parameters + ---------- + latitude : numeric + Latitude of location in radians. + hour_angle : numeric + Hour angle in the local solar time in radians. + declination : numeric + Declination of the sun in radians. + zenith : numeric + Solar zenith angle in radians. + + Returns + ------- + azimuth : numeric + Solar azimuth angle in radians. + + References + ---------- + [1] J. A. Duffie and W. A. Beckman, "Solar Engineering of Thermal + Processes, 3rd Edition" pp. 14, J. Wiley and Sons, New York (2006) + + [2] J. H. Seinfeld and S. N. Pandis, "Atmospheric Chemistry and Physics" + p. 132, J. Wiley (1998) + + [3] `Wikipedia: Solar Azimuth Angle + `_ + + [4] `PVCDROM: Azimuth Angle `_ + + See Also + -------- + declination_spencer71 + declination_cooper69 + hour_angle + solar_zenith_analytical + """ + return np.sign(hour_angle) * np.abs(np.arccos((np.cos(zenith) * np.sin( + latitude) - np.sin(declination)) / (np.sin(zenith) * np.cos( + latitude)))) + np.pi + + def solar_zenith_analytical(latitude, hour_angle, declination): """ Analytical expression of solar zenith angle based on spherical trigonometry. diff --git a/pvlib/test/test_clearsky.py b/pvlib/test/test_clearsky.py index 15b7db0aaa..07cf7d1fd1 100644 --- a/pvlib/test/test_clearsky.py +++ b/pvlib/test/test_clearsky.py @@ -234,25 +234,24 @@ def test_lookup_linke_turbidity_nointerp_months(): def test_haurwitz(): - tus = Location(32.2, -111, 'US/Arizona', 700) - times = pd.date_range(start='2014-06-24', end='2014-06-25', freq='3h') - times_localized = times.tz_localize(tus.tz) - ephem_data = solarposition.get_solarposition(times_localized, tus.latitude, - tus.longitude) - expected = pd.DataFrame(np.array([[0.], - [0.], - [82.85934048], - [699.74514735], - [1016.50198354], - [838.32103769], - [271.90853863], - [0.], - [0.]]), - columns=['ghi'], index=times_localized) - out = clearsky.haurwitz(ephem_data['zenith']) + apparent_solar_elevation = np.array([-20, -0.05, -0.001, 5, 10, 30, 50, 90]) + apparent_solar_zenith = 90 - apparent_solar_elevation + data_in = pd.DataFrame(data=apparent_solar_zenith, + index=apparent_solar_zenith, + columns=['apparent_zenith']) + expected = pd.DataFrame(np.array([0., + 0., + 0., + 48.6298687941956, + 135.741748091813, + 487.894132885425, + 778.766689344363, + 1035.09203253450]), + columns=['ghi'], + index=apparent_solar_zenith) + out = clearsky.haurwitz(data_in['apparent_zenith']) assert_frame_equal(expected, out) - def test_simplified_solis_series_elevation(): tus = Location(32.2, -111, 'US/Arizona', 700) times = pd.date_range(start='2014-06-24', end='2014-06-25', freq='3h') diff --git a/pvlib/test/test_forecast.py b/pvlib/test/test_forecast.py index 44575f0856..bd59d622f8 100644 --- a/pvlib/test/test_forecast.py +++ b/pvlib/test/test_forecast.py @@ -129,6 +129,7 @@ def test_cloud_cover_to_transmittance_linear(): amodel = GFS() assert_allclose(amodel.cloud_cover_to_transmittance_linear(0), 0.75) assert_allclose(amodel.cloud_cover_to_transmittance_linear(100), 0.0) + assert_allclose(amodel.cloud_cover_to_transmittance_linear(0, 0.5), 0.5) def test_cloud_cover_to_ghi_linear(): diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 91e004d3b7..aa5726f2cc 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -21,7 +21,8 @@ def system(sam_data): module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'].copy() inverters = sam_data['cecinverter'] inverter = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'].copy() - system = PVSystem(module_parameters=module_parameters, + system = PVSystem(surface_tilt=32.2, surface_azimuth=180, + module_parameters=module_parameters, inverter_parameters=inverter) return system @@ -35,7 +36,8 @@ def cec_dc_snl_ac_system(sam_data): module_parameters['dEgdT'] = -0.0002677 inverters = sam_data['cecinverter'] inverter = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'].copy() - system = PVSystem(module_parameters=module_parameters, + system = PVSystem(surface_tilt=32.2, surface_azimuth=180, + module_parameters=module_parameters, inverter_parameters=inverter) return system @@ -49,7 +51,8 @@ def cec_dc_adr_ac_system(sam_data): module_parameters['dEgdT'] = -0.0002677 inverters = sam_data['adrinverter'] inverter = inverters['Zigor__Sunzet_3_TL_US_240V__CEC_2011_'].copy() - system = PVSystem(module_parameters=module_parameters, + system = PVSystem(surface_tilt=32.2, surface_azimuth=180, + module_parameters=module_parameters, inverter_parameters=inverter) return system @@ -59,7 +62,8 @@ def pvwatts_dc_snl_ac_system(sam_data): module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} inverters = sam_data['cecinverter'] inverter = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'].copy() - system = PVSystem(module_parameters=module_parameters, + system = PVSystem(surface_tilt=32.2, surface_azimuth=180, + module_parameters=module_parameters, inverter_parameters=inverter) return system @@ -68,7 +72,8 @@ def pvwatts_dc_snl_ac_system(sam_data): def pvwatts_dc_pvwatts_ac_system(sam_data): module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} inverter_parameters = {'eta_inv_nom': 0.95} - system = PVSystem(module_parameters=module_parameters, + system = PVSystem(surface_tilt=32.2, surface_azimuth=180, + module_parameters=module_parameters, inverter_parameters=inverter_parameters) return system @@ -82,12 +87,8 @@ def test_ModelChain_creation(system, location): mc = ModelChain(system, location) -def test_orientation_strategy(system, location): - strategies = {} - - @pytest.mark.parametrize('strategy, expected', [ - (None, (0, 180)), ('None', (0, 180)), ('flat', (0, 180)), + (None, (32.2, 180)), ('None', (32.2, 180)), ('flat', (0, 180)), ('south_at_latitude_tilt', (32.2, 180)) ]) def test_orientation_strategy(strategy, expected, system, location): @@ -168,7 +169,7 @@ def test_run_model_tracker(system, location): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') ac = mc.run_model(times).ac - expected = pd.Series(np.array([ 122.333764454, -2.00000000e-02]), + expected = pd.Series(np.array([119.067713606, nan]), index=times) assert_series_equal(ac, expected, check_less_precise=2) @@ -417,9 +418,10 @@ def test_basic_chain_altitude_pressure(sam_data): assert_series_equal(ac, expected, check_less_precise=2) -def test_ModelChain___repr__(system, location): - - strategy = 'south_at_latitude_tilt' +@pytest.mark.parametrize('strategy, strategy_str', [ + ('south_at_latitude_tilt', 'south_at_latitude_tilt'), + (None, 'None')]) # GitHub issue 352 +def test_ModelChain___repr__(system, location, strategy, strategy_str): mc = ModelChain(system, location, orientation_strategy=strategy, name='my mc') @@ -427,7 +429,7 @@ def test_ModelChain___repr__(system, location): expected = '\n'.join([ 'ModelChain: ', ' name: my mc', - ' orientation_strategy: south_at_latitude_tilt', + ' orientation_strategy: ' + strategy_str, ' clearsky_model: ineichen', ' transposition_model: haydavies', ' solar_position_method: nrel_numpy', @@ -442,7 +444,6 @@ def test_ModelChain___repr__(system, location): assert mc.__repr__() == expected - @requires_scipy def test_weather_irradiance_input(system, location): """Test will raise a warning and should be removed in future versions.""" diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 841601869d..9283536846 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -94,10 +94,10 @@ def test_systemdef_dict(): @needs_numpy_1_10 def test_ashraeiam(): - thetas = np.linspace(-90, 90, 9) + thetas = np.array([-90. , -67.5, -45. , -22.5, 0. , 22.5, 45. , 67.5, 89., 90. , np.nan]) iam = pvsystem.ashraeiam(thetas, .05) - expected = np.array([ nan, 0.9193437 , 0.97928932, 0.99588039, 1. , - 0.99588039, 0.97928932, 0.9193437 , nan]) + expected = np.array([ 0, 0.9193437 , 0.97928932, 0.99588039, 1. , + 0.99588039, 0.97928932, 0.9193437 , 0, 0, np.nan]) assert_allclose(iam, expected, equal_nan=True) @@ -105,19 +105,19 @@ def test_ashraeiam(): def test_PVSystem_ashraeiam(): module_parameters = pd.Series({'b': 0.05}) system = pvsystem.PVSystem(module_parameters=module_parameters) - thetas = np.linspace(-90, 90, 9) + thetas = np.array([-90. , -67.5, -45. , -22.5, 0. , 22.5, 45. , 67.5, 89., 90. , np.nan]) iam = system.ashraeiam(thetas) - expected = np.array([ nan, 0.9193437 , 0.97928932, 0.99588039, 1. , - 0.99588039, 0.97928932, 0.9193437 , nan]) + expected = np.array([ 0, 0.9193437 , 0.97928932, 0.99588039, 1. , + 0.99588039, 0.97928932, 0.9193437 , 0, 0, np.nan]) assert_allclose(iam, expected, equal_nan=True) @needs_numpy_1_10 def test_physicaliam(): - thetas = np.linspace(-90, 90, 9) + thetas = np.array([-90. , -67.5, -45. , -22.5, 0. , 22.5, 45. , 67.5, 90. , np.nan]) iam = pvsystem.physicaliam(thetas, 1.526, 0.002, 4) - expected = np.array([ nan, 0.8893998 , 0.98797788, 0.99926198, nan, - 0.99926198, 0.98797788, 0.8893998 , nan]) + expected = np.array([ 0, 0.8893998, 0.98797788, 0.99926198, 1, + 0.99926198, 0.98797788, 0.8893998, 0, np.nan]) assert_allclose(iam, expected, equal_nan=True) @@ -125,10 +125,10 @@ def test_physicaliam(): def test_PVSystem_physicaliam(): module_parameters = pd.Series({'K': 4, 'L': 0.002, 'n': 1.526}) system = pvsystem.PVSystem(module_parameters=module_parameters) - thetas = np.linspace(-90, 90, 9) + thetas = np.array([-90. , -67.5, -45. , -22.5, 0. , 22.5, 45. , 67.5, 90. , np.nan]) iam = system.physicaliam(thetas) - expected = np.array([ nan, 0.8893998 , 0.98797788, 0.99926198, nan, - 0.99926198, 0.98797788, 0.8893998 , nan]) + expected = np.array([ 0, 0.8893998 , 0.98797788, 0.99926198, 1, + 0.99926198, 0.98797788, 0.8893998 , 0, np.nan]) assert_allclose(iam, expected, equal_nan=True) @@ -239,7 +239,7 @@ def test_PVSystem_sapm_spectral_loss(sapm_module_params): @pytest.mark.parametrize('aoi,expected', [ (45, 0.9975036250000002), (np.array([[-30, 30, 100, np.nan]]), - np.array([[np.nan, 1.007572, 0, np.nan]])), + np.array([[0, 1.007572, 0, np.nan]])), (pd.Series([80]), pd.Series([0.597472])) ]) def test_sapm_aoi_loss(sapm_module_params, aoi, expected): @@ -570,7 +570,7 @@ def test_PVSystem_sapm_celltemp(): def test_adrinverter(sam_data): inverters = sam_data['adrinverter'] - testinv = 'Ablerex_Electronics_Co___Ltd___' + \ + testinv = 'Ablerex_Electronics_Co___Ltd___' \ 'ES_2200_US_240__240_Vac__240V__CEC_2011_' vdcs = pd.Series([135, 154, 390, 420, 551]) pdcs = pd.Series([135, 1232, 1170, 420, 551]) @@ -582,7 +582,7 @@ def test_adrinverter(sam_data): def test_adrinverter_vtol(sam_data): inverters = sam_data['adrinverter'] - testinv = 'Ablerex_Electronics_Co___Ltd___' + \ + testinv = 'Ablerex_Electronics_Co___Ltd___' \ 'ES_2200_US_240__240_Vac__240V__CEC_2011_' vdcs = pd.Series([135, 154, 390, 420, 551]) pdcs = pd.Series([135, 1232, 1170, 420, 551]) @@ -594,7 +594,7 @@ def test_adrinverter_vtol(sam_data): def test_adrinverter_float(sam_data): inverters = sam_data['adrinverter'] - testinv = 'Ablerex_Electronics_Co___Ltd___' + \ + testinv = 'Ablerex_Electronics_Co___Ltd___' \ 'ES_2200_US_240__240_Vac__240V__CEC_2011_' vdcs = 154. pdcs = 1232. diff --git a/pvlib/test/test_solarposition.py b/pvlib/test/test_solarposition.py index bf802d7e53..0584dd744e 100644 --- a/pvlib/test/test_solarposition.py +++ b/pvlib/test/test_solarposition.py @@ -420,3 +420,33 @@ def test_analytical_zenith(): zenith_2 = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl) assert np.allclose(zenith_1, solar_zenith, atol=0.015) assert np.allclose(zenith_2, solar_zenith, atol=0.025) + + +def test_analytical_azimuth(): + times = pd.DatetimeIndex(start="1/1/2015 0:00", end="12/31/2015 23:00", + freq="H").tz_localize('Etc/GMT+8') + lat, lon = 37.8, -122.25 + lat_rad = np.deg2rad(lat) + output = solarposition.spa_python(times, lat, lon, 100) + solar_azimuth = np.deg2rad(output['azimuth']) # spa + solar_zenith = np.deg2rad(output['zenith']) + # spencer + eot = solarposition.equation_of_time_spencer71(times.dayofyear) + hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot)) + decl = solarposition.declination_spencer71(times.dayofyear) + zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl) + azimuth_1 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle, + decl, zenith) + # pvcdrom and cooper + eot = solarposition.equation_of_time_pvcdrom(times.dayofyear) + hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot)) + decl = solarposition.declination_cooper69(times.dayofyear) + zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl) + azimuth_2 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle, + decl, zenith) + + idx = np.where(solar_zenith < np.pi/2) + assert np.allclose(azimuth_1[idx], solar_azimuth.as_matrix()[idx], + atol=0.01) + assert np.allclose(azimuth_2[idx], solar_azimuth.as_matrix()[idx], + atol=0.017) diff --git a/pvlib/test/test_tracking.py b/pvlib/test/test_tracking.py index 360927a29c..a1ac8cbca4 100644 --- a/pvlib/test/test_tracking.py +++ b/pvlib/test/test_tracking.py @@ -6,9 +6,9 @@ import pytest from pandas.util.testing import assert_frame_equal +from numpy.testing import assert_allclose from pvlib.location import Location -from pvlib import solarposition from pvlib import tracking @@ -246,6 +246,21 @@ def test_SingleAxisTracker_localize_location(): assert localized_system.longitude == -111 +# see test_irradiance for more thorough testing +def test_get_aoi(): + system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, + axis_azimuth=180, gcr=2.0/7.0, + backtrack=True) + surface_tilt = np.array([30, 0]) + surface_azimuth = np.array([90, 270]) + solar_zenith = np.array([70, 10]) + solar_azimuth = np.array([100, 180]) + out = system.get_aoi(surface_tilt, surface_azimuth, + solar_zenith, solar_azimuth) + expected = np.array([40.632115, 10.]) + assert_allclose(out, expected, atol=0.000001) + + def test_get_irradiance(): system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, axis_azimuth=180, gcr=2.0/7.0, @@ -260,19 +275,17 @@ def test_get_irradiance(): solar_azimuth = solar_position['azimuth'] tracker_data = system.singleaxis(solar_zenith, solar_azimuth) - irradiance = system.get_irradiance(irrads['dni'], + irradiance = system.get_irradiance(tracker_data['surface_tilt'], + tracker_data['surface_azimuth'], + solar_zenith, + solar_azimuth, + irrads['dni'], irrads['ghi'], - irrads['dhi'], - solar_zenith=solar_zenith, - solar_azimuth=solar_azimuth, - surface_tilt=tracker_data['surface_tilt'], - surface_azimuth=tracker_data['surface_azimuth']) + irrads['dhi']) expected = pd.DataFrame(data=np.array( - [[ 961.80070, 815.94490, 145.85580, 135.32820, - 10.52757492], - [ nan, nan, nan, nan, - nan]]), + [[961.80070, 815.94490, 145.85580, 135.32820, 10.52757492], + [nan, nan, nan, nan, nan]]), columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', 'poa_ground_diffuse'], @@ -284,7 +297,7 @@ def test_get_irradiance(): def test_SingleAxisTracker___repr__(): system = tracking.SingleAxisTracker(max_angle=45, gcr=.25, module='blah', inverter='blarg') - expected = 'SingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n max_angle: 45\n backtrack: True\n gcr: 0.25\n name: None\n surface_tilt: 0\n surface_azimuth: 180\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback' + expected = 'SingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n max_angle: 45\n backtrack: True\n gcr: 0.25\n name: None\n surface_tilt: None\n surface_azimuth: None\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback' assert system.__repr__() == expected @@ -295,7 +308,6 @@ def test_LocalizedSingleAxisTracker___repr__(): inverter='blarg', gcr=0.25) - expected = 'LocalizedSingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n max_angle: 90\n backtrack: True\n gcr: 0.25\n name: None\n surface_tilt: 0\n surface_azimuth: 180\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback\n latitude: 32\n longitude: -111\n altitude: 0\n tz: UTC' + expected = 'LocalizedSingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n max_angle: 90\n backtrack: True\n gcr: 0.25\n name: None\n surface_tilt: None\n surface_azimuth: None\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback\n latitude: 32\n longitude: -111\n altitude: 0\n tz: UTC' assert localized_system.__repr__() == expected - diff --git a/pvlib/tmy.py b/pvlib/tmy.py index e1b9b78c96..833ecdd220 100644 --- a/pvlib/tmy.py +++ b/pvlib/tmy.py @@ -158,8 +158,8 @@ def readtmy3(filename=None, coerce_year=None, recolumn=True): try: filename = _interactive_load() except: - raise Exception('Interactive load failed. Tkinter not supported ' + - 'on this system. Try installing X-Quartz and ' + + raise Exception('Interactive load failed. Tkinter not supported ' + 'on this system. Try installing X-Quartz and ' 'reloading') head = ['USAF', 'Name', 'State', 'TZ', 'latitude', 'longitude', 'altitude'] @@ -467,20 +467,17 @@ def _read_tmy2(string, columns, hdr_columns, fname): try: val = float(val) except: - raise Exception('WARNING: In' + fname + - ' Read value is not an integer " ' + - val + ' " ') + raise Exception('WARNING: In {} Read value is not an ' + 'integer " {} " '.format(fname, val)) elif marker[-1] == 's': try: val = str(val) except: - raise Exception('WARNING: In' + fname + - ' Read value is not a string" ' + - val + ' " ') + raise Exception('WARNING: In {} Read value is not a ' + 'string " {} " '.format(fname, val)) else: - raise Exception('WARNING: In' + __name__ + - 'Improper column DataFrame " %' + - marker + ' " ') + raise Exception('WARNING: In {} Improper column DataFrame ' + '" %{} " '.format(__name__, marker)) part.append(val) diff --git a/pvlib/tracking.py b/pvlib/tracking.py index c14a69da2f..c23c9a9da5 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -14,7 +14,7 @@ class SingleAxisTracker(PVSystem): """ - Inherits all of the PV modeling methods from PVSystem. + Inherits the PV modeling methods from :ref:PVSystem:. axis_tilt : float, default 0 The tilt of the axis of rotation (i.e, the y-axis defined by @@ -54,12 +54,15 @@ def __init__(self, axis_tilt=0, axis_azimuth=0, self.backtrack = backtrack self.gcr = gcr + kwargs['surface_tilt'] = None + kwargs['surface_azimuth'] = None + super(SingleAxisTracker, self).__init__(**kwargs) def __repr__(self): attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', 'backtrack', 'gcr'] sat_repr = ('SingleAxisTracker: \n ' + '\n '.join( - (attr + ': ' + str(getattr(self, attr)) for attr in attrs))) + ('{}: {}'.format(attr, getattr(self, attr)) for attr in attrs))) # get the parent PVSystem info pvsystem_repr = super(SingleAxisTracker, self).__repr__() # remove the first line (contains 'PVSystem: \n') @@ -98,20 +101,60 @@ def localize(self, location=None, latitude=None, longitude=None, return LocalizedSingleAxisTracker(pvsystem=self, location=location) - def get_irradiance(self, dni, ghi, dhi, + def get_aoi(self, surface_tilt, surface_azimuth, solar_zenith, + solar_azimuth): + """Get the angle of incidence on the system. + + For a given set of solar zenith and azimuth angles, the + surface tilt and azimuth parameters are typically determined + by :py:method:`~SingleAxisTracker.singleaxis`. The + :py:method:`~SingleAxisTracker.singleaxis` method also returns + the angle of incidence, so this method is only needed + if using a different tracking algorithm. + + Parameters + ---------- + surface_tilt : numeric + Panel tilt from horizontal. + surface_azimuth : numeric + Panel azimuth from north + solar_zenith : float or Series. + Solar zenith angle. + solar_azimuth : float or Series. + Solar azimuth angle. + + Returns + ------- + aoi : Series + The angle of incidence in degrees from normal. + """ + + aoi = irradiance.aoi(surface_tilt, surface_azimuth, + solar_zenith, solar_azimuth) + return aoi + + def get_irradiance(self, surface_tilt, surface_azimuth, + solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, model='haydavies', **kwargs): """ Uses the :func:`irradiance.total_irrad` function to calculate the plane of array irradiance components on a tilted surface - defined by ``self.surface_tilt``, ``self.surface_azimuth``, and - ``self.albedo``. + defined by the input data and ``self.albedo``. + + For a given set of solar zenith and azimuth angles, the + surface tilt and azimuth parameters are typically determined + by :py:method:`~SingleAxisTracker.singleaxis`. Parameters ---------- - solar_zenith : float or Series. + surface_tilt : numeric + Panel tilt from horizontal. + surface_azimuth : numeric + Panel azimuth from north + solar_zenith : numeric Solar zenith angle. - solar_azimuth : float or Series. + solar_azimuth : numeric Solar azimuth angle. dni : float or Series Direct Normal Irradiance @@ -135,23 +178,9 @@ def get_irradiance(self, dni, ghi, dhi, Column names are: ``total, beam, sky, ground``. """ - surface_tilt = kwargs.pop('surface_tilt', self.surface_tilt) - surface_azimuth = kwargs.pop('surface_azimuth', self.surface_azimuth) - - try: - solar_zenith = kwargs['solar_zenith'] - except KeyError: - solar_zenith = self.solar_zenith - - try: - solar_azimuth = kwargs['solar_azimuth'] - except KeyError: - solar_azimuth = self.solar_azimuth - # not needed for all models, but this is easier if dni_extra is None: dni_extra = irradiance.extraradiation(solar_zenith.index) - dni_extra = pd.Series(dni_extra, index=solar_zenith.index) if airmass is None: airmass = atmosphere.relativeairmass(solar_zenith) @@ -196,8 +225,8 @@ def __repr__(self): attrs = ['latitude', 'longitude', 'altitude', 'tz'] return ('Localized' + super(LocalizedSingleAxisTracker, self).__repr__() + '\n ' + - '\n '.join( - (attr + ': ' + str(getattr(self, attr)) for attr in attrs))) + '\n '.join(('{}: {}'.format(attr, getattr(self, attr)) + for attr in attrs))) def singleaxis(apparent_zenith, apparent_azimuth, @@ -280,7 +309,7 @@ def singleaxis(apparent_zenith, apparent_azimuth, pvl_logger.debug('tracking.singleaxis') - pvl_logger.debug('axis_tilt=%s, axis_azimuth=%s, max_angle=%s, ' + + pvl_logger.debug('axis_tilt=%s, axis_azimuth=%s, max_angle=%s, ' 'backtrack=%s, gcr=%.3f', axis_tilt, axis_azimuth, max_angle, backtrack, gcr) @@ -310,7 +339,7 @@ def singleaxis(apparent_zenith, apparent_azimuth, pd.util.testing.assert_index_equal(apparent_azimuth.index, apparent_zenith.index) except AssertionError: - raise ValueError('apparent_azimuth.index and ' + + raise ValueError('apparent_azimuth.index and ' 'apparent_zenith.index must match.') times = apparent_azimuth.index