diff --git a/appveyor.yml b/appveyor.yml index ad8afa1efc..5d3d1a8ae5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -31,11 +31,13 @@ install: - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" - # install xray and depenencies - - "conda install --yes --quiet pip numpy scipy pandas nose pytz ephem numba" + # install depenencies + - "conda install --yes --quiet pip numpy scipy=0.16.0 pandas nose pytz ephem numba" + + # install pvlib - "python setup.py install" build: false test_script: - - "nosetests -v pvlib" \ No newline at end of file + - "nosetests -v pvlib" diff --git a/ci/requirements-py27-min.yml b/ci/requirements-py27-min.yml index 1cd6103267..fab0813e94 100644 --- a/ci/requirements-py27-min.yml +++ b/ci/requirements-py27-min.yml @@ -2,10 +2,8 @@ name: test_env dependencies: - python=2.7 - numpy==1.8.2 - - scipy - pandas==0.13.1 - nose - pytz - - ephem - pip: - coveralls \ No newline at end of file diff --git a/docs/environment.yml b/docs/environment.yml index ca06545e2d..2fd1519aae 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -1,13 +1,13 @@ name: pvlib dependencies: - - python=2.7 + - python=2.7 - numpy - scipy - pandas - pytz - ephem - numba - - ipython + - ipython=4.0.1 - sphinx - numpydoc - matplotlib diff --git a/docs/sphinx/source/_static/.gitignore b/docs/sphinx/source/_static/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/sphinx/source/classes.rst b/docs/sphinx/source/classes.rst new file mode 100644 index 0000000000..f1fc154cd2 --- /dev/null +++ b/docs/sphinx/source/classes.rst @@ -0,0 +1,55 @@ +.. _classes: + +Classes +======= + +pvlib-python provides a collection of classes +for users that prefer object-oriented programming. +These classes can help users keep track of data in a more organized way, +and can help to simplify the modeling process. +The classes do not add any functionality beyond the procedural code. +Most of the object methods are simple wrappers around the +corresponding procedural code. + +Location +-------- +.. autoclass:: pvlib.location.Location + :members: + :undoc-members: + :show-inheritance: + +PVSystem +-------- +.. autoclass:: pvlib.pvsystem.PVSystem + :members: + :undoc-members: + :show-inheritance: + +ModelChain +---------- +.. autoclass:: pvlib.modelchain.ModelChain + :members: + :undoc-members: + :show-inheritance: + +LocalizedPVSystem +----------------- +.. autoclass:: pvlib.pvsystem.LocalizedPVSystem + :members: + :undoc-members: + :show-inheritance: + +SingleAxisTracker +----------------- +.. autoclass:: pvlib.tracking.SingleAxisTracker + :members: + :undoc-members: + :show-inheritance: + +LocalizedSingleAxisTracker +-------------------------- +.. autoclass:: pvlib.tracking.LocalizedSingleAxisTracker + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/sphinx/source/conf.py b/docs/sphinx/source/conf.py index 24d41deb6a..a74ea4675d 100644 --- a/docs/sphinx/source/conf.py +++ b/docs/sphinx/source/conf.py @@ -37,6 +37,9 @@ def __getattr__(cls, name): # -- General configuration ------------------------------------------------ +# turns off numpydoc autosummary warnings +numpydoc_show_class_members = False + # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' @@ -127,7 +130,15 @@ def __getattr__(cls, name): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +else: + html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index 84e202cadb..dae85e27b8 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -60,11 +60,12 @@ Contents :maxdepth: 2 self + package_overview whatsnew + modules + classes comparison_pvlib_matlab variables_style_rules - pvlib - Indices and tables diff --git a/docs/sphinx/source/pvlib.rst b/docs/sphinx/source/modules.rst similarity index 83% rename from docs/sphinx/source/pvlib.rst rename to docs/sphinx/source/modules.rst index 8896f6dea2..3ae22dd0f4 100644 --- a/docs/sphinx/source/pvlib.rst +++ b/docs/sphinx/source/modules.rst @@ -1,7 +1,7 @@ Modules ======= -atmosphere module +atmosphere ----------------- .. automodule:: pvlib.atmosphere @@ -9,7 +9,7 @@ atmosphere module :undoc-members: :show-inheritance: -clearsky module +clearsky ---------------- .. automodule:: pvlib.clearsky @@ -17,7 +17,7 @@ clearsky module :undoc-members: :show-inheritance: -irradiance module +irradiance ----------------- .. automodule:: pvlib.irradiance @@ -25,7 +25,7 @@ irradiance module :undoc-members: :show-inheritance: -location module +location --------------- .. automodule:: pvlib.location @@ -33,7 +33,15 @@ location module :undoc-members: :show-inheritance: -pvsystem module +modelchain +---------- + +.. automodule:: pvlib.modelchain + :members: + :undoc-members: + :show-inheritance: + +pvsystem --------------- .. automodule:: pvlib.pvsystem @@ -41,7 +49,7 @@ pvsystem module :undoc-members: :show-inheritance: -solarposition module +solarposition -------------------- .. automodule:: pvlib.solarposition @@ -49,7 +57,7 @@ solarposition module :undoc-members: :show-inheritance: -tmy module +tmy -------------------- .. automodule:: pvlib.tmy @@ -57,7 +65,7 @@ tmy module :undoc-members: :show-inheritance: -tracking module +tracking -------------------- .. automodule:: pvlib.tracking @@ -65,7 +73,7 @@ tracking module :undoc-members: :show-inheritance: -tools module +tools -------------------- .. automodule:: pvlib.tools diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst new file mode 100644 index 0000000000..5e5d6eacbe --- /dev/null +++ b/docs/sphinx/source/package_overview.rst @@ -0,0 +1,284 @@ +.. _package_overview: + +Package Overview +================ + +Introduction +------------ + +The core mission of pvlib-python is to provide open, reliable, +interoperable, and benchmark implementations of PV system models. + +There are at least as many opinions about how to model PV systems as +there are modelers of PV systems, so +pvlib-python provides several modeling paradigms. + + +Modeling paradigms +------------------ + +The backbone of pvlib-python +is well-tested procedural code that implements PV system models. +pvlib-python also provides a collection of classes for users +that prefer object-oriented programming. +These classes can help users keep track of data in a more organized way, +provide some "smart" functions with more flexible inputs, +and simplify the modeling process for common situations. +The classes do not add any algorithms beyond what's available +in the procedural code, and most of the object methods +are simple wrappers around the corresponding procedural code. + +Let's use each of these pvlib modeling paradigms +to calculate the yearly energy yield for a given hardware +configuration at a handful of sites listed below. + +.. ipython:: python + + import pandas as pd + import matplotlib.pyplot as plt + + # seaborn makes the plots look nicer + import seaborn as sns + sns.set_color_codes() + + times = pd.DatetimeIndex(start='2015', end='2016', freq='1h') + + # very approximate + # latitude, longitude, name, altitude + coordinates = [(30, -110, 'Tucson', 700), + (35, -105, 'Albuquerque', 1500), + (40, -120, 'San Francisco', 10), + (50, 10, 'Berlin', 34)] + + import pvlib + + # get the module and inverter specifications from SAM + sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod') + sapm_inverters = pvlib.pvsystem.retrieve_sam('sandiainverter') + module = sandia_modules['Canadian_Solar_CS5P_220M___2009_'] + inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'] + + # specify constant ambient air temp and wind for simplicity + temp_air = 20 + wind_speed = 0 + + +Procedural +^^^^^^^^^^ + +The straightforward procedural code can be used for all modeling +steps in pvlib-python. + +The following code demonstrates how to use the procedural code +to accomplish our system modeling goal: + +.. ipython:: python + + system = {'module': module, 'inverter': inverter, + 'surface_azimuth': 180} + + energies = {} + for latitude, longitude, name, altitude in coordinates: + system['surface_tilt'] = latitude + cs = pvlib.clearsky.ineichen(times, latitude, longitude, altitude=altitude) + solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude) + dni_extra = pvlib.irradiance.extraradiation(times) + dni_extra = pd.Series(dni_extra, index=times) + airmass = pvlib.atmosphere.relativeairmass(solpos['apparent_zenith']) + pressure = pvlib.atmosphere.alt2pres(altitude) + am_abs = pvlib.atmosphere.absoluteairmass(airmass, pressure) + aoi = pvlib.irradiance.aoi(system['surface_tilt'], system['surface_azimuth'], + solpos['apparent_zenith'], solpos['azimuth']) + total_irrad = pvlib.irradiance.total_irrad(system['surface_tilt'], + system['surface_azimuth'], + solpos['apparent_zenith'], + solpos['azimuth'], + cs['dni'], cs['ghi'], cs['dhi'], + dni_extra=dni_extra, + model='haydavies') + temps = pvlib.pvsystem.sapm_celltemp(total_irrad['poa_global'], + wind_speed, temp_air) + dc = pvlib.pvsystem.sapm(module, total_irrad['poa_direct'], + total_irrad['poa_diffuse'], temps['temp_cell'], + am_abs, aoi) + ac = pvlib.pvsystem.snlinverter(inverter, dc['v_mp'], dc['p_mp']) + annual_energy = ac.sum() + energies[name] = annual_energy + + energies = pd.Series(energies) + + # based on the parameters specified above, these are in W*hrs + print(energies.round(0)) + + energies.plot(kind='bar', rot=0) + @savefig proc-energies.png width=6in + plt.ylabel('Yearly energy yield (W hr)') + +pvlib-python provides a :py:func:`~pvlib.modelchain.basic_chain` +function that implements much of the code above. Use this function with +a full understanding of what it is doing internally! + +.. ipython:: python + + from pvlib.modelchain import basic_chain + + energies = {} + for latitude, longitude, name, altitude in coordinates: + dc, ac = basic_chain(times, latitude, longitude, + module, inverter, + altitude=altitude, + orientation_strategy='south_at_latitude_tilt') + annual_energy = ac.sum() + energies[name] = annual_energy + + energies = pd.Series(energies) + + # based on the parameters specified above, these are in W*hrs + print(energies.round(0)) + + energies.plot(kind='bar', rot=0) + @savefig basic-chain-energies.png width=6in + plt.ylabel('Yearly energy yield (W hr)') + + +Object oriented (Location, PVSystem, ModelChain) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The first object oriented paradigm uses a model where a +:py:class:`~pvlib.pvsystem.PVSystem` object represents an assembled +collection of modules, inverters, etc., a +:py:class:`~pvlib.location.Location` object represents a particular +place on the planet, and a :py:class:`~pvlib.modelchain.ModelChain` +object describes the modeling chain used to calculate PV output at that +Location. This can be a useful paradigm if you prefer to think about the +PV system and its location as separate concepts or if you develop your +own ModelChain subclasses. It can also be helpful if you make extensive +use of Location-specific methods for other calculations. + +The following code demonstrates how to use +:py:class:`~pvlib.location.Location`, +:py:class:`~pvlib.pvsystem.PVSystem`, and +:py:class:`~pvlib.modelchain.ModelChain` +objects to accomplish our system modeling goal: + +.. ipython:: python + + from pvlib.pvsystem import PVSystem + from pvlib.location import Location + from pvlib.modelchain import ModelChain + + system = PVSystem(module_parameters=module, + inverter_parameters=inverter) + + energies = {} + for latitude, longitude, name, altitude in coordinates: + location = Location(latitude, longitude, name=name, altitude=altitude) + # very experimental + mc = ModelChain(system, location, + orientation_strategy='south_at_latitude_tilt') + # model results (ac, dc) and intermediates (aoi, temps, etc.) + # assigned as mc object attributes + mc.run_model(times) + annual_energy = mc.ac.sum() + energies[name] = annual_energy + + energies = pd.Series(energies) + + # based on the parameters specified above, these are in W*hrs + print(energies.round(0)) + + energies.plot(kind='bar', rot=0) + @savefig modelchain-energies.png width=6in + plt.ylabel('Yearly energy yield (W hr)') + + +Object oriented (LocalizedPVSystem) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The second object oriented paradigm uses a model where a +:py:class:`~pvlib.pvsystem.LocalizedPVSystem` represents a +PV system at a particular place on the planet. This can be a useful +paradigm if you're thinking about a power plant that already exists. + +The following code demonstrates how to use a +:py:class:`~pvlib.pvsystem.LocalizedPVSystem` +object to accomplish our modeling goal: + +.. ipython:: python + + from pvlib.pvsystem import LocalizedPVSystem + + energies = {} + for latitude, longitude, name, altitude in coordinates: + localized_system = LocalizedPVSystem(module_parameters=module, + inverter_parameters=inverter, + surface_tilt=latitude, + surface_azimuth=180, + latitude=latitude, + longitude=longitude, + name=name, + altitude=altitude) + clearsky = localized_system.get_clearsky(times) + solar_position = localized_system.get_solarposition(times) + total_irrad = localized_system.get_irradiance(solar_position['apparent_zenith'], + solar_position['azimuth'], + clearsky['dni'], + clearsky['ghi'], + clearsky['dhi']) + temps = localized_system.sapm_celltemp(total_irrad['poa_global'], + wind_speed, temp_air) + aoi = localized_system.get_aoi(solar_position['apparent_zenith'], + solar_position['azimuth']) + airmass = localized_system.get_airmass(solar_position=solar_position) + dc = localized_system.sapm(total_irrad['poa_direct'], + total_irrad['poa_diffuse'], + temps['temp_cell'], + airmass['airmass_absolute'], + aoi) + ac = localized_system.snlinverter(dc['v_mp'], dc['p_mp']) + annual_energy = ac.sum() + energies[name] = annual_energy + + energies = pd.Series(energies) + + # based on the parameters specified above, these are in W*hrs + print(energies.round(0)) + + energies.plot(kind='bar', rot=0) + @savefig localized-pvsystem-energies.png width=6in + plt.ylabel('Yearly energy yield (W hr)') + + +User extensions +--------------- +There are many other ways to organize PV modeling code. We encourage you +to build on these paradigms and to share your experiences with the pvlib +community via issues and pull requests. + + +Getting support +--------------- +The best way to get support is to make an issue on our +`GitHub issues page `_ . + + +How do I contribute? +-------------------- +We're so glad you asked! Please see our +`wiki `_ +for information and instructions on how to contribute. +We really appreciate it! + + +Credits +------- +The pvlib-python community thanks Sandia National Lab +for developing PVLIB Matlab and for supporting +Rob Andrews of Calama Consulting to port the library to Python. +Will Holmgren thanks the DOE EERE Postdoctoral Fellowship program +for support. +The pvlib-python maintainers thank all of pvlib's contributors of issues +and especially pull requests. +The pvlib-python community thanks all of the +maintainers and contributors to the PyData stack. + diff --git a/docs/sphinx/source/whatsnew.rst b/docs/sphinx/source/whatsnew.rst index dadc0ecaef..7504ceb2be 100644 --- a/docs/sphinx/source/whatsnew.rst +++ b/docs/sphinx/source/whatsnew.rst @@ -6,6 +6,7 @@ What's New These are new features and improvements of note in each release. +.. include:: whatsnew/v0.3.0.txt .. include:: whatsnew/v0.2.2.txt .. include:: whatsnew/v0.2.1.txt .. include:: whatsnew/v0.2.0.txt diff --git a/docs/sphinx/source/whatsnew/v0.3.0.txt b/docs/sphinx/source/whatsnew/v0.3.0.txt new file mode 100644 index 0000000000..01a897abc4 --- /dev/null +++ b/docs/sphinx/source/whatsnew/v0.3.0.txt @@ -0,0 +1,78 @@ +.. _whatsnew_0300: + +v0.3.0 (2016) +----------------------- + +This is a major release from 0.2.2. +It will almost certainly break your code, but it's worth it! +We recommend that all users upgrade to this version after testing +their code for compatibility and updating as necessary. + + +API changes +~~~~~~~~~~~ + +* The ``location`` argument in ``solarposition.get_solarposition`` + and ``clearsky.ineichen`` + has been replaced with ``latitude``, ``longitude``, + ``altitude``, and ``tz`` as appropriate. + This separates the object-oriented API from the procedural API. + (:issue:`17`) +* ``Location`` classes gain the ``get_solarposition``, ``get_clearsky``, + and ``get_airmass`` functions. +* Adds ``ModelChain``, ``PVSystem``, ``LocalizedPVSystem``, + ``SingleAxisTracker``, and ``LocalizedSingleAxisTracker`` + classes. (:issue:`17`) +* ``Location`` objects can be created from TMY2/TMY3 metadata + using the ``from_tmy`` constructor. +* Change default ``Location`` timezone to ``'UTC'``. +* The solar position calculators now assume UTC time if the input time + is not localized. The calculators previously tried to infer the timezone + from the now defunct location argument. +* ``pvsystem.sapm_celltemp`` argument names now follow the + variable conventions. +* ``irradiance.total_irrad`` now follows the variable conventions. + (:issue:`105`) + + +Enhancements +~~~~~~~~~~~~ + +* Added new sections to the documentation: + + * :ref:`package_overview` + * :ref:`variables_style_rules` + * :ref:`classes` + +* Adds support for Appveyor, a Windows continuous integration service. + (:issue:`111`) +* The readthedocs documentation build now uses conda packages + instead of mock packages. This enables code to be run + and figures to be generated during the documentation builds. + (:issue:`104`) +* Reconfigures TravisCI builds and adds e.g. ``has_numba`` decorators + to the test suite. The result is that the TravisCI test suite runs + almost 10x faster and users do not have to install all optional + dependencies to run the test suite. (:issue:`109`) +* Adds more unit tests that test that the return values are + actually correct. +* Add ``atmosphere.APPARENT_ZENITH_MODELS`` and + ``atmosphere.TRUE_ZENITH_MODELS`` to enable code that can + automatically determine which type of zenith data to use + e.g. ``Location.get_airmass``. + + +Bug fixes +~~~~~~~~~ + +* Fixed the metadata key specification in documentation of the + ``readtmy2`` function. +* Fixes the import of tkinter on Python 3 (:issue:`112`) + + +Contributors +~~~~~~~~~~~~ + +* Will Holmgren +* pyElena21 +* DaCoEx diff --git a/pvlib/__init__.py b/pvlib/__init__.py index 20ed1d4d26..e462f5be43 100644 --- a/pvlib/__init__.py +++ b/pvlib/__init__.py @@ -11,3 +11,4 @@ from pvlib import tracking from pvlib import pvsystem from pvlib import spa +from pvlib import modelchain \ No newline at end of file diff --git a/pvlib/atmosphere.py b/pvlib/atmosphere.py index 0ba96bf855..d61315ba23 100644 --- a/pvlib/atmosphere.py +++ b/pvlib/atmosphere.py @@ -11,10 +11,11 @@ import numpy as np -AIRMASS_MODELS = ['kastenyoung1989', 'kasten1966', 'simple', - 'pickering2002', 'youngirvine1967', 'young1994', - 'gueymard1993'] - +APPARENT_ZENITH_MODELS = ('simple', 'kasten1966', 'kastenyoung1989', + 'gueymard1993', 'pickering2002') +TRUE_ZENITH_MODELS = ('youngirvine1967', 'young1994') +AIRMASS_MODELS = APPARENT_ZENITH_MODELS + TRUE_ZENITH_MODELS + def pres2alt(pressure): ''' @@ -143,12 +144,12 @@ def absoluteairmass(airmass_relative, pressure=101325.): def relativeairmass(zenith, model='kastenyoung1989'): ''' - Gives the relative (not pressure-corrected) airmass + Gives the relative (not pressure-corrected) airmass. - Gives the airmass at sea-level when given a sun zenith angle, z (in - degrees). - The "model" variable allows selection of different airmass models - (described below). "model" must be a valid string. If "model" is not + Gives the airmass at sea-level when given a sun zenith angle + (in degrees). + The ``model`` variable allows selection of different airmass models + (described below). If ``model`` is not included or is not valid, the default model is 'kastenyoung1989'. Parameters diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index 7980194d45..fb64a5d473 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -19,9 +19,8 @@ from pvlib import solarposition - -def ineichen(time, location, linke_turbidity=None, - solarposition_method='pyephem', zenith_data=None, +def ineichen(time, latitude, longitude, altitude=0, linke_turbidity=None, + solarposition_method='nrel_numpy', zenith_data=None, airmass_model='young1994', airmass_data=None, interp_turbidity=True): ''' @@ -40,7 +39,11 @@ def ineichen(time, location, linke_turbidity=None, ----------- time : pandas.DatetimeIndex - location : pvlib.Location + latitude : float + + longitude : float + + altitude : float linke_turbidity : None or float If None, uses ``LinkeTurbidities.mat`` lookup table. @@ -101,7 +104,10 @@ def ineichen(time, location, linke_turbidity=None, I0 = irradiance.extraradiation(time.dayofyear) if zenith_data is None: - ephem_data = solarposition.get_solarposition(time, location, + ephem_data = solarposition.get_solarposition(time, + latitude=latitude, + longitude=longitude, + altitude=altitude, method=solarposition_method) time = ephem_data.index # fixes issue with time possibly not being tz-aware try: @@ -115,8 +121,7 @@ def ineichen(time, location, linke_turbidity=None, if linke_turbidity is None: - TL = lookup_linke_turbidity(time, location.latitude, - location.longitude, + TL = lookup_linke_turbidity(time, latitude, longitude, interp_turbidity=interp_turbidity) else: TL = linke_turbidity @@ -126,14 +131,14 @@ def ineichen(time, location, linke_turbidity=None, if airmass_data is None: AMabsolute = atmosphere.absoluteairmass(airmass_relative=atmosphere.relativeairmass(ApparentZenith, airmass_model), - pressure=atmosphere.alt2pres(location.altitude)) + pressure=atmosphere.alt2pres(altitude)) else: AMabsolute = airmass_data - fh1 = np.exp(-location.altitude/8000.) - fh2 = np.exp(-location.altitude/1250.) - cg1 = 5.09e-05 * location.altitude + 0.868 - cg2 = 3.92e-05 * location.altitude + 0.0387 + fh1 = np.exp(-altitude/8000.) + fh2 = np.exp(-altitude/1250.) + cg1 = 5.09e-05 * altitude + 0.868 + cg2 = 3.92e-05 * altitude + 0.0387 logger.debug('fh1=%s, fh2=%s, cg1=%s, cg2=%s', fh1, fh2, cg1, cg2) # Dan's note on the TL correction: By my reading of the publication on diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 0d0e7d5ad4..057a39f385 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -315,11 +315,11 @@ def beam_component(surface_tilt, surface_azimuth, # ToDo: how to best structure this function? wholmgren 2014-11-03 def total_irrad(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, + apparent_zenith, azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, albedo=.25, surface_type=None, model='isotropic', - model_perez='allsitescomposite1990'): + model_perez='allsitescomposite1990', **kwargs): ''' Determine diffuse irradiance from the sky on a tilted surface. @@ -359,7 +359,8 @@ def total_irrad(surface_tilt, surface_azimuth, Returns ------- - DataFrame with columns ``'total', 'beam', 'sky', 'ground'``. + DataFrame with columns ``'poa_global', 'poa_direct', + 'poa_sky_diffuse', 'poa_ground_diffuse'``. References ---------- @@ -370,6 +371,9 @@ def total_irrad(surface_tilt, surface_azimuth, pvl_logger.debug('planeofarray.total_irrad()') + solar_zenith = apparent_zenith + solar_azimuth = azimuth + beam = beam_component(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, dni) @@ -396,12 +400,15 @@ def total_irrad(surface_tilt, surface_azimuth, ground = grounddiffuse(surface_tilt, ghi, albedo, surface_type) - total = beam + sky + ground + diffuse = sky + ground + total = beam + diffuse - all_irrad = pd.DataFrame({'total': total, - 'beam': beam, - 'sky': sky, - 'ground': ground}) + all_irrad = pd.DataFrame() + all_irrad['poa_global'] = total + all_irrad['poa_direct'] = beam + all_irrad['poa_diffuse'] = diffuse + all_irrad['poa_sky_diffuse'] = sky + all_irrad['poa_ground_diffuse'] = ground return all_irrad diff --git a/pvlib/location.py b/pvlib/location.py index 9157766d20..1b0b9db23e 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -2,15 +2,18 @@ This module contains the Location class. """ -# Will Holmgren, University of Arizona, 2014. - -import logging -pvl_logger = logging.getLogger('pvlib') +# Will Holmgren, University of Arizona, 2014-2016. import datetime +import pandas as pd import pytz +from pvlib import solarposition +from pvlib import clearsky +from pvlib import atmosphere + + class Location(object): """ Location objects are convenient containers for latitude, longitude, @@ -19,8 +22,8 @@ class Location(object): Location objects have two timezone attributes: - * ``location.tz`` is a IANA timezone string. - * ``location.pytz`` is a pytz timezone object. + * ``tz`` is a IANA timezone string. + * ``pytz`` is a pytz timezone object. Location objects support the print method. @@ -29,24 +32,35 @@ class Location(object): latitude : float. Positive is north of the equator. Use decimal degrees notation. + longitude : float. Positive is east of the prime meridian. Use decimal degrees notation. - tz : string or pytz.timezone. + + tz : str, int, float, or pytz.timezone. See http://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of valid time zones. pytz.timezone objects will be converted to strings. + ints and floats must be in hours from UTC. + alitude : float. Altitude from sea level in meters. + name : None or string. Sets the name attribute of the Location object. + + **kwargs + Arbitrary keyword arguments. + Included for compatibility, but not used. + + See also + -------- + pvsystem.PVSystem """ - def __init__(self, latitude, longitude, tz='US/Mountain', altitude=100, - name=None): - - pvl_logger.debug('creating Location object') + def __init__(self, latitude, longitude, tz='UTC', altitude=0, + name=None, **kwargs): self.latitude = latitude self.longitude = longitude @@ -57,6 +71,9 @@ def __init__(self, latitude, longitude, tz='US/Mountain', altitude=100, elif isinstance(tz, datetime.tzinfo): self.tz = tz.zone self.pytz = tz + elif isinstance(tz, (int, float)): + self.tz = tz + self.pytz = pytz.FixedOffset(tz*60) else: raise TypeError('Invalid tz specification') @@ -64,9 +81,173 @@ def __init__(self, latitude, longitude, tz='US/Mountain', altitude=100, self.name = name + # needed for tying together Location and PVSystem in LocalizedPVSystem + # if LocalizedPVSystem signature is reversed + # super(Location, self).__init__(**kwargs) + def __str__(self): return ('{}: latitude={}, longitude={}, tz={}, altitude={}' .format(self.name, self.latitude, self.longitude, - self.tz, self.altitude)) \ No newline at end of file + self.tz, self.altitude)) + + + @classmethod + def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): + """ + Create an object based on a metadata + dictionary from tmy2 or tmy3 data readers. + + Parameters + ---------- + tmy_metadata : dict + Returned from tmy.readtmy2 or tmy.readtmy3 + tmy_data : None or DataFrame + Optionally attach the TMY data to this object. + + Returns + ------- + Location object (or the child class of Location that you + called this method from). + """ + # not complete, but hopefully you get the idea. + # might need code to handle the difference between tmy2 and tmy3 + + # determine if we're dealing with TMY2 or TMY3 data + tmy2 = tmy_metadata.get('City', False) + + latitude = tmy_metadata['latitude'] + longitude = tmy_metadata['longitude'] + + if tmy2: + name = tmy_metadata['City'] + else: + name = tmy_metadata['Name'] + + tz = tmy_metadata['TZ'] + altitude = tmy_metadata['altitude'] + + new_object = cls(latitude, longitude, tz=tz, altitude=altitude, + name=name, **kwargs) + + # not sure if this should be assigned regardless of input. + if tmy_data is not None: + new_object.tmy_data = tmy_data + + return new_object + + + def get_solarposition(self, times, pressure=None, temperature=12, + **kwargs): + """ + Uses the :py:func:`solarposition.get_solarposition` function + to calculate the solar zenith, azimuth, etc. at this location. + + Parameters + ---------- + times : DatetimeIndex + pressure : None, float, or array-like + If None, pressure will be calculated using + :py:func:`atmosphere.alt2pres` and ``self.altitude``. + temperature : None, float, or array-like + + kwargs passed to :py:func:`solarposition.get_solarposition` + + Returns + ------- + solar_position : DataFrame + Columns depend on the ``method`` kwarg, but always include + ``zenith`` and ``azimuth``. + """ + if pressure is None: + pressure = atmosphere.alt2pres(self.altitude) + + return solarposition.get_solarposition(times, latitude=self.latitude, + longitude=self.longitude, + altitude=self.altitude, + pressure=pressure, + temperature=temperature, + **kwargs) + + + def get_clearsky(self, times, model='ineichen', **kwargs): + """ + Calculate the clear sky estimates of GHI, DNI, and/or DHI + at this location. + + Parameters + ---------- + times : DatetimeIndex + + model : str + The clear sky model to use. + + kwargs passed to the relevant function(s). + + Returns + ------- + clearsky : DataFrame + Column names are: ``ghi, dni, dhi``. + """ + + if model == 'ineichen': + cs = clearsky.ineichen(times, latitude=self.latitude, + longitude=self.longitude, + altitude=self.altitude, + **kwargs) + elif model == 'haurwitz': + solpos = self.get_solarposition(times, **kwargs) + cs = clearsky.haurwitz(solpos['apparent_zenith']) + else: + raise ValueError('{} is not a valid clear sky model' + .format(model)) + + return cs + + + def get_airmass(self, times=None, solar_position=None, + model='kastenyoung1989'): + """ + Calculate the relative and absolute airmass. + + Automatically chooses zenith or apparant zenith + depending on the selected model. + + Parameters + ---------- + times : None or DatetimeIndex + Only used if solar_position is not provided. + solar_position : None or DataFrame + DataFrame with with columns 'apparent_zenith', 'zenith'. + model : str + Relative airmass model + + Returns + ------- + airmass : DataFrame + Columns are 'airmass_relative', 'airmass_absolute' + """ + + if solar_position is None: + solar_position = self.get_solarposition(times) + + if model in atmosphere.APPARENT_ZENITH_MODELS: + zenith = solar_position['apparent_zenith'] + elif model in atmosphere.TRUE_ZENITH_MODELS: + zenith = solar_position['zenith'] + else: + raise ValueError('{} is not a valid airmass model'.format(model)) + + airmass_relative = atmosphere.relativeairmass(zenith, model) + + pressure = atmosphere.alt2pres(self.altitude) + airmass_absolute = atmosphere.absoluteairmass(airmass_relative, + pressure) + + airmass = pd.DataFrame() + airmass['airmass_relative'] = airmass_relative + airmass['airmass_absolute'] = airmass_absolute + + return airmass + \ No newline at end of file diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py new file mode 100644 index 0000000000..09f5336718 --- /dev/null +++ b/pvlib/modelchain.py @@ -0,0 +1,347 @@ +""" +The ``modelchain`` module contains functions and classes that combine +many of the PV power modeling steps. These tools make it easy to +get started with pvlib and demonstrate standard ways to use the +library. With great power comes great responsibility: users should take +the time to read the source code for the module. +""" + +import pandas as pd + +from pvlib import solarposition, pvsystem, clearsky, atmosphere +import pvlib.irradiance # avoid name conflict with full import + + +def basic_chain(times, latitude, longitude, + module_parameters, inverter_parameters, + irradiance=None, weather=None, + surface_tilt=None, surface_azimuth=None, + orientation_strategy=None, + transposition_model='haydavies', + solar_position_method='nrel_numpy', + airmass_model='kastenyoung1989', + altitude=None, pressure=None, + **kwargs): + """ + An experimental function that computes all of the modeling steps + necessary for calculating power or energy for a PV system at a given + location. + + Parameters + ---------- + times : DatetimeIndex + Times at which to evaluate the model. + + latitude : float. + Positive is north of the equator. + Use decimal degrees notation. + + longitude : float. + Positive is east of the prime meridian. + Use decimal degrees notation. + + module_parameters : None, dict or Series + Module parameters as defined by the SAPM, CEC, or other. + + inverter_parameters : None, dict or Series + Inverter parameters as defined by the SAPM, CEC, or other. + + irradiance : None or DataFrame + If None, calculates clear sky data. + Columns must be 'dni', 'ghi', 'dhi'. + + weather : None or DataFrame + If None, assumes air temperature is 20 C and + wind speed is 0 m/s. + Columns must be 'wind_speed', 'temp_air'. + + surface_tilt : float or Series + Surface tilt angles in decimal degrees. + The tilt angle is defined as degrees from horizontal + (e.g. surface facing up = 0, surface facing horizon = 90) + + surface_azimuth : float or Series + Surface azimuth angles in decimal degrees. + The azimuth convention is defined + as degrees east of north + (North=0, South=180, East=90, West=270). + + orientation_strategy : None or str + The strategy for aligning the modules. + If not None, sets the ``surface_azimuth`` and ``surface_tilt`` + properties of the ``system``. + + transposition_model : str + Passed to system.get_irradiance. + + solar_position_method : str + Passed to location.get_solarposition. + + airmass_model : str + Passed to location.get_airmass. + + altitude : None or float + If None, computed from pressure. Assumed to be 0 m + if pressure is also None. + + pressure : None or float + If None, computed from altitude. Assumed to be 101325 Pa + if altitude is also None. + + **kwargs + Arbitrary keyword arguments. + See code for details. + + Returns + ------- + output : (dc, ac) + Tuple of DC power (with SAPM parameters) (DataFrame) and AC + power (Series). + """ + + # use surface_tilt and surface_azimuth if provided, + # otherwise set them using the orientation_strategy + if surface_tilt is not None and surface_azimuth is not None: + pass + elif orientation_strategy is not None: + surface_tilt, surface_azimuth = \ + get_orientation(orientation_strategy, latitude=latitude) + else: + raise ValueError('orientation_strategy or surface_tilt and ' + + 'surface_azimuth must be provided') + + times = times + + if altitude is None and pressure is None: + altitude = 0. + pressure = 101325. + elif altitude is None: + altitude = atmosphere.pres2alt(pressure) + elif pressure is None: + pressure = atmosphere.alt2pres(altitude) + + solar_position = solarposition.get_solarposition(times, latitude, + longitude, + altitude=altitude, + pressure=pressure, + **kwargs) + + # possible error with using apparent zenith with some models + airmass = atmosphere.relativeairmass(solar_position['apparent_zenith'], + model=airmass_model) + airmass = atmosphere.absoluteairmass(airmass, pressure) + dni_extra = pvlib.irradiance.extraradiation(solar_position.index) + dni_extra = pd.Series(dni_extra, index=solar_position.index) + + aoi = pvlib.irradiance.aoi(surface_tilt, surface_azimuth, + solar_position['apparent_zenith'], + solar_position['azimuth']) + + if irradiance is None: + irradiance = clearsky.ineichen( + solar_position.index, + latitude, + longitude, + zenith_data=solar_position['apparent_zenith'], + airmass_data=airmass, + altitude=altitude) + + total_irrad = pvlib.irradiance.total_irrad( + surface_tilt, + surface_azimuth, + solar_position['apparent_zenith'], + solar_position['azimuth'], + irradiance['dni'], + irradiance['ghi'], + irradiance['dhi'], + model=transposition_model, + dni_extra=dni_extra) + + if weather is None: + weather = {'wind_speed': 0, 'temp_air': 20} + + temps = pvsystem.sapm_celltemp(total_irrad['poa_global'], + weather['wind_speed'], + weather['temp_air']) + + dc = pvsystem.sapm(module_parameters, total_irrad['poa_direct'], + total_irrad['poa_diffuse'], + temps['temp_cell'], + airmass, + aoi) + + ac = pvsystem.snlinverter(inverter_parameters, dc['v_mp'], dc['p_mp']) + + return dc, ac + + +def get_orientation(strategy, **kwargs): + """ + Determine a PV system's surface tilt and surface azimuth + using a named strategy. + + Parameters + ---------- + strategy: str + The orientation strategy. + Allowed strategies include 'flat', 'south_at_latitude_tilt'. + **kwargs: + Strategy-dependent keyword arguments. See code for details. + + Returns + ------- + surface_tilt, surface_azimuth + """ + + if strategy == 'south_at_latitude_tilt': + surface_azimuth = 180 + surface_tilt = kwargs['latitude'] + elif strategy == 'flat': + surface_azimuth = 180 + surface_tilt = 0 + else: + raise ValueError('invalid orientation strategy. strategy must ' + + 'be one of south_at_latitude, flat,') + + return surface_tilt, surface_azimuth + + +class ModelChain(object): + """ + An experimental class that represents all of the modeling steps + necessary for calculating power or energy for a PV system at a given + location. + + Parameters + ---------- + system : PVSystem + A :py:class:`~pvlib.pvsystem.PVSystem` object that represents + the connected set of modules, inverters, etc. + + location : Location + A :py:class:`~pvlib.location.Location` object that represents + the physical location at which to evaluate the model. + + orientation_strategy : None or str + The strategy for aligning the modules. If not None, sets the + ``surface_azimuth`` and ``surface_tilt`` properties of the + ``system``. Allowed strategies include 'flat', + 'south_at_latitude_tilt'. + + clearsky_model : str + Passed to location.get_clearsky. + + transposition_model : str + Passed to system.get_irradiance. + + solar_position_method : str + Passed to location.get_solarposition. + + airmass_model : str + Passed to location.get_airmass. + + **kwargs + Arbitrary keyword arguments. + Included for compatibility, but not used. + """ + + def __init__(self, system, location, + orientation_strategy='south_at_latitude_tilt', + clearsky_model='ineichen', + transposition_model='haydavies', + solar_position_method='nrel_numpy', + airmass_model='kastenyoung1989', + **kwargs): + + self.system = system + self.location = location + self.clearsky_model = clearsky_model + self.transposition_model = transposition_model + self.solar_position_method = solar_position_method + self.airmass_model = airmass_model + + # calls setter + self.orientation_strategy = orientation_strategy + + @property + def orientation_strategy(self): + return self._orientation_strategy + + @orientation_strategy.setter + def orientation_strategy(self, strategy): + if strategy == 'None': + strategy = None + + if strategy is not None: + self.system.surface_tilt, self.system.surface_azimuth = \ + get_orientation(strategy, latitude=self.location.latitude) + + self._orientation_strategy = strategy + + def run_model(self, times, irradiance=None, weather=None): + """ + Run the model. + + Parameters + ---------- + times : DatetimeIndex + Times at which to evaluate the model. + + irradiance : None or DataFrame + If None, calculates clear sky data. + Columns must be 'dni', 'ghi', 'dhi'. + + weather : None or DataFrame + If None, assumes air temperature is 20 C and + wind speed is 0 m/s. + Columns must be 'wind_speed', 'temp_air'. + + Returns + ------- + self + + Assigns attributes: times, solar_position, airmass, irradiance, + total_irrad, weather, temps, aoi, dc, ac + """ + self.times = times + + self.solar_position = self.location.get_solarposition(self.times) + + self.airmass = self.location.get_airmass( + solar_position=self.solar_position, model=self.airmass_model) + + if irradiance is None: + irradiance = self.location.get_clearsky( + self.solar_position.index, self.clearsky_model, + zenith_data=self.solar_position['apparent_zenith'], + airmass_data=self.airmass['airmass_absolute']) + self.irradiance = irradiance + + self.total_irrad = self.system.get_irradiance( + self.solar_position['apparent_zenith'], + self.solar_position['azimuth'], + self.irradiance['dni'], + self.irradiance['ghi'], + self.irradiance['dhi'], + model=self.transposition_model) + + if weather is None: + weather = {'wind_speed': 0, 'temp_air': 20} + self.weather = weather + + self.temps = self.system.sapm_celltemp(self.total_irrad['poa_global'], + self.weather['wind_speed'], + self.weather['temp_air']) + + self.aoi = self.system.get_aoi(self.solar_position['apparent_zenith'], + self.solar_position['azimuth']) + + self.dc = self.system.sapm(self.total_irrad['poa_direct'], + self.total_irrad['poa_diffuse'], + self.temps['temp_cell'], + self.airmass['airmass_absolute'], + self.aoi) + + self.ac = self.system.snlinverter(self.dc['v_mp'], self.dc['p_mp']) + + return self diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 0719538223..a3a16f0ae7 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -5,19 +5,417 @@ from __future__ import division -import logging import io +try: + from urllib2 import urlopen +except ImportError: + from urllib.request import urlopen + import numpy as np import pandas as pd from pvlib import tools +from pvlib.location import Location +from pvlib import irradiance, atmosphere -try: - from urllib2 import urlopen -except ImportError: - from urllib.request import urlopen -pvl_logger = logging.getLogger('pvlib') +# not sure if this belongs in the pvsystem module. +# maybe something more like core.py? It may eventually grow to +# import a lot more functionality from other modules. +class PVSystem(object): + """ + The PVSystem class defines a standard set of PV system attributes + and modeling functions. This class describes the collection and + interactions of PV system components rather than an installed system + on the ground. It is typically used in combination with + :py:class:`~pvlib.location.Location` and + :py:class:`~pvlib.modelchain.ModelChain` + objects. + + See the :py:class:`LocalizedPVSystem` class for an object model that + describes an installed PV system. + + The class is complementary to the module-level functions. + + The attributes should generally be things that don't change about + the system, such the type of module and the inverter. The instance + methods accept arguments for things that do change, such as + irradiance and temperature. + + Parameters + ---------- + surface_tilt: float or array-like + Tilt angle of the module surface. + Up=0, horizon=90. + + surface_azimuth: float or array-like + Azimuth angle of the module surface. + North=0, East=90, South=180, West=270. + + albedo : None, float + The ground albedo. If ``None``, will attempt to use + ``surface_type`` and ``irradiance.SURFACE_ALBEDOS`` + to lookup albedo. + + surface_type : None, string + The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` + for valid values. + + module : None, string + The model name of the modules. + May be used to look up the module_parameters dictionary + via some other method. + + module_parameters : None, dict or Series + Module parameters as defined by the SAPM, CEC, or other. + + inverter : None, string + The model name of the inverters. + May be used to look up the inverter_parameters dictionary + via some other method. + + inverter_parameters : None, dict or Series + Inverter parameters as defined by the SAPM, CEC, or other. + + racking_model : None or string + Used for cell and module temperature calculations. + + **kwargs + Arbitrary keyword arguments. + Included for compatibility, but not used. + + See also + -------- + pvlib.location.Location + pvlib.tracking.SingleAxisTracker + pvlib.pvsystem.LocalizedPVSystem + """ + + def __init__(self, + surface_tilt=0, surface_azimuth=180, + albedo=None, surface_type=None, + module=None, module_parameters=None, + series_modules=None, parallel_modules=None, + inverter=None, inverter_parameters=None, + racking_model='open_rack_cell_glassback', + **kwargs): + + self.surface_tilt = surface_tilt + self.surface_azimuth = surface_azimuth + + # could tie these together with @property + self.surface_type = surface_type + if albedo is None: + self.albedo = irradiance.SURFACE_ALBEDOS.get(surface_type, 0.25) + else: + self.albedo = albedo + + # could tie these together with @property + self.module = module + self.module_parameters = module_parameters + + self.series_modules = series_modules + self.parallel_modules = parallel_modules + + self.inverter = inverter + self.inverter_parameters = inverter_parameters + + self.racking_model = racking_model + + # needed for tying together Location and PVSystem in LocalizedPVSystem + super(PVSystem, self).__init__(**kwargs) + + def get_aoi(self, solar_zenith, solar_azimuth): + """Get the angle of incidence on the system. + + Parameters + ---------- + solar_zenith : float or Series. + Solar zenith angle. + solar_azimuth : float or Series. + Solar azimuth angle. + + Returns + ------- + aoi : Series + The angle of incidence + """ + + aoi = irradiance.aoi(self.surface_tilt, self.surface_azimuth, + solar_zenith, solar_azimuth) + return aoi + + def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, + dni_extra=None, airmass=None, model='haydavies', + **kwargs): + """ + Uses the :py: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``. + + Parameters + ---------- + solar_zenith : float or Series. + Solar zenith angle. + solar_azimuth : float or Series. + Solar azimuth angle. + dni : float or Series + Direct Normal Irradiance + ghi : float or Series + Global horizontal irradiance + dhi : float or Series + Diffuse horizontal irradiance + dni_extra : float or Series + Extraterrestrial direct normal irradiance + airmass : float or Series + Airmass + model : String + Irradiance model. + + **kwargs + Passed to :func:`irradiance.total_irrad`. + + Returns + ------- + poa_irradiance : DataFrame + Column names are: ``total, beam, sky, ground``. + """ + + # 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) + + return irradiance.total_irrad(self.surface_tilt, + self.surface_azimuth, + solar_zenith, solar_azimuth, + dni, ghi, dhi, + dni_extra=dni_extra, airmass=airmass, + model=model, + albedo=self.albedo, + **kwargs) + + def ashraeiam(self, aoi): + """ + Determine the incidence angle modifier using + ``self.module_parameters['b']``, ``aoi``, + and the :py:func:`ashraeiam` function. + + Parameters + ---------- + aoi : numeric + The angle of incidence in degrees. + + Returns + ------- + modifier : numeric + The AOI modifier. + """ + b = self.module_parameters['b'] + return ashraeiam(b, aoi) + + def physicaliam(self, aoi): + """ + Determine the incidence angle modifier using + ``self.module_parameters['K']``, + ``self.module_parameters['L']``, + ``self.module_parameters['n']``, + ``aoi``, and the + :py:func:`physicaliam` function. + + Parameters + ---------- + See pvsystem.physicaliam for details + + Returns + ------- + See pvsystem.physicaliam for details + """ + K = self.module_parameters['K'] + L = self.module_parameters['L'] + n = self.module_parameters['n'] + return physicaliam(K, L, n, aoi) + + def calcparams_desoto(self, poa_global, temp_cell, **kwargs): + """ + Use the :py:func:`calcparams_desoto` function, the input + parameters and ``self.module_parameters`` to calculate the + module currents and resistances. + + Parameters + ---------- + poa_global : float or Series + The irradiance (in W/m^2) absorbed by the module. + + temp_cell : float or Series + The average cell temperature of cells within a module in C. + + **kwargs + See pvsystem.calcparams_desoto for details + + Returns + ------- + See pvsystem.calcparams_desoto for details + """ + return calcparams_desoto(poa_global, temp_cell, + self.module_parameters['alpha_sc'], + self.module_parameters, + self.module_parameters['EgRef'], + self.module_parameters['dEgdT'], **kwargs) + + def sapm(self, poa_direct, poa_diffuse, + temp_cell, airmass_absolute, aoi, **kwargs): + """ + Use the :py:func:`sapm` function, the input parameters, + and ``self.module_parameters`` to calculate + Voc, Isc, Ix, Ixx, Vmp/Imp. + + Parameters + ---------- + poa_direct : Series + The direct irradiance incident upon the module (W/m^2). + + poa_diffuse : Series + The diffuse irradiance incident on module. + + temp_cell : Series + The cell temperature (degrees C). + + airmass_absolute : Series + Absolute airmass. + + aoi : Series + Angle of incidence (degrees). + + **kwargs + See pvsystem.sapm for details + + Returns + ------- + See pvsystem.sapm for details + """ + return sapm(self.module_parameters, poa_direct, poa_diffuse, + temp_cell, airmass_absolute, aoi) + + # model now specified by self.racking_model + def sapm_celltemp(self, irrad, wind, temp): + """Uses :py:func:`sapm_celltemp` to calculate module and cell + temperatures based on ``self.racking_model`` and + the input parameters. + + Parameters + ---------- + See pvsystem.sapm_celltemp for details + + Returns + ------- + See pvsystem.sapm_celltemp for details + """ + return sapm_celltemp(irrad, wind, temp, self.racking_model) + + def singlediode(self, photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth): + """Wrapper around the :py:func:`singlediode` function. + + Parameters + ---------- + See pvsystem.singlediode for details + + Returns + ------- + See pvsystem.singlediode for details + """ + return singlediode(self.module_parameters, photocurrent, + saturation_current, + resistance_series, resistance_shunt, nNsVth) + + def i_from_v(self, resistance_shunt, resistance_series, nNsVth, voltage, + saturation_current, photocurrent): + """Wrapper around the :py:func:`i_from_v` function. + + Parameters + ---------- + See pvsystem.i_from_v for details + + Returns + ------- + See pvsystem.i_from_v for details + """ + return i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, + saturation_current, photocurrent) + + # inverter now specified by self.inverter_parameters + def snlinverter(self, v_dc, p_dc): + """Uses :func:`snlinverter` to calculate AC power based on + ``self.inverter_parameters`` and the input parameters. + + Parameters + ---------- + See pvsystem.snlinverter for details + + Returns + ------- + See pvsystem.snlinverter for details + """ + return snlinverter(self.inverter_parameters, v_dc, p_dc) + + def localize(self, location=None, latitude=None, longitude=None, + **kwargs): + """Creates a LocalizedPVSystem object using this object + and location data. Must supply either location object or + latitude, longitude, and any location kwargs + + Parameters + ---------- + location : None or Location + latitude : None or float + longitude : None or float + **kwargs : see Location + + Returns + ------- + localized_system : LocalizedPVSystem + """ + + if location is None: + location = Location(latitude, longitude, **kwargs) + + return LocalizedPVSystem(pvsystem=self, location=location) + + +class LocalizedPVSystem(PVSystem, Location): + """ + The LocalizedPVSystem class defines a standard set of installed PV + system attributes and modeling functions. This class combines the + attributes and methods of the PVSystem and Location classes. + + See the :py:class:`PVSystem` class for an object model that + describes an unlocalized PV system. + """ + def __init__(self, pvsystem=None, location=None, **kwargs): + + # get and combine attributes from the pvsystem and/or location + # with the rest of the kwargs + + if pvsystem is not None: + pv_dict = pvsystem.__dict__ + else: + pv_dict = {} + + if location is not None: + loc_dict = location.__dict__ + else: + loc_dict = {} + + new_kwargs = dict(list(pv_dict.items()) + + list(loc_dict.items()) + + list(kwargs.items())) + + super(LocalizedPVSystem, self).__init__(**new_kwargs) def systemdef(meta, surface_tilt, surface_azimuth, albedo, series_modules, @@ -29,8 +427,8 @@ def systemdef(meta, surface_tilt, surface_azimuth, albedo, series_modules, ---------- meta : dict - meta dict either generated from a TMY file using readtmy2 or readtmy3, - or a dict containing at least the following fields: + meta dict either generated from a TMY file using readtmy2 or + readtmy3, or a dict containing at least the following fields: =============== ====== ==================== meta field format description @@ -55,9 +453,9 @@ def systemdef(meta, surface_tilt, surface_azimuth, albedo, series_modules, (North=0, South=180, East=90, West=270). albedo : float or Series - Ground reflectance, typically 0.1-0.4 for - surfaces on Earth (land), may increase over snow, ice, etc. May also - be known as the reflection coefficient. Must be >=0 and <=1. + Ground reflectance, typically 0.1-0.4 for surfaces on Earth + (land), may increase over snow, ice, etc. May also be known as + the reflection coefficient. Must be >=0 and <=1. series_modules : int Number of modules connected in series in a string. @@ -109,12 +507,13 @@ def systemdef(meta, surface_tilt, surface_azimuth, albedo, series_modules, def ashraeiam(b, aoi): ''' - Determine the incidence angle modifier using the ASHRAE transmission model. + Determine the incidence angle modifier using the ASHRAE transmission + model. ashraeiam calculates the incidence angle modifier as developed in - [1], and adopted by ASHRAE (American Society of Heating, Refrigeration, - and Air Conditioning Engineers) [2]. The model has been used by model - programs such as PVSyst [3]. + [1], and adopted by ASHRAE (American Society of Heating, + Refrigeration, and Air Conditioning Engineers) [2]. The model has + been used by model programs such as PVSyst [3]. Note: For incident angles near 90 degrees, this model has a discontinuity which has been addressed in this function. @@ -141,9 +540,9 @@ def ashraeiam(b, aoi): References ---------- - [1] Souka A.F., Safwat H.H., "Determindation of the optimum orientations - for the double exposure flat-plate collector and its reflections". - Solar Energy vol .10, pp 170-174. 1966. + [1] Souka A.F., Safwat H.H., "Determindation of the optimum + orientations for the double exposure flat-plate collector and its + reflections". Solar Energy vol .10, pp 170-174. 1966. [2] ASHRAE standard 93-77 @@ -167,15 +566,15 @@ def ashraeiam(b, aoi): def physicaliam(K, L, n, aoi): ''' - Determine the incidence angle modifier using refractive - index, glazing thickness, and extinction coefficient + Determine the incidence angle modifier using refractive index, + glazing thickness, and extinction coefficient physicaliam calculates the incidence angle modifier as described in - De Soto et al. "Improvement and validation of a model for photovoltaic - array performance", section 3. The calculation is based upon a physical - model of absorbtion and transmission through a cover. Required - information includes, incident angle, cover extinction coefficient, - cover thickness + De Soto et al. "Improvement and validation of a model for + photovoltaic array performance", section 3. The calculation is based + upon a physical model of absorbtion and transmission through a + cover. Required information includes, incident angle, cover + extinction coefficient, cover thickness Note: The authors of this function believe that eqn. 14 in [1] is incorrect. This function uses the following equation in its place: @@ -184,22 +583,24 @@ def physicaliam(K, L, n, aoi): Parameters ---------- K : float - The glazing extinction coefficient in units of 1/meters. Reference - [1] indicates that a value of 4 is reasonable for "water white" - glass. K must be a numeric scalar or vector with all values >=0. If K - is a vector, it must be the same size as all other input vectors. + The glazing extinction coefficient in units of 1/meters. + Reference [1] indicates that a value of 4 is reasonable for + "water white" glass. K must be a numeric scalar or vector with + all values >=0. If K is a vector, it must be the same size as + all other input vectors. L : float - The glazing thickness in units of meters. Reference [1] indicates - that 0.002 meters (2 mm) is reasonable for most glass-covered - PV panels. L must be a numeric scalar or vector with all values >=0. - If L is a vector, it must be the same size as all other input vectors. + The glazing thickness in units of meters. Reference [1] + indicates that 0.002 meters (2 mm) is reasonable for most + glass-covered PV panels. L must be a numeric scalar or vector + with all values >=0. If L is a vector, it must be the same size + as all other input vectors. n : float The effective index of refraction (unitless). Reference [1] - indicates that a value of 1.526 is acceptable for glass. n must be a - numeric scalar or vector with all values >=0. If n is a vector, it - must be the same size as all other input vectors. + indicates that a value of 1.526 is acceptable for glass. n must + be a numeric scalar or vector with all values >=0. If n is a + vector, it must be the same size as all other input vectors. aoi : Series The angle of incidence between the module normal vector and the @@ -212,10 +613,9 @@ def physicaliam(K, L, n, aoi): IAM is a column vector with the same number of elements as the largest input vector. - Theta must be a numeric scalar or vector. - For any values of theta where abs(aoi)>90, IAM is set to 0. For any - values of aoi where -90 < aoi < 0, theta is set to abs(aoi) and - evaluated. + Theta must be a numeric scalar or vector. For any values of + theta where abs(aoi)>90, IAM is set to 0. For any values of aoi + where -90 < aoi < 0, theta is set to abs(aoi) and evaluated. References ---------- @@ -263,8 +663,8 @@ def physicaliam(K, L, n, aoi): def calcparams_desoto(poa_global, temp_cell, alpha_isc, module_parameters, EgRef, dEgdT, M=1, irrad_ref=1000, temp_ref=25): ''' - Applies the temperature and irradiance corrections to - inputs for singlediode. + Applies the temperature and irradiance corrections to inputs for + singlediode. Applies the temperature and irradiance corrections to the IL, I0, Rs, Rsh, and a parameters at reference conditions (IL_ref, I0_ref, @@ -312,17 +712,18 @@ def calcparams_desoto(poa_global, temp_cell, alpha_isc, module_parameters, 1.121 eV for silicon. EgRef must be >0. dEgdT : float - The temperature dependence of the energy bandgap at SRC (in 1/C). - May be either a scalar value (e.g. -0.0002677 as in [1]) or a - DataFrame of dEgdT values corresponding to each input condition (this - may be useful if dEgdT is a function of temperature). + The temperature dependence of the energy bandgap at SRC (in + 1/C). May be either a scalar value (e.g. -0.0002677 as in [1]) + or a DataFrame of dEgdT values corresponding to each input + condition (this may be useful if dEgdT is a function of + temperature). M : float or Series (optional, default=1) - An optional airmass modifier, if omitted, M is given a value of 1, - which assumes absolute (pressure corrected) airmass = 1.5. In this - code, M is equal to M/Mref as described in [1] (i.e. Mref is assumed - to be 1). Source [1] suggests that an appropriate value for M - as a function absolute airmass (AMa) may be: + An optional airmass modifier, if omitted, M is given a value of + 1, which assumes absolute (pressure corrected) airmass = 1.5. In + this code, M is equal to M/Mref as described in [1] (i.e. Mref + is assumed to be 1). Source [1] suggests that an appropriate + value for M as a function absolute airmass (AMa) may be: >>> M = np.polyval([-0.000126, 0.002816, -0.024459, 0.086257, 0.918093], ... AMa) # doctest: +SKIP @@ -348,17 +749,20 @@ def calcparams_desoto(poa_global, temp_cell, alpha_isc, module_parameters, S and cell temperature Tcell. resistance_series : float - Series resistance in ohms at irradiance S and cell temperature Tcell. + Series resistance in ohms at irradiance S and cell temperature + Tcell. resistance_shunt : float or Series - Shunt resistance in ohms at irradiance S and cell temperature Tcell. + Shunt resistance in ohms at irradiance S and cell temperature + Tcell. nNsVth : float or Series - Modified diode ideality factor at irradiance S and cell temperature - Tcell. Note that in source [1] nNsVth = a (equation 2). nNsVth is the - product of the usual diode ideality factor (n), the number of - series-connected cells in the module (Ns), and the thermal voltage - of a cell in the module (Vth) at a cell temperature of Tcell. + Modified diode ideality factor at irradiance S and cell + temperature Tcell. Note that in source [1] nNsVth = a (equation + 2). nNsVth is the product of the usual diode ideality factor + (n), the number of series-connected cells in the module (Ns), + and the thermal voltage of a cell in the module (Vth) at a cell + temperature of Tcell. References ---------- @@ -385,22 +789,22 @@ def calcparams_desoto(poa_global, temp_cell, alpha_isc, module_parameters, Notes ----- If the reference parameters in the ModuleParameters struct are read - from a database or library of parameters (e.g. System Advisor Model), - it is important to use the same EgRef and dEgdT values that + from a database or library of parameters (e.g. System Advisor + Model), it is important to use the same EgRef and dEgdT values that were used to generate the reference parameters, regardless of the actual bandgap characteristics of the semiconductor. For example, in - the case of the System Advisor Model library, created as described in - [3], EgRef and dEgdT for all modules were 1.121 and -0.0002677, + the case of the System Advisor Model library, created as described + in [3], EgRef and dEgdT for all modules were 1.121 and -0.0002677, respectively. This table of reference bandgap energies (EgRef), bandgap energy - temperature dependence (dEgdT), and "typical" airmass response (M) is - provided purely as reference to those who may generate their own - reference module parameters (a_ref, IL_ref, I0_ref, etc.) based upon the - various PV semiconductors. Again, we stress the importance of - using identical EgRef and dEgdT when generation reference - parameters and modifying the reference parameters (for irradiance, - temperature, and airmass) per DeSoto's equations. + temperature dependence (dEgdT), and "typical" airmass response (M) + is provided purely as reference to those who may generate their own + reference module parameters (a_ref, IL_ref, I0_ref, etc.) based upon + the various PV semiconductors. Again, we stress the importance of + using identical EgRef and dEgdT when generation reference parameters + and modifying the reference parameters (for irradiance, temperature, + and airmass) per DeSoto's equations. Silicon (Si): * EgRef = 1.121 @@ -497,11 +901,12 @@ def retrieve_sam(name=None, samfile=None): samfile : String Absolute path to the location of local versions of the SAM file. - If file is specified, the latest versions of the SAM database will - not be downloaded. The selected file must be in .csv format. + If file is specified, the latest versions of the SAM database + will not be downloaded. The selected file must be in .csv + format. - If set to 'select', a dialogue will open allowing the user to navigate - to the appropriate page. + If set to 'select', a dialogue will open allowing the user to + navigate to the appropriate page. Returns ------- @@ -540,7 +945,9 @@ def retrieve_sam(name=None, samfile=None): url = base_url + 'sam-library-cec-modules-2015-6-30.csv' elif name == 'sandiamod': url = base_url + 'sam-library-sandia-modules-2015-6-30.csv' - elif name in ['cecinverter', 'sandiainverter']: # Allowing either, to provide for old code, while aligning with current expectations + elif name in ['cecinverter', 'sandiainverter']: + # Allowing either, to provide for old code, + # while aligning with current expectations url = base_url + 'sam-library-cec-inverters-2015-6-30.csv' elif samfile is None: raise ValueError('invalid name {}'.format(name)) @@ -549,7 +956,6 @@ def retrieve_sam(name=None, samfile=None): raise ValueError('must supply name or samfile') if samfile is None: - pvl_logger.info('retrieving %s from %s', name, url) response = urlopen(url) csvdata = io.StringIO(response.read().decode(errors='ignore')) elif samfile == 'select': @@ -596,15 +1002,15 @@ def _parse_raw_sam_df(csvdata): def sapm(module, poa_direct, poa_diffuse, temp_cell, airmass_absolute, aoi): ''' - The Sandia PV Array Performance Model (SAPM) generates 5 points on a PV - module's I-V curve (Voc, Isc, Ix, Ixx, Vmp/Imp) according to + The Sandia PV Array Performance Model (SAPM) generates 5 points on a + PV module's I-V curve (Voc, Isc, Ix, Ixx, Vmp/Imp) according to SAND2004-3535. Assumes a reference cell temperature of 25 C. Parameters ---------- module : Series or dict - A DataFrame defining the SAPM performance parameters. See the notes - section for more details. + A DataFrame defining the SAPM performance parameters. See the + notes section for more details. poa_direct : Series The direct irradiance incident upon the module (W/m^2). @@ -638,12 +1044,12 @@ def sapm(module, poa_direct, poa_diffuse, temp_cell, airmass_absolute, aoi): Notes ----- - The coefficients from SAPM which are required in ``module`` are listed in - the following table. + The coefficients from SAPM which are required in ``module`` are + listed in the following table. - The modules in the Sandia module database contain these coefficients, but - the modules in the CEC module database do not. Both databases can be - accessed using :py:func:`retrieve_sam`. + The modules in the Sandia module database contain these + coefficients, but the modules in the CEC module database do not. + Both databases can be accessed using :py:func:`retrieve_sam`. ================ ======================================================== Key Description @@ -679,8 +1085,9 @@ def sapm(module, poa_direct, poa_diffuse, temp_cell, airmass_absolute, aoi): References ---------- - [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance Model", - SAND Report 3535, Sandia National Laboratories, Albuquerque, NM. + [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance + Model", SAND Report 3535, Sandia National Laboratories, Albuquerque, + NM. See Also -------- @@ -713,21 +1120,25 @@ def sapm(module, poa_direct, poa_diffuse, temp_cell, airmass_absolute, aoi): dfout = pd.DataFrame(index=Ee.index) dfout['i_sc'] = ( - module['Isco'] * Ee * (1 + module['Aisc']*(temp_cell - T0))) + module['Isco'] * Ee * (1 + module['Aisc']*(temp_cell - T0)) + ) - dfout['i_mp'] = (module['Impo'] * - (module['C0']*Ee + module['C1']*(Ee**2)) * - (1 + module['Aimp']*(temp_cell - T0))) + dfout['i_mp'] = ( + module['Impo'] * (module['C0']*Ee + module['C1']*(Ee**2)) * + (1 + module['Aimp']*(temp_cell - T0)) + ) - dfout['v_oc'] = ((module['Voco'] + - module['Cells_in_Series']*delta*np.log(Ee) + - Bvoco*(temp_cell - T0)).clip_lower(0)) + dfout['v_oc'] = ( + module['Voco'] + module['Cells_in_Series']*delta*np.log(Ee) + + Bvoco*(temp_cell - T0) + ).clip_lower(0) dfout['v_mp'] = ( module['Vmpo'] + module['C2']*module['Cells_in_Series']*delta*np.log(Ee) + module['C3']*module['Cells_in_Series']*((delta*np.log(Ee)) ** 2) + - Bvmpo*(temp_cell - T0)).clip_lower(0) + Bvmpo*(temp_cell - T0) + ).clip_lower(0) dfout['p_mp'] = dfout['i_mp'] * dfout['v_mp'] @@ -745,7 +1156,8 @@ def sapm(module, poa_direct, poa_diffuse, temp_cell, airmass_absolute, aoi): return dfout -def sapm_celltemp(irrad, wind, temp, model='open_rack_cell_glassback'): +def sapm_celltemp(poa_global, wind_speed, temp_air, + model='open_rack_cell_glassback'): ''' Estimate cell and module temperatures per the Sandia PV Array Performance Model (SAPM, SAND2004-3535), from the incident @@ -754,16 +1166,16 @@ def sapm_celltemp(irrad, wind, temp, model='open_rack_cell_glassback'): Parameters ---------- - irrad : float or Series + poa_global : float or Series Total incident irradiance in W/m^2. - wind : float or Series + wind_speed : float or Series Wind speed in m/s at a height of 10 meters. - temp : float or Series + temp_air : float or Series Ambient dry bulb temperature in degrees C. - model : string or list + model : string, list, or dict Model to be used. If string, can be: @@ -775,7 +1187,8 @@ def sapm_celltemp(irrad, wind, temp, model='open_rack_cell_glassback'): * 'open_rack_polymer_thinfilm_steel' * '22x_concentrator_tracker' - If list, supply the following parameters in the following order: + If dict, supply the following parameters + (if list, in the following order): * a : float SAPM module parameter for establishing the upper @@ -799,8 +1212,9 @@ def sapm_celltemp(irrad, wind, temp, model='open_rack_cell_glassback'): References ---------- - [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance Model", - SAND Report 3535, Sandia National Laboratories, Albuquerque, NM. + [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance + Model", SAND Report 3535, Sandia National Laboratories, Albuquerque, + NM. See Also -------- @@ -813,12 +1227,14 @@ def sapm_celltemp(irrad, wind, temp, model='open_rack_cell_glassback'): 'insulated_back_polymerback': [-2.81, -.0455, 0], 'open_rack_polymer_thinfilm_steel': [-3.58, -.113, 3], '22x_concentrator_tracker': [-3.23, -.130, 13] - } + } if isinstance(model, str): model = temp_models[model.lower()] elif isinstance(model, list): model = model + elif isinstance(model, (dict, pd.Series)): + model = [model['a'], model['b'], model['deltaT']] a = model[0] b = model[1] @@ -826,9 +1242,9 @@ def sapm_celltemp(irrad, wind, temp, model='open_rack_cell_glassback'): E0 = 1000. # Reference irradiance - temp_module = pd.Series(irrad*np.exp(a + b*wind) + temp) + temp_module = pd.Series(poa_global*np.exp(a + b*wind_speed) + temp_air) - temp_cell = temp_module + (irrad / E0)*(deltaT) + temp_cell = temp_module + (poa_global / E0)*(deltaT) return pd.DataFrame({'temp_cell': temp_cell, 'temp_module': temp_module}) @@ -844,13 +1260,12 @@ def singlediode(module, photocurrent, saturation_current, I = IL - I0*[exp((V+I*Rs)/(nNsVth))-1] - (V + I*Rs)/Rsh - for ``I`` and ``V`` when given - ``IL, I0, Rs, Rsh,`` and ``nNsVth (nNsVth = n*Ns*Vth)`` which - are described later. Returns a DataFrame which contains - the 5 points on the I-V curve specified in SAND2004-3535 [3]. - If all IL, I0, Rs, Rsh, and nNsVth are scalar, a single curve - will be returned, if any are Series (of the same length), multiple IV - curves will be calculated. + for ``I`` and ``V`` when given ``IL, I0, Rs, Rsh,`` and ``nNsVth + (nNsVth = n*Ns*Vth)`` which are described later. Returns a DataFrame + which contains the 5 points on the I-V curve specified in + SAND2004-3535 [3]. If all IL, I0, Rs, Rsh, and nNsVth are scalar, a + single curve will be returned, if any are Series (of the same + length), multiple IV curves will be calculated. The input parameters can be calculated using calcparams_desoto from meteorological data. @@ -861,8 +1276,8 @@ def singlediode(module, photocurrent, saturation_current, A DataFrame defining the SAPM performance parameters. photocurrent : float or Series - Light-generated current (photocurrent) in amperes under desired IV - curve conditions. Often abbreviated ``I_L``. + Light-generated current (photocurrent) in amperes under desired + IV curve conditions. Often abbreviated ``I_L``. saturation_current : float or Series Diode saturation current in amperes under desired IV curve @@ -877,18 +1292,19 @@ def singlediode(module, photocurrent, saturation_current, Often abbreviated ``Rsh``. nNsVth : float or Series - The product of three components. 1) The usual diode ideal - factor (n), 2) the number of cells in series (Ns), and 3) the cell - thermal voltage under the desired IV curve conditions (Vth). - The thermal voltage of the cell (in volts) may be calculated as + The product of three components. 1) The usual diode ideal factor + (n), 2) the number of cells in series (Ns), and 3) the cell + thermal voltage under the desired IV curve conditions (Vth). The + thermal voltage of the cell (in volts) may be calculated as ``k*temp_cell/q``, where k is Boltzmann's constant (J/K), - temp_cell is the temperature of the p-n junction in Kelvin, - and q is the charge of an electron (coulombs). + temp_cell is the temperature of the p-n junction in Kelvin, and + q is the charge of an electron (coulombs). Returns ------- - If ``photocurrent`` is a Series, a DataFrame with the following columns. - All columns have the same number of rows as the largest input DataFrame. + If ``photocurrent`` is a Series, a DataFrame with the following + columns. All columns have the same number of rows as the largest + input DataFrame. If ``photocurrent`` is a scalar, a dict with the following keys. @@ -908,12 +1324,12 @@ def singlediode(module, photocurrent, saturation_current, References ----------- - [1] S.R. Wenham, M.A. Green, M.E. Watt, "Applied Photovoltaics" - ISBN 0 86758 909 4 + [1] S.R. Wenham, M.A. Green, M.E. Watt, "Applied Photovoltaics" ISBN + 0 86758 909 4 - [2] A. Jain, A. Kapoor, "Exact analytical solutions of the parameters of - real solar cells using Lambert W-function", Solar Energy Materials - and Solar Cells, 81 (2004) 269-277. + [2] A. Jain, A. Kapoor, "Exact analytical solutions of the + parameters of real solar cells using Lambert W-function", Solar + Energy Materials and Solar Cells, 81 (2004) 269-277. [3] D. King et al, "Sandia Photovoltaic Array Performance Model", SAND2004-3535, Sandia National Laboratories, Albuquerque, NM @@ -923,7 +1339,6 @@ def singlediode(module, photocurrent, saturation_current, sapm calcparams_desoto ''' - pvl_logger.debug('pvsystem.singlediode') # Find short circuit current using Lambert W i_sc = i_from_v(resistance_shunt, resistance_series, nNsVth, 0.01, @@ -1097,17 +1512,17 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, conditions. Often abbreviated ``I_0``. nNsVth : float or Series - The product of three components. 1) The usual diode ideal - factor (n), 2) the number of cells in series (Ns), and 3) the cell - thermal voltage under the desired IV curve conditions (Vth). - The thermal voltage of the cell (in volts) may be calculated as + The product of three components. 1) The usual diode ideal factor + (n), 2) the number of cells in series (Ns), and 3) the cell + thermal voltage under the desired IV curve conditions (Vth). The + thermal voltage of the cell (in volts) may be calculated as ``k*temp_cell/q``, where k is Boltzmann's constant (J/K), - temp_cell is the temperature of the p-n junction in Kelvin, - and q is the charge of an electron (coulombs). + temp_cell is the temperature of the p-n junction in Kelvin, and + q is the charge of an electron (coulombs). photocurrent : float or Series - Light-generated current (photocurrent) in amperes under desired IV - curve conditions. Often abbreviated ``I_L``. + Light-generated current (photocurrent) in amperes under desired + IV curve conditions. Often abbreviated ``I_L``. Returns ------- @@ -1115,9 +1530,9 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, References ---------- - [1] A. Jain, A. Kapoor, "Exact analytical solutions of the parameters of - real solar cells using Lambert W-function", Solar Energy Materials - and Solar Cells, 81 (2004) 269-277. + [1] A. Jain, A. Kapoor, "Exact analytical solutions of the + parameters of real solar cells using Lambert W-function", Solar + Energy Materials and Solar Cells, 81 (2004) 269-277. ''' try: from scipy.special import lambertw @@ -1143,15 +1558,16 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, def snlinverter(inverter, v_dc, p_dc): ''' - Converts DC power and voltage to AC power using - Sandia's Grid-Connected PV Inverter model. + Converts DC power and voltage to AC power using Sandia's + Grid-Connected PV Inverter model. - Determines the AC power output of an inverter given the DC voltage, DC - power, and appropriate Sandia Grid-Connected Photovoltaic Inverter - Model parameters. The output, ac_power, is clipped at the maximum power - output, and gives a negative power during low-input power conditions, - but does NOT account for maximum power point tracking voltage windows - nor maximum current or voltage limits on the inverter. + Determines the AC power output of an inverter given the DC voltage, + DC power, and appropriate Sandia Grid-Connected Photovoltaic + Inverter Model parameters. The output, ac_power, is clipped at the + maximum power output, and gives a negative power during low-input + power conditions, but does NOT account for maximum power point + tracking voltage windows nor maximum current or voltage limits on + the inverter. Parameters ---------- @@ -1159,8 +1575,8 @@ def snlinverter(inverter, v_dc, p_dc): A DataFrame defining the inverter to be used, giving the inverter performance parameters according to the Sandia Grid-Connected Photovoltaic Inverter Model (SAND 2007-5036) [1]. - A set of inverter performance parameters are provided with pvlib, - or may be generated from a System Advisor Model (SAM) [2] + A set of inverter performance parameters are provided with + pvlib, or may be generated from a System Advisor Model (SAM) [2] library using retrievesam. Required DataFrame columns are: @@ -1192,8 +1608,8 @@ def snlinverter(inverter, v_dc, p_dc): ====== ============================================================ v_dc : float or Series - DC voltages, in volts, which are provided as input to the inverter. - Vdc must be >= 0. + DC voltages, in volts, which are provided as input to the + inverter. Vdc must be >= 0. p_dc : float or Series A scalar or DataFrame of DC powers, in watts, which are provided as input to the inverter. Pdc must be >= 0. @@ -1201,19 +1617,20 @@ def snlinverter(inverter, v_dc, p_dc): Returns ------- ac_power : float or Series - Modeled AC power output given the input - DC voltage, Vdc, and input DC power, Pdc. When ac_power would be - greater than Pac0, it is set to Pac0 to represent inverter - "clipping". When ac_power would be less than Ps0 (startup power - required), then ac_power is set to -1*abs(Pnt) to represent nightly - power losses. ac_power is not adjusted for maximum power point + Modeled AC power output given the input DC voltage, Vdc, and + input DC power, Pdc. When ac_power would be greater than Pac0, + it is set to Pac0 to represent inverter "clipping". When + ac_power would be less than Ps0 (startup power required), then + ac_power is set to -1*abs(Pnt) to represent nightly power + losses. ac_power is not adjusted for maximum power point tracking (MPPT) voltage windows or maximum current limits of the inverter. References ---------- - [1] SAND2007-5036, "Performance Model for Grid-Connected Photovoltaic - Inverters by D. King, S. Gonzalez, G. Galbraith, W. Boyson + [1] SAND2007-5036, "Performance Model for Grid-Connected + Photovoltaic Inverters by D. King, S. Gonzalez, G. Galbraith, W. + Boyson [2] System Advisor Model web page. https://sam.nrel.gov. diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 1420cf92e8..cf20106f03 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -20,15 +20,18 @@ except ImportError: pass +import warnings import numpy as np import pandas as pd - +from pvlib import atmosphere from pvlib.tools import localize_to_utc, datetime_to_djd, djd_to_datetime -def get_solarposition(time, location, method='nrel_numpy', pressure=101325, +def get_solarposition(time, latitude, longitude, + altitude=None, pressure=None, + method='nrel_numpy', temperature=12, **kwargs): """ A convenience wrapper for the solar position calculators. @@ -36,7 +39,14 @@ def get_solarposition(time, location, method='nrel_numpy', pressure=101325, Parameters ---------- time : pandas.DatetimeIndex - location : pvlib.Location object + latitude : float + longitude : float + altitude : None or float + If None, computed from pressure. Assumed to be 0 m + if pressure is also None. + pressure : None or float + If None, computed from altitude. Assumed to be 101325 Pa + if altitude is also None. method : string 'pyephem' uses the PyEphem package: :func:`pyephem` @@ -49,8 +59,6 @@ def get_solarposition(time, location, method='nrel_numpy', pressure=101325, described in [1], but also compiles the code first: :func:`spa_python` 'ephemeris' uses the pvlib ephemeris code: :func:`ephemeris` - pressure : float - Pascals. temperature : float Degrees C. @@ -67,29 +75,43 @@ def get_solarposition(time, location, method='nrel_numpy', pressure=101325, [3] NREL SPA code: http://rredc.nrel.gov/solar/codesandalgorithms/spa/ """ + if altitude is None and pressure is None: + altitude = 0. + pressure = 101325. + elif altitude is None: + altitude = atmosphere.pres2alt(pressure) + elif pressure is None: + pressure = atmosphere.alt2pres(altitude) + method = method.lower() if isinstance(time, dt.datetime): time = pd.DatetimeIndex([time, ]) if method == 'nrel_c': - ephem_df = spa_c(time, location, pressure, temperature, **kwargs) + ephem_df = spa_c(time, latitude, longitude, pressure, temperature, + **kwargs) elif method == 'nrel_numba': - ephem_df = spa_python(time, location, pressure, temperature, + ephem_df = spa_python(time, latitude, longitude, altitude, + pressure, temperature, how='numba', **kwargs) elif method == 'nrel_numpy': - ephem_df = spa_python(time, location, pressure, temperature, + ephem_df = spa_python(time, latitude, longitude, altitude, + pressure, temperature, how='numpy', **kwargs) elif method == 'pyephem': - ephem_df = pyephem(time, location, pressure, temperature, **kwargs) + ephem_df = pyephem(time, latitude, longitude, pressure, temperature, + **kwargs) elif method == 'ephemeris': - ephem_df = ephemeris(time, location, pressure, temperature, **kwargs) + ephem_df = ephemeris(time, latitude, longitude, pressure, temperature, + **kwargs) else: raise ValueError('Invalid solar position method') return ephem_df -def spa_c(time, location, pressure=101325, temperature=12, delta_t=67.0, +def spa_c(time, latitude, longitude, pressure=101325, altitude=0, + temperature=12, delta_t=67.0, raw_spa_output=False): """ Calculate the solar position using the C implementation of the NREL @@ -103,9 +125,13 @@ def spa_c(time, location, pressure=101325, temperature=12, delta_t=67.0, Parameters ---------- time : pandas.DatetimeIndex - location : pvlib.Location object + Localized or UTC. + latitude : float + longitude : float pressure : float Pressure in Pascals + altitude : float + Elevation above sea level. temperature : float Temperature in C delta_t : float @@ -147,7 +173,7 @@ def spa_c(time, location, pressure=101325, temperature=12, delta_t=67.0, pvl_logger.debug('using built-in spa code to calculate solar position') - time_utc = localize_to_utc(time, location) + time_utc = time spa_out = [] @@ -158,16 +184,16 @@ def spa_c(time, location, pressure=101325, temperature=12, delta_t=67.0, hour=date.hour, minute=date.minute, second=date.second, - timezone=0, # tz corrections handled above - latitude=location.latitude, - longitude=location.longitude, - elevation=location.altitude, + timezone=0, # must input localized or utc times + latitude=latitude, + longitude=longitude, + elevation=altitude, pressure=pressure / 100, temperature=temperature, delta_t=delta_t )) - spa_df = pd.DataFrame(spa_out, index=time_utc).tz_convert(location.tz) + spa_df = pd.DataFrame(spa_out, index=time_utc) if raw_spa_output: return spa_df @@ -211,7 +237,8 @@ def _spa_python_import(how): return spa -def spa_python(time, location, pressure=101325, temperature=12, delta_t=None, +def spa_python(time, latitude, longitude, + altitude=0, pressure=101325, temperature=12, delta_t=None, atmos_refract=None, how='numpy', numthreads=4): """ Calculate the solar position using a python implementation of the @@ -225,7 +252,10 @@ def spa_python(time, location, pressure=101325, temperature=12, delta_t=None, Parameters ---------- time : pandas.DatetimeIndex - location : pvlib.Location object + Localized or UTC. + latitude : float + longitude : float + altitude : float pressure : int or float, optional avg. yearly air pressure in Pascals. temperature : int or float, optional @@ -274,9 +304,9 @@ def spa_python(time, location, pressure=101325, temperature=12, delta_t=None, pvl_logger.debug('Calculating solar position with spa_python code') - lat = location.latitude - lon = location.longitude - elev = location.altitude + lat = latitude + lon = longitude + elev = altitude pressure = pressure / 100 # pressure must be in millibars for calculation delta_t = delta_t or 67.0 atmos_refract = atmos_refract or 0.5667 @@ -287,7 +317,7 @@ def spa_python(time, location, pressure=101325, temperature=12, delta_t=None, except (TypeError, ValueError): time = pd.DatetimeIndex([time, ]) - unixtime = localize_to_utc(time, location).astype(np.int64)/10**9 + unixtime = time.astype(np.int64)/10**9 spa = _spa_python_import(how) @@ -301,15 +331,11 @@ def spa_python(time, location, pressure=101325, temperature=12, delta_t=None, 'equation_of_time': eot}, index=time) - try: - result = result.tz_convert(location.tz) - except TypeError: - result = result.tz_localize(location.tz) - return result -def get_sun_rise_set_transit(time, location, how='numpy', delta_t=None, +def get_sun_rise_set_transit(time, latitude, longitude, how='numpy', + delta_t=None, numthreads=4): """ Calculate the sunrise, sunset, and sun transit times using the @@ -324,7 +350,8 @@ def get_sun_rise_set_transit(time, location, how='numpy', delta_t=None, ---------- time : pandas.DatetimeIndex Only the date part is used - location : pvlib.Location object + latitude : float + longitude : float delta_t : float, optional Difference between terrestrial time and UT1. By default, use USNO historical data and predictions @@ -351,8 +378,8 @@ def get_sun_rise_set_transit(time, location, how='numpy', delta_t=None, pvl_logger.debug('Calculating sunrise, set, transit with spa_python code') - lat = location.latitude - lon = location.longitude + lat = latitude + lon = longitude delta_t = delta_t or 67.0 if not isinstance(time, pd.DatetimeIndex): @@ -372,31 +399,26 @@ def get_sun_rise_set_transit(time, location, how='numpy', delta_t=None, # arrays are in seconds since epoch format, need to conver to timestamps transit = pd.to_datetime(transit, unit='s', utc=True).tz_convert( - location.tz).tolist() + time.tz).tolist() sunrise = pd.to_datetime(sunrise, unit='s', utc=True).tz_convert( - location.tz).tolist() + time.tz).tolist() sunset = pd.to_datetime(sunset, unit='s', utc=True).tz_convert( - location.tz).tolist() + time.tz).tolist() result = pd.DataFrame({'transit': transit, 'sunrise': sunrise, 'sunset': sunset}, index=time) - try: - result = result.tz_convert(location.tz) - except TypeError: - result = result.tz_localize(location.tz) - return result -def _ephem_setup(location, pressure, temperature): +def _ephem_setup(latitude, longitude, altitude, pressure, temperature): import ephem # initialize a PyEphem observer obs = ephem.Observer() - obs.lat = str(location.latitude) - obs.lon = str(location.longitude) - obs.elevation = location.altitude + obs.lat = str(latitude) + obs.lon = str(longitude) + obs.elevation = altitude obs.pressure = pressure / 100. # convert to mBar obs.temp = temperature @@ -405,14 +427,19 @@ def _ephem_setup(location, pressure, temperature): return obs, sun -def pyephem(time, location, pressure=101325, temperature=12): +def pyephem(time, latitude, longitude, altitude=0, pressure=101325, + temperature=12): """ Calculate the solar position using the PyEphem package. Parameters ---------- time : pandas.DatetimeIndex - location : pvlib.Location object + Localized or UTC. + latitude : float + longitude : float + altitude : float + distance above sea level. pressure : int or float, optional air pressure in Pascals. temperature : int or float, optional @@ -429,7 +456,6 @@ def pyephem(time, location, pressure=101325, temperature=12): See also -------- spa_python, spa_c, ephemeris - """ # Written by Will Holmgren (@wholmgren), University of Arizona, 2014 @@ -440,17 +466,22 @@ def pyephem(time, location, pressure=101325, temperature=12): pvl_logger.debug('using PyEphem to calculate solar position') - time_utc = localize_to_utc(time, location) + # if localized, convert to UTC. otherwise, assume UTC. + try: + time_utc = time.tz_convert('UTC') + except TypeError: + time_utc = time - sun_coords = pd.DataFrame(index=time_utc) + sun_coords = pd.DataFrame(index=time) - obs, sun = _ephem_setup(location, pressure, temperature) + obs, sun = _ephem_setup(latitude, longitude, altitude, + pressure, temperature) # make and fill lists of the sun's altitude and azimuth # this is the pressure and temperature corrected apparent alt/az. alts = [] azis = [] - for thetime in sun_coords.index: + for thetime in time_utc: obs.date = ephem.Date(thetime) sun.compute(obs) alts.append(sun.alt) @@ -463,7 +494,7 @@ def pyephem(time, location, pressure=101325, temperature=12): obs.pressure = 0 alts = [] azis = [] - for thetime in sun_coords.index: + for thetime in time_utc: obs.date = ephem.Date(thetime) sun.compute(obs) alts.append(sun.alt) @@ -477,13 +508,10 @@ def pyephem(time, location, pressure=101325, temperature=12): sun_coords['apparent_zenith'] = 90 - sun_coords['apparent_elevation'] sun_coords['zenith'] = 90 - sun_coords['elevation'] - try: - return sun_coords.tz_convert(location.tz) - except TypeError: - return sun_coords.tz_localize(location.tz) + return sun_coords -def ephemeris(time, location, pressure=101325, temperature=12): +def ephemeris(time, latitude, longitude, pressure=101325, temperature=12): """ Python-native solar position calculator. The accuracy of this code is not guaranteed. @@ -492,7 +520,8 @@ def ephemeris(time, location, pressure=101325, temperature=12): Parameters ---------- time : pandas.DatetimeIndex - location : pvlib.Location + latitude : float + longitude : float pressure : float or Series Ambient pressure (Pascals) temperature : float or Series @@ -536,9 +565,6 @@ def ephemeris(time, location, pressure=101325, temperature=12): # This helps a little bit: # http://www.cv.nrao.edu/~rfisher/Ephemerides/times.html - pvl_logger.debug('location=%s, temperature=%s, pressure=%s', - location, temperature, pressure) - # the inversion of longitude is due to the fact that this code was # originally written for the convention that positive longitude were for # locations west of the prime meridian. However, the correct convention (as @@ -547,8 +573,8 @@ def ephemeris(time, location, pressure=101325, temperature=12): # correct convention (e.g. Albuquerque is at -106 longitude), but it needs # to be inverted for use in the code. - Latitude = location.latitude - Longitude = -1 * location.longitude + Latitude = latitude + Longitude = -1 * longitude Abber = 20 / 3600. LatR = np.radians(Latitude) @@ -556,8 +582,11 @@ def ephemeris(time, location, pressure=101325, temperature=12): # the SPA algorithm needs time to be expressed in terms of # decimal UTC hours of the day of the year. - # first convert to utc - time_utc = localize_to_utc(time, location) + # if localized, convert to UTC. otherwise, assume UTC. + try: + time_utc = time.tz_convert('UTC') + except TypeError: + time_utc = time # strip out the day of the year and calculate the decimal hour DayOfYear = time_utc.dayofyear @@ -644,7 +673,7 @@ def ephemeris(time, location, pressure=101325, temperature=12): ApparentSunEl = SunEl + Refract # make output DataFrame - DFOut = pd.DataFrame(index=time_utc).tz_convert(location.tz) + DFOut = pd.DataFrame(index=time) DFOut['apparent_elevation'] = ApparentSunEl DFOut['elevation'] = SunEl DFOut['azimuth'] = SunAz @@ -655,8 +684,8 @@ def ephemeris(time, location, pressure=101325, temperature=12): return DFOut -def calc_time(lower_bound, upper_bound, location, attribute, value, - pressure=101325, temperature=12, xtol=1.0e-12): +def calc_time(lower_bound, upper_bound, latitude, longitude, attribute, value, + altitude=0, pressure=101325, temperature=12, xtol=1.0e-12): """ Calculate the time between lower_bound and upper_bound where the attribute is equal to value. Uses PyEphem for @@ -666,13 +695,16 @@ def calc_time(lower_bound, upper_bound, location, attribute, value, ---------- lower_bound : datetime.datetime upper_bound : datetime.datetime - location : pvlib.Location object + latitude : float + longitude : float attribute : str The attribute of a pyephem.Sun object that you want to solve for. Likely options are 'alt' and 'az' (which must be given in radians). value : int or float The value of the attribute to solve for + altitude : float + Distance above sea level. pressure : int or float, optional Air pressure in Pascals. Set to 0 for no atmospheric correction. @@ -699,7 +731,8 @@ def calc_time(lower_bound, upper_bound, location, attribute, value, except ImportError: raise ImportError('The calc_time function requires scipy') - obs, sun = _ephem_setup(location, pressure, temperature) + obs, sun = _ephem_setup(latitude, longitude, altitude, + pressure, temperature) def compute_attr(thetime, target, attr): obs.date = thetime @@ -712,7 +745,7 @@ def compute_attr(thetime, target, attr): djd_root = so.brentq(compute_attr, lb, ub, (value, attribute), xtol=xtol) - return djd_to_datetime(djd_root, location.tz) + return djd_to_datetime(djd_root) def pyephem_earthsun_distance(time): diff --git a/pvlib/test/__init__.py b/pvlib/test/__init__.py index e49bf69687..4153965a7c 100644 --- a/pvlib/test/__init__.py +++ b/pvlib/test/__init__.py @@ -18,6 +18,15 @@ def requires_scipy(test): return test if has_scipy else unittest.skip('requires scipy')(test) +try: + import ephem + has_ephem = True +except ImportError: + has_ephem = False + +def requires_ephem(test): + return test if has_ephem else unittest.skip('requires ephem')(test) + def incompatible_conda_linux_py3(test): """ Test won't work in Python 3.x due to Anaconda issue. diff --git a/pvlib/test/test_atmosphere.py b/pvlib/test/test_atmosphere.py index f211361242..2466c9a549 100644 --- a/pvlib/test/test_atmosphere.py +++ b/pvlib/test/test_atmosphere.py @@ -22,7 +22,8 @@ times_localized = times.tz_localize(tus.tz) -ephem_data = solarposition.get_solarposition(times, tus) +ephem_data = solarposition.get_solarposition(times_localized, tus.latitude, + tus.longitude) # need to add physical tests instead of just functional tests diff --git a/pvlib/test/test_clearsky.py b/pvlib/test/test_clearsky.py index ce9a010039..b6b50876d4 100644 --- a/pvlib/test/test_clearsky.py +++ b/pvlib/test/test_clearsky.py @@ -12,70 +12,80 @@ from pvlib import clearsky from pvlib import solarposition +from . import requires_scipy + # setup times and location to be tested. 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, tus) - +ephem_data = solarposition.get_solarposition(times_localized, tus.latitude, + tus.longitude) +@requires_scipy def test_ineichen_required(): # the clearsky function should call lookup_linke_turbidity by default - # will fail without scipy - expected = pd.DataFrame(np.array([[0.,0.,0.], - [0.,0.,0.], - [40.53660309,302.47614235,78.1470311], - [98.88372629,865.98938602,699.93403875], - [122.57870881,931.83716051,1038.62116584], - [109.30270612,899.88002304,847.68806472], - [64.25699595,629.91187925,254.53048144], - [0.,0.,0.], - [0.,0.,0.]]), + expected = pd.DataFrame( + np.array([[ 0. , 0. , 0. ], + [ 0. , 0. , 0. ], + [ 51.47811191, 265.33462162, 84.48262202], + [ 105.008507 , 832.29100407, 682.67761951], + [ 121.97988054, 901.31821834, 1008.02102657], + [ 112.57957512, 867.76297247, 824.61702926], + [ 76.69672675, 588.8462898 , 254.5808329 ], + [ 0. , 0. , 0. ], + [ 0. , 0. , 0. ]]), columns=['dhi', 'dni', 'ghi'], index=times_localized) - out = clearsky.ineichen(times, tus) + out = clearsky.ineichen(times_localized, tus.latitude, tus.longitude) assert_frame_equal(expected, out) - + def test_ineichen_supply_linke(): - expected = pd.DataFrame(np.array([[0.,0.,0.], - [0.,0.,0.], - [40.18673553,322.0649964,80.23287692], - [95.14405816,876.49507151,703.48596755], - [118.45873721,939.81653473,1042.34531752], - [105.36671577,909.113377,851.3283881], - [61.91607984,647.40869542,257.47471759], - [0.,0.,0.], - [0.,0.,0.]]), + expected = pd.DataFrame(np.array( + [[ 0. , 0. , 0. ], + [ 0. , 0. , 0. ], + [ 40.16490879, 321.71856556, 80.12815294], + [ 95.14336873, 876.49252839, 703.47605855], + [ 118.4587024 , 939.81646535, 1042.34480815], + [ 105.36645492, 909.11265773, 851.32459694], + [ 61.91187639, 647.35889938, 257.42691896], + [ 0. , 0. , 0. ], + [ 0. , 0. , 0. ]]), columns=['dhi', 'dni', 'ghi'], index=times_localized) - out = clearsky.ineichen(times, tus, linke_turbidity=3) + out = clearsky.ineichen(times_localized, tus.latitude, tus.longitude, + altitude=tus.altitude, + linke_turbidity=3) assert_frame_equal(expected, out) def test_ineichen_solpos(): - clearsky.ineichen(times, tus, linke_turbidity=3, + clearsky.ineichen(times_localized, tus.latitude, tus.longitude, + linke_turbidity=3, solarposition_method='ephemeris') def test_ineichen_airmass(): - expected = pd.DataFrame(np.array([[0.,0.,0.], - [0.,0.,0.], - [41.70761136,293.72203458,78.22953786], - [95.20590465,876.1650047,703.31872722], - [118.46089555,939.8078753,1042.33896321], - [105.39577655,908.97804342,851.24640259], - [62.35382269,642.91022293,256.55363539], - [0.,0.,0.], - [0.,0.,0.]]), + expected = pd.DataFrame( + np.array([[ 0. , 0. , 0. ], + [ 0. , 0. , 0. ], + [ 53.90422388, 257.01655613, 85.87406435], + [ 101.34055688, 842.92925705, 686.39337307], + [ 117.7573735 , 909.70367947, 1012.04184961], + [ 108.6233401 , 877.30589626, 828.49118038], + [ 75.23108133, 602.06895546, 257.10961202], + [ 0. , 0. , 0. ], + [ 0. , 0. , 0. ]]), columns=['dhi', 'dni', 'ghi'], index=times_localized) - out = clearsky.ineichen(times, tus, linke_turbidity=3, + out = clearsky.ineichen(times_localized, tus.latitude, tus.longitude, + linke_turbidity=3, airmass_model='simple') assert_frame_equal(expected, out) +@requires_scipy def test_lookup_linke_turbidity(): times = pd.date_range(start='2014-06-24', end='2014-06-25', freq='12h', tz=tus.tz) @@ -87,6 +97,7 @@ def test_lookup_linke_turbidity(): assert_series_equal(expected, out) +@requires_scipy def test_lookup_linke_turbidity_nointerp(): times = pd.date_range(start='2014-06-24', end='2014-06-25', freq='12h', tz=tus.tz) @@ -97,6 +108,7 @@ def test_lookup_linke_turbidity_nointerp(): assert_series_equal(expected, out) +@requires_scipy def test_lookup_linke_turbidity_months(): times = pd.date_range(start='2014-04-01', end='2014-07-01', freq='1M', tz=tus.tz) @@ -107,6 +119,7 @@ def test_lookup_linke_turbidity_months(): assert_series_equal(expected, out) +@requires_scipy def test_lookup_linke_turbidity_nointerp_months(): times = pd.date_range(start='2014-04-10', end='2014-07-10', freq='1M', tz=tus.tz) diff --git a/pvlib/test/test_irradiance.py b/pvlib/test/test_irradiance.py index 44911253c1..2fa2f5b072 100644 --- a/pvlib/test/test_irradiance.py +++ b/pvlib/test/test_irradiance.py @@ -15,6 +15,8 @@ from pvlib import irradiance from pvlib import atmosphere +from . import requires_ephem + # setup times and location to be tested. times = pd.date_range(start=datetime.datetime(2014, 6, 24), end=datetime.datetime(2014, 6, 26), freq='1Min') @@ -23,10 +25,12 @@ times_localized = times.tz_localize(tus.tz) -ephem_data = solarposition.get_solarposition(times, tus, method='pyephem') +ephem_data = solarposition.get_solarposition(times, tus.latitude, + tus.longitude, method='nrel_numpy') -irrad_data = clearsky.ineichen(times, tus, linke_turbidity=3, - solarposition_method='pyephem') +irrad_data = clearsky.ineichen(times, tus.latitude, tus.longitude, + altitude=tus.altitude, linke_turbidity=3, + solarposition_method='nrel_numpy') dni_et = irradiance.extraradiation(times.dayofyear) @@ -58,15 +62,18 @@ def test_extraradiation_spencer(): 1382, irradiance.extraradiation(300, method='spencer'), -1) +@requires_ephem def test_extraradiation_ephem_dtindex(): irradiance.extraradiation(times, method='pyephem') +@requires_ephem def test_extraradiation_ephem_scalar(): assert_almost_equals( 1382, irradiance.extraradiation(300, method='pyephem').values[0], -1) +@requires_ephem def test_extraradiation_ephem_doyarray(): irradiance.extraradiation(times.dayofyear, method='pyephem') @@ -109,21 +116,21 @@ def test_klucher_series_float(): def test_klucher_series(): irradiance.klucher(40, 180, irrad_data['dhi'], irrad_data['ghi'], ephem_data['apparent_zenith'], - ephem_data['apparent_azimuth']) + ephem_data['azimuth']) def test_haydavies(): irradiance.haydavies(40, 180, irrad_data['dhi'], irrad_data['dni'], dni_et, ephem_data['apparent_zenith'], - ephem_data['apparent_azimuth']) + ephem_data['azimuth']) def test_reindl(): irradiance.reindl(40, 180, irrad_data['dhi'], irrad_data['dni'], irrad_data['ghi'], dni_et, ephem_data['apparent_zenith'], - ephem_data['apparent_azimuth']) + ephem_data['azimuth']) def test_king(): @@ -135,7 +142,7 @@ def test_perez(): AM = atmosphere.relativeairmass(ephem_data['apparent_zenith']) irradiance.perez(40, 180, irrad_data['dhi'], irrad_data['dni'], dni_et, ephem_data['apparent_zenith'], - ephem_data['apparent_azimuth'], AM) + ephem_data['azimuth'], AM) # klutcher (misspelling) will be removed in 0.3 def test_total_irrad(): @@ -152,25 +159,30 @@ def test_total_irrad(): dni_extra=dni_et, airmass=AM, model=model, surface_type='urban') + + assert total.columns.tolist() == ['poa_global', 'poa_direct', + 'poa_diffuse', 'poa_sky_diffuse', + 'poa_ground_diffuse'] def test_globalinplane(): aoi = irradiance.aoi(40, 180, ephem_data['apparent_zenith'], - ephem_data['apparent_azimuth']) + ephem_data['azimuth']) airmass = atmosphere.relativeairmass(ephem_data['apparent_zenith']) gr_sand = irradiance.grounddiffuse(40, ghi, surface_type='sand') diff_perez = irradiance.perez( 40, 180, irrad_data['dhi'], irrad_data['dni'], dni_et, - ephem_data['apparent_zenith'], ephem_data['apparent_azimuth'], airmass) + ephem_data['apparent_zenith'], ephem_data['azimuth'], airmass) irradiance.globalinplane( aoi=aoi, dni=irrad_data['dni'], poa_sky_diffuse=diff_perez, poa_ground_diffuse=gr_sand) def test_disc_keys(): - clearsky_data = clearsky.ineichen(times, tus, linke_turbidity=3) + clearsky_data = clearsky.ineichen(times, tus.latitude, tus.longitude, + linke_turbidity=3) disc_data = irradiance.disc(clearsky_data['ghi'], ephem_data['zenith'], - ephem_data.index) + ephem_data.index) assert 'dni' in disc_data.columns assert 'kt' in disc_data.columns assert 'airmass' in disc_data.columns @@ -187,7 +199,8 @@ def test_disc_value(): def test_dirint(): - clearsky_data = clearsky.ineichen(times, tus, linke_turbidity=3) + clearsky_data = clearsky.ineichen(times, tus.latitude, tus.longitude, + linke_turbidity=3) pressure = 93193. dirint_data = irradiance.dirint(clearsky_data['ghi'], ephem_data['zenith'], ephem_data.index, pressure=pressure) diff --git a/pvlib/test/test_location.py b/pvlib/test/test_location.py index 9cf24540e2..9ac4719cd3 100644 --- a/pvlib/test/test_location.py +++ b/pvlib/test/test_location.py @@ -1,9 +1,13 @@ -import logging -pvl_logger = logging.getLogger('pvlib') +import datetime +import numpy as np +from numpy import nan +import pandas as pd import pytz + from nose.tools import raises from pytz.exceptions import UnknownTimeZoneError +from pandas.util.testing import assert_series_equal, assert_frame_equal from ..location import Location @@ -11,27 +15,148 @@ def test_location_required(): Location(32.2, -111) - + def test_location_all(): Location(32.2, -111, 'US/Arizona', 700, 'Tucson') -@raises(UnknownTimeZoneError) +@raises(UnknownTimeZoneError) def test_location_invalid_tz(): Location(32.2, -111, 'invalid') - + @raises(TypeError) def test_location_invalid_tz_type(): - Location(32.2, -111, 5) - + Location(32.2, -111, [5]) + def test_location_pytz_tz(): Location(32.2, -111, aztz) +def test_location_int_float_tz(): + Location(32.2, -111, -7) + Location(32.2, -111, -7.0) + def test_location_print_all(): tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') expected_str = 'Tucson: latitude=32.2, longitude=-111, tz=US/Arizona, altitude=700' assert tus.__str__() == expected_str - + def test_location_print_pytz(): tus = Location(32.2, -111, aztz, 700, 'Tucson') expected_str = 'Tucson: latitude=32.2, longitude=-111, tz=US/Arizona, altitude=700' assert tus.__str__() == expected_str + + +def test_get_clearsky(): + tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + times = pd.DatetimeIndex(start='20160101T0600-0700', + end='20160101T1800-0700', + freq='3H') + clearsky = tus.get_clearsky(times) + expected = pd.DataFrame(data=np.array( + [[ 0. , 0. , 0. ], + [ 49.99257714, 762.92663984, 258.84368467], + [ 70.79757257, 957.14396999, 612.04545874], + [ 59.01570645, 879.06844381, 415.26616693], + [ 0. , 0. , 0. ]]), + columns=['dhi', 'dni', 'ghi'], + index=times) + assert_frame_equal(expected, clearsky) + + +def test_get_clearsky_haurwitz(): + tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + times = pd.DatetimeIndex(start='20160101T0600-0700', + end='20160101T1800-0700', + freq='3H') + clearsky = tus.get_clearsky(times, model='haurwitz') + expected = pd.DataFrame(data=np.array( + [[ 0. ], + [ 242.30085588], + [ 559.38247117], + [ 384.6873791 ], + [ 0. ]]), + columns=['ghi'], + index=times) + assert_frame_equal(expected, clearsky) + + +@raises(ValueError) +def test_get_clearsky_valueerror(): + tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + times = pd.DatetimeIndex(start='20160101T0600-0700', + end='20160101T1800-0700', + freq='3H') + clearsky = tus.get_clearsky(times, model='invalid_model') + + +def test_from_tmy_3(): + from .test_tmy import tmy3_testfile + from ..tmy import readtmy3 + data, meta = readtmy3(tmy3_testfile) + print(meta) + loc = Location.from_tmy(meta, data) + assert loc.name is not None + assert loc.altitude != 0 + assert loc.tz != 'UTC' + assert_frame_equal(loc.tmy_data, data) + + +def test_from_tmy_2(): + from .test_tmy import tmy2_testfile + from ..tmy import readtmy2 + data, meta = readtmy2(tmy2_testfile) + print(meta) + loc = Location.from_tmy(meta, data) + assert loc.name is not None + assert loc.altitude != 0 + assert loc.tz != 'UTC' + assert_frame_equal(loc.tmy_data, data) + + +def test_get_solarposition(): + from .test_solarposition import expected, golden_mst + times = pd.date_range(datetime.datetime(2003,10,17,12,30,30), + periods=1, freq='D', tz=golden_mst.tz) + ephem_data = golden_mst.get_solarposition(times, temperature=11) + ephem_data = np.round(ephem_data, 3) + this_expected = expected.copy() + this_expected.index = times + this_expected = np.round(this_expected, 3) + print(this_expected, ephem_data[expected.columns]) + assert_frame_equal(this_expected, ephem_data[expected.columns]) + + +def test_get_airmass(): + tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + times = pd.DatetimeIndex(start='20160101T0600-0700', + end='20160101T1800-0700', + freq='3H') + airmass = tus.get_airmass(times) + expected = pd.DataFrame(data=np.array( + [[ nan, nan], + [ 3.61046506, 3.32072602], + [ 1.76470864, 1.62309115], + [ 2.45582153, 2.25874238], + [ nan, nan]]), + columns=['airmass_relative', 'airmass_absolute'], + index=times) + assert_frame_equal(expected, airmass) + + airmass = tus.get_airmass(times, model='young1994') + expected = pd.DataFrame(data=np.array( + [[ nan, nan], + [ 3.6075018 , 3.31800056], + [ 1.7641033 , 1.62253439], + [ 2.45413091, 2.25718744], + [ nan, nan]]), + columns=['airmass_relative', 'airmass_absolute'], + index=times) + assert_frame_equal(expected, airmass) + + +@raises(ValueError) +def test_get_airmass_valueerror(): + tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + times = pd.DatetimeIndex(start='20160101T0600-0700', + end='20160101T1800-0700', + freq='3H') + clearsky = tus.get_airmass(times, model='invalid_model') diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py new file mode 100644 index 0000000000..56d2aef89e --- /dev/null +++ b/pvlib/test/test_modelchain.py @@ -0,0 +1,200 @@ +import numpy as np +import pandas as pd + +from pvlib import modelchain, pvsystem +from pvlib.modelchain import ModelChain +from pvlib.pvsystem import PVSystem +from pvlib.location import Location + +from pandas.util.testing import assert_series_equal, assert_frame_equal +from nose.tools import with_setup, raises + +# should store this test data locally, but for now... +sam_data = {} +def retrieve_sam_network(): + sam_data['cecmod'] = pvsystem.retrieve_sam('cecmod') + sam_data['sandiamod'] = pvsystem.retrieve_sam('sandiamod') + sam_data['cecinverter'] = pvsystem.retrieve_sam('cecinverter') + + +def mc_setup(): + # limit network usage + try: + modules = sam_data['sandiamod'] + except KeyError: + retrieve_sam_network() + modules = sam_data['sandiamod'] + + module = 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, + inverter_parameters=inverter) + + location = Location(32.2, -111, altitude=700) + + return system, location + + +def test_ModelChain_creation(): + system, location = mc_setup() + mc = ModelChain(system, location) + + +def test_orientation_strategy(): + strategies = {None: (0, 180), 'None': (0, 180), + 'south_at_latitude_tilt': (32.2, 180), + 'flat': (0, 180)} + + for strategy, expected in strategies.items(): + yield run_orientation_strategy, strategy, expected + + +def run_orientation_strategy(strategy, expected): + system = PVSystem() + location = Location(32.2, -111, altitude=700) + + mc = ModelChain(system, location, orientation_strategy=strategy) + + # the || accounts for the coercion of 'None' to None + assert (mc.orientation_strategy == strategy or + mc.orientation_strategy == None) + assert system.surface_tilt == expected[0] + assert system.surface_azimuth == expected[1] + + +def test_run_model(): + system, location = mc_setup() + mc = ModelChain(system, location) + times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') + ac = mc.run_model(times).ac + + expected = pd.Series(np.array([ 1.82033564e+02, -2.00000000e-02]), + index=times) + assert_series_equal(ac, expected) + + +def test_run_model_with_irradiance(): + system, location = mc_setup() + mc = ModelChain(system, location) + times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') + irradiance = pd.DataFrame({'dni':900, 'ghi':600, 'dhi':150}, + index=times) + ac = mc.run_model(times, irradiance=irradiance).ac + + expected = pd.Series(np.array([ 1.90054749e+02, -2.00000000e-02]), + index=times) + assert_series_equal(ac, expected) + + +def test_run_model_with_weather(): + system, location = mc_setup() + mc = ModelChain(system, location) + times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') + weather = pd.DataFrame({'wind_speed':5, 'temp_air':10}, index=times) + ac = mc.run_model(times, weather=weather).ac + + expected = pd.Series(np.array([ 1.99952400e+02, -2.00000000e-02]), + index=times) + assert_series_equal(ac, expected) + + +@raises(ValueError) +def test_bad_get_orientation(): + modelchain.get_orientation('bad value') + + +@raises(ValueError) +def test_basic_chain_required(): + times = pd.DatetimeIndex(start='20160101 1200-0700', + end='20160101 1800-0700', freq='6H') + latitude = 32 + longitude = -111 + altitude = 700 + modules = sam_data['sandiamod'] + module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] + inverters = sam_data['cecinverter'] + inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'] + + dc, ac = modelchain.basic_chain(times, latitude, longitude, + module_parameters, inverter_parameters, + altitude=altitude) + + +def test_basic_chain_alt_az(): + times = pd.DatetimeIndex(start='20160101 1200-0700', + end='20160101 1800-0700', freq='6H') + latitude = 32.2 + longitude = -111 + altitude = 700 + surface_tilt = 0 + surface_azimuth = 0 + modules = sam_data['sandiamod'] + module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] + inverters = sam_data['cecinverter'] + inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'] + + dc, ac = modelchain.basic_chain(times, latitude, longitude, + module_parameters, inverter_parameters, + surface_tilt=surface_tilt, + surface_azimuth=surface_azimuth) + + expected = pd.Series(np.array([ 1.14490928477e+02, -2.00000000e-02]), + index=times) + assert_series_equal(ac, expected) + + +def test_basic_chain_strategy(): + times = pd.DatetimeIndex(start='20160101 1200-0700', + end='20160101 1800-0700', freq='6H') + latitude = 32.2 + longitude = -111 + altitude = 700 + modules = sam_data['sandiamod'] + module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] + inverters = sam_data['cecinverter'] + inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'] + + dc, ac = modelchain.basic_chain(times, latitude, longitude, + module_parameters, inverter_parameters, + orientation_strategy='south_at_latitude_tilt', + altitude=altitude) + + expected = pd.Series(np.array([ 1.82033563543e+02, -2.00000000e-02]), + index=times) + assert_series_equal(ac, expected) + + +def test_basic_chain_altitude_pressure(): + times = pd.DatetimeIndex(start='20160101 1200-0700', + end='20160101 1800-0700', freq='6H') + latitude = 32.2 + longitude = -111 + altitude = 700 + surface_tilt = 0 + surface_azimuth = 0 + modules = sam_data['sandiamod'] + module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] + inverters = sam_data['cecinverter'] + inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'] + + dc, ac = modelchain.basic_chain(times, latitude, longitude, + module_parameters, inverter_parameters, + surface_tilt=surface_tilt, + surface_azimuth=surface_azimuth, + pressure=93194) + + expected = pd.Series(np.array([ 1.15771428788e+02, -2.00000000e-02]), + index=times) + assert_series_equal(ac, expected) + + dc, ac = modelchain.basic_chain(times, latitude, longitude, + module_parameters, inverter_parameters, + surface_tilt=surface_tilt, + surface_azimuth=surface_azimuth, + altitude=altitude) + + expected = pd.Series(np.array([ 1.15771428788e+02, -2.00000000e-02]), + index=times) + assert_series_equal(ac, expected) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 0abc9c78fc..94281d50a1 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -1,11 +1,9 @@ -import logging -pvl_logger = logging.getLogger('pvlib') - import inspect import os import datetime import numpy as np +from numpy import nan import pandas as pd from nose.tools import assert_equals, assert_almost_equals @@ -20,14 +18,20 @@ from pvlib import solarposition from pvlib.location import Location -tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') +latitude = 32.2 +longitude = -111 +tus = Location(latitude, longitude, 'US/Arizona', 700, 'Tucson') times = pd.date_range(start=datetime.datetime(2014,1,1), end=datetime.datetime(2014,1,2), freq='1Min') -ephem_data = solarposition.get_solarposition(times, tus, method='pyephem') -irrad_data = clearsky.ineichen(times, tus, linke_turbidity=3, - solarposition_method='pyephem') +ephem_data = solarposition.get_solarposition(times, + latitude=latitude, + longitude=longitude, + method='nrel_numpy') +irrad_data = clearsky.ineichen(times, latitude=latitude, longitude=longitude, + linke_turbidity=3, + solarposition_method='nrel_numpy') aoi = irradiance.aoi(0, 0, ephem_data['apparent_zenith'], - ephem_data['apparent_azimuth']) + ephem_data['azimuth']) am = atmosphere.relativeairmass(ephem_data.apparent_zenith) meta = {'latitude': 37.8, @@ -86,13 +90,41 @@ def test_systemdef_dict(): def test_ashraeiam(): - thetas = pd.Series(np.linspace(-180,180,361)) + thetas = np.linspace(-90, 90, 9) iam = pvsystem.ashraeiam(.05, thetas) + expected = np.array([ nan, 0.9193437 , 0.97928932, 0.99588039, 1. , + 0.99588039, 0.97928932, 0.9193437 , nan]) + assert np.isclose(iam, expected, equal_nan=True).all() + + +def test_PVSystem_ashraeiam(): + module_parameters = pd.Series({'b': 0.05}) + system = pvsystem.PVSystem(module='blah', inverter='blarg', + module_parameters=module_parameters) + thetas = np.linspace(-90, 90, 9) + iam = system.ashraeiam(thetas) + expected = np.array([ nan, 0.9193437 , 0.97928932, 0.99588039, 1. , + 0.99588039, 0.97928932, 0.9193437 , nan]) + assert np.isclose(iam, expected, equal_nan=True).all() def test_physicaliam(): - thetas = pd.Series(np.linspace(-180,180,361)) + thetas = np.linspace(-90, 90, 9) iam = pvsystem.physicaliam(4, 0.002, 1.526, thetas) + expected = np.array([ nan, 0.8893998 , 0.98797788, 0.99926198, nan, + 0.99926198, 0.98797788, 0.8893998 , nan]) + assert np.isclose(iam, expected, equal_nan=True).all() + + +def test_PVSystem_physicaliam(): + module_parameters = pd.Series({'K': 4, 'L': 0.002, 'n': 1.526}) + system = pvsystem.PVSystem(module='blah', inverter='blarg', + module_parameters=module_parameters) + thetas = np.linspace(-90, 90, 9) + iam = system.physicaliam(thetas) + expected = np.array([ nan, 0.8893998 , 0.98797788, 0.99926198, nan, + 0.99926198, 0.98797788, 0.8893998 , nan]) + assert np.isclose(iam, expected, equal_nan=True).all() # if this completes successfully we'll be able to do more tests below. @@ -105,23 +137,98 @@ def test_retrieve_sam_network(): def test_sapm(): modules = sam_data['sandiamod'] - module = modules.Canadian_Solar_CS5P_220M___2009_ - - sapm = pvsystem.sapm(module, irrad_data['dni'], + module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] + times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') + irrad_data = pd.DataFrame({'dni':[0,1000], 'ghi':[0,600], 'dhi':[0,100]}, + index=times) + am = pd.Series([0, 2.25], index=times) + aoi = pd.Series([180, 30], index=times) + + sapm = pvsystem.sapm(module_parameters, irrad_data['dni'], irrad_data['dhi'], 25, am, aoi) + + expected = pd.DataFrame(np.array( + [[ 0. , 0. , 0. , 0. , + 0. , 0. , 0. , 0. ], + [ 5.74526799, 5.12194115, 59.67914031, 48.41924255, + 248.00051089, 5.61787615, 3.52581308, 1.12848138]]), + columns=['i_sc', 'i_mp', 'v_oc', 'v_mp', 'p_mp', 'i_x', 'i_xx', + 'effective_irradiance'], + index=times) + + assert_frame_equal(sapm, expected) - sapm = pvsystem.sapm(module.to_dict(), irrad_data['dni'], + # just make sure it works with a dict input + sapm = pvsystem.sapm(module_parameters.to_dict(), irrad_data['dni'], irrad_data['dhi'], 25, am, aoi) +def test_PVSystem_sapm(): + modules = sam_data['sandiamod'] + module = 'Canadian_Solar_CS5P_220M___2009_' + module_parameters = modules[module] + system = pvsystem.PVSystem(module=module, + module_parameters=module_parameters) + times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') + irrad_data = pd.DataFrame({'dni':[0,1000], 'ghi':[0,600], 'dhi':[0,100]}, + index=times) + am = pd.Series([0, 2.25], index=times) + aoi = pd.Series([180, 30], index=times) + + sapm = system.sapm(irrad_data['dni'], irrad_data['dhi'], 25, am, aoi) + + expected = pd.DataFrame(np.array( + [[ 0. , 0. , 0. , 0. , + 0. , 0. , 0. , 0. ], + [ 5.74526799, 5.12194115, 59.67914031, 48.41924255, + 248.00051089, 5.61787615, 3.52581308, 1.12848138]]), + columns=['i_sc', 'i_mp', 'v_oc', 'v_mp', 'p_mp', 'i_x', 'i_xx', + 'effective_irradiance'], + index=times) + + assert_frame_equal(sapm, expected) + + def test_calcparams_desoto(): - cecmodule = sam_data['cecmod'].Example_Module - pvsystem.calcparams_desoto(irrad_data['ghi'], - temp_cell=25, - alpha_isc=cecmodule['alpha_sc'], - module_parameters=cecmodule, - EgRef=1.121, - dEgdT=-0.0002677) + module = 'Example_Module' + module_parameters = sam_data['cecmod'][module] + times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') + poa_data = pd.Series([0, 800], index=times) + + IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( + poa_data, + temp_cell=25, + alpha_isc=module_parameters['alpha_sc'], + module_parameters=module_parameters, + EgRef=1.121, + dEgdT=-0.0002677) + + assert_series_equal(np.round(IL, 3), pd.Series([0.0, 6.036], index=times)) + assert_almost_equals(I0, 1.943e-9) + assert_almost_equals(Rs, 0.094) + assert_series_equal(np.round(Rsh, 3), pd.Series([np.inf, 19.65], index=times)) + assert_almost_equals(nNsVth, 0.473) + + +def test_PVSystem_calcparams_desoto(): + module = 'Example_Module' + module_parameters = sam_data['cecmod'][module].copy() + module_parameters['EgRef'] = 1.121 + module_parameters['dEgdT'] = -0.0002677 + system = pvsystem.PVSystem(module=module, + module_parameters=module_parameters) + times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') + poa_data = pd.Series([0, 800], index=times) + temp_cell = 25 + + IL, I0, Rs, Rsh, nNsVth = system.calcparams_desoto(poa_data, temp_cell) + + assert_series_equal(np.round(IL, 3), pd.Series([0.0, 6.036], index=times)) + assert_almost_equals(I0, 1.943e-9) + assert_almost_equals(Rs, 0.094) + assert_series_equal(np.round(Rsh, 3), pd.Series([np.inf, 19.65], index=times)) + assert_almost_equals(nNsVth, 0.473) + @incompatible_conda_linux_py3 def test_i_from_v(): @@ -129,22 +236,56 @@ def test_i_from_v(): assert_almost_equals(-299.746389916, output, 5) -def test_singlediode_series(): - cecmodule = sam_data['cecmod'].Example_Module +@incompatible_conda_linux_py3 +def test_PVSystem_i_from_v(): + module = 'Example_Module' + module_parameters = sam_data['cecmod'][module] + system = pvsystem.PVSystem(module=module, + module_parameters=module_parameters) + output = system.i_from_v(20, .1, .5, 40, 6e-7, 7) + assert_almost_equals(-299.746389916, output, 5) + + +def test_singlediode_series(): + module = 'Example_Module' + module_parameters = sam_data['cecmod'][module] + times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') + poa_data = pd.Series([0, 800], index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( - irrad_data['ghi'], + poa_data, temp_cell=25, - alpha_isc=cecmodule['alpha_sc'], - module_parameters=cecmodule, + alpha_isc=module_parameters['alpha_sc'], + module_parameters=module_parameters, EgRef=1.121, dEgdT=-0.0002677) - out = pvsystem.singlediode(cecmodule, IL, I0, Rs, Rsh, nNsVth) + out = pvsystem.singlediode(module_parameters, IL, I0, Rs, Rsh, nNsVth) assert isinstance(out, pd.DataFrame) + @incompatible_conda_linux_py3 -def test_singlediode_series(): - cecmodule = sam_data['cecmod'].Example_Module - out = pvsystem.singlediode(cecmodule, 7, 6e-7, .1, 20, .5) +def test_singlediode_floats(): + module = 'Example_Module' + module_parameters = sam_data['cecmod'][module] + out = pvsystem.singlediode(module_parameters, 7, 6e-7, .1, 20, .5) + expected = {'i_xx': 4.2549732697234193, + 'i_mp': 6.1390251797935704, + 'v_oc': 8.1147298764528042, + 'p_mp': 38.194165464983037, + 'i_x': 6.7556075876880621, + 'i_sc': 6.9646747613963198, + 'v_mp': 6.221535886625464} + assert isinstance(out, dict) + for k, v in out.items(): + assert_almost_equals(expected[k], v, 5) + + +@incompatible_conda_linux_py3 +def test_PVSystem_singlediode_floats(): + module = 'Example_Module' + module_parameters = sam_data['cecmod'][module] + system = pvsystem.PVSystem(module=module, + module_parameters=module_parameters) + out = system.singlediode(7, 6e-7, .1, 20, .5) expected = {'i_xx': 4.2549732697234193, 'i_mp': 6.1390251797935704, 'v_oc': 8.1147298764528042, @@ -165,6 +306,16 @@ def test_sapm_celltemp(): [-3.47, -.0594, 3])) +def test_sapm_celltemp_dict_like(): + default = pvsystem.sapm_celltemp(900, 5, 20) + assert_almost_equals(43.509, default.ix[0, 'temp_cell'], 3) + assert_almost_equals(40.809, default.ix[0, 'temp_module'], 3) + model = {'a':-3.47, 'b':-.0594, 'deltaT':3} + assert_frame_equal(default, pvsystem.sapm_celltemp(900, 5, 20, model)) + model = pd.Series(model) + assert_frame_equal(default, pvsystem.sapm_celltemp(900, 5, 20, model)) + + def test_sapm_celltemp_with_index(): times = pd.DatetimeIndex(start='2015-01-01', end='2015-01-02', freq='12H') temps = pd.Series([0, 10, 5], index=times) @@ -179,7 +330,23 @@ def test_sapm_celltemp_with_index(): assert_frame_equal(expected, pvtemps) - + +def test_PVSystem_sapm_celltemp(): + system = pvsystem.PVSystem(racking_model='roof_mount_cell_glassback') + times = pd.DatetimeIndex(start='2015-01-01', end='2015-01-02', freq='12H') + temps = pd.Series([0, 10, 5], index=times) + irrads = pd.Series([0, 500, 0], index=times) + winds = pd.Series([10, 5, 0], index=times) + + pvtemps = system.sapm_celltemp(irrads, winds, temps) + + expected = pd.DataFrame({'temp_cell':[0., 30.56763059, 5.], + 'temp_module':[0., 30.06763059, 5.]}, + index=times) + + assert_frame_equal(expected, pvtemps) + + def test_snlinverter(): inverters = sam_data['cecinverter'] testinv = 'ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_' @@ -191,6 +358,19 @@ def test_snlinverter(): assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000])) +def test_PVSystem_snlinverter(): + inverters = sam_data['cecinverter'] + testinv = 'ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_' + system = pvsystem.PVSystem(inverter=testinv, + inverter_parameters=inverters[testinv]) + vdcs = pd.Series(np.linspace(0,50,3)) + idcs = pd.Series(np.linspace(0,11,3)) + pdcs = idcs * vdcs + + pacs = system.snlinverter(vdcs, pdcs) + assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000])) + + def test_snlinverter_float(): inverters = sam_data['cecinverter'] testinv = 'ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_' @@ -201,3 +381,79 @@ def test_snlinverter_float(): pacs = pvsystem.snlinverter(inverters[testinv], vdcs, pdcs) assert_almost_equals(pacs, 132.004278, 5) + +def test_PVSystem_creation(): + pv_system = pvsystem.PVSystem(module='blah', inverter='blarg') + + +def test_PVSystem_get_aoi(): + system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) + aoi = system.get_aoi(30, 225) + assert np.round(aoi, 4) == 42.7408 + + +def test_PVSystem_get_irradiance(): + system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) + times = pd.DatetimeIndex(start='20160101 1200-0700', + end='20160101 1800-0700', freq='6H') + location = Location(latitude=32, longitude=-111) + solar_position = location.get_solarposition(times) + irrads = pd.DataFrame({'dni':[900,0], 'ghi':[600,0], 'dhi':[100,0]}, + index=times) + + irradiance = system.get_irradiance(solar_position['apparent_zenith'], + solar_position['azimuth'], + irrads['dni'], + irrads['ghi'], + irrads['dhi']) + + expected = pd.DataFrame(data=np.array( + [[ 883.65494055, 745.86141676, 137.79352379, 126.397131 , + 11.39639279], + [ 0. , -0. , 0. , 0. , 0. ]]), + columns=['poa_global', 'poa_direct', + 'poa_diffuse', 'poa_sky_diffuse', + 'poa_ground_diffuse'], + index=times) + + irradiance = np.round(irradiance, 4) + expected = np.round(expected, 4) + assert_frame_equal(irradiance, expected) + + +def test_PVSystem_localize_with_location(): + system = pvsystem.PVSystem(module='blah', inverter='blarg') + location = Location(latitude=32, longitude=-111) + localized_system = system.localize(location=location) + + assert localized_system.module == 'blah' + assert localized_system.inverter == 'blarg' + assert localized_system.latitude == 32 + assert localized_system.longitude == -111 + + +def test_PVSystem_localize_with_latlon(): + system = pvsystem.PVSystem(module='blah', inverter='blarg') + localized_system = system.localize(latitude=32, longitude=-111) + + assert localized_system.module == 'blah' + assert localized_system.inverter == 'blarg' + assert localized_system.latitude == 32 + assert localized_system.longitude == -111 + + +# we could retest each of the models tested above +# when they are attached to LocalizedPVSystem, but +# that's probably not necessary at this point. + + +def test_LocalizedPVSystem_creation(): + localized_system = pvsystem.LocalizedPVSystem(latitude=32, + longitude=-111, + module='blah', + inverter='blarg') + + assert localized_system.module == 'blah' + assert localized_system.inverter == 'blarg' + assert localized_system.latitude == 32 + assert localized_system.longitude == -111 diff --git a/pvlib/test/test_solarposition.py b/pvlib/test/test_solarposition.py index 8123f7bfed..cf3f3b98ac 100644 --- a/pvlib/test/test_solarposition.py +++ b/pvlib/test/test_solarposition.py @@ -9,83 +9,93 @@ from nose.tools import raises, assert_almost_equals from nose.plugins.skip import SkipTest -from pandas.util.testing import assert_frame_equal +from pandas.util.testing import assert_frame_equal, assert_index_equal from pvlib.location import Location from pvlib import solarposition +from . import requires_ephem # setup times and locations to be tested. -times = pd.date_range(start=datetime.datetime(2014,6,24), +times = pd.date_range(start=datetime.datetime(2014,6,24), end=datetime.datetime(2014,6,26), freq='15Min') tus = Location(32.2, -111, 'US/Arizona', 700) # no DST issues possible +# In 2003, DST in US was from April 6 to October 26 golden_mst = Location(39.742476, -105.1786, 'MST', 1830.14) # no DST issues possible golden = Location(39.742476, -105.1786, 'America/Denver', 1830.14) # DST issues possible times_localized = times.tz_localize(tus.tz) +tol = 5 + +expected = pd.DataFrame({'elevation': 39.872046, + 'apparent_zenith': 50.111622, + 'azimuth': 194.340241, + 'apparent_elevation': 39.888378}, + index=['2003-10-17T12:30:30Z']) + # the physical tests are run at the same time as the NREL SPA test. # pyephem reproduces the NREL result to 2 decimal places. -# this doesn't mean that one code is better than the other. +# this doesn't mean that one code is better than the other. -def test_spa_physical(): - times = pd.date_range(datetime.datetime(2003,10,17,12,30,30), periods=1, freq='D') - try: - ephem_data = solarposition.spa_c(times, golden_mst, pressure=82000, - temperature=11).ix[0] - except ImportError: - raise SkipTest - assert_almost_equals(39.872046, ephem_data['elevation'], 6) - assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 6) - assert_almost_equals(194.340241, ephem_data['azimuth'], 6) - assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 6) - - -def test_spa_physical_dst(): - times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), periods=1, - freq='D') +def test_spa_c_physical(): + times = pd.date_range(datetime.datetime(2003,10,17,12,30,30), + periods=1, freq='D', tz=golden_mst.tz) try: - ephem_data = solarposition.spa_c(times, golden, pressure=82000, - temperature=11).ix[0] + ephem_data = solarposition.spa_c(times, golden_mst.latitude, + golden_mst.longitude, + pressure=82000, + temperature=11) except ImportError: - raise SkipTest - assert_almost_equals(39.872046, ephem_data['elevation'], 6) - assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 6) - assert_almost_equals(194.340241, ephem_data['azimuth'], 6) - assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 6) + raise SkipTest + this_expected = expected.copy() + this_expected.index = times + assert_frame_equal(this_expected, ephem_data[expected.columns]) -def test_spa_localization(): +def test_spa_c_physical_dst(): + times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), + periods=1, freq='D', tz=golden.tz) try: - assert_frame_equal(solarposition.spa_c(times, tus), - solarposition.spa_c(times_localized, tus)) + ephem_data = solarposition.spa_c(times, golden.latitude, + golden.longitude, + pressure=82000, + temperature=11) except ImportError: raise SkipTest + this_expected = expected.copy() + this_expected.index = times + assert_frame_equal(this_expected, ephem_data[expected.columns]) def test_spa_python_numpy_physical(): - times = pd.date_range(datetime.datetime(2003,10,17,12,30,30), periods=1, freq='D') - ephem_data = solarposition.spa_python(times, golden_mst, pressure=82000, - temperature=11, delta_t=67, + times = pd.date_range(datetime.datetime(2003,10,17,12,30,30), + periods=1, freq='D', tz=golden_mst.tz) + ephem_data = solarposition.spa_python(times, golden_mst.latitude, + golden_mst.longitude, + pressure=82000, + temperature=11, delta_t=67, atmos_refract=0.5667, - how='numpy').ix[0] - assert_almost_equals(39.872046, ephem_data['elevation'], 6) - assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 6) - assert_almost_equals(194.340241, ephem_data['azimuth'], 6) - assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 6) + how='numpy') + this_expected = expected.copy() + this_expected.index = times + assert_frame_equal(this_expected, ephem_data[expected.columns]) def test_spa_python_numpy_physical_dst(): - times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), periods=1, freq='D') - ephem_data = solarposition.spa_python(times, golden, pressure=82000, - temperature=11, delta_t=67, + times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), + periods=1, freq='D', tz=golden.tz) + ephem_data = solarposition.spa_python(times, golden.latitude, + golden.longitude, + pressure=82000, + temperature=11, delta_t=67, atmos_refract=0.5667, - how='numpy').ix[0] - assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 6) - assert_almost_equals(194.340241, ephem_data['azimuth'], 6) - assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 6) + how='numpy') + this_expected = expected.copy() + this_expected.index = times + assert_frame_equal(this_expected, ephem_data[expected.columns]) def test_spa_python_numba_physical(): @@ -96,16 +106,18 @@ def test_spa_python_numba_physical(): vers = numba.__version__.split('.') if int(vers[0] + vers[1]) < 17: raise SkipTest - - times = pd.date_range(datetime.datetime(2003,10,17,12,30,30), periods=1, freq='D') - ephem_data = solarposition.spa_python(times, golden_mst, pressure=82000, - temperature=11, delta_t=67, + + times = pd.date_range(datetime.datetime(2003,10,17,12,30,30), + periods=1, freq='D', tz=golden_mst.tz) + ephem_data = solarposition.spa_python(times, golden_mst.latitude, + golden_mst.longitude, + pressure=82000, + temperature=11, delta_t=67, atmos_refract=0.5667, - how='numba', numthreads=1).ix[0] - assert_almost_equals(39.872046, ephem_data['elevation'], 6) - assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 6) - assert_almost_equals(194.340241, ephem_data['azimuth'], 6) - assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 6) + how='numba', numthreads=1) + this_expected = expected.copy() + this_expected.index = times + assert_frame_equal(this_expected, ephem_data[expected.columns]) def test_spa_python_numba_physical_dst(): @@ -117,19 +129,16 @@ def test_spa_python_numba_physical_dst(): if int(vers[0] + vers[1]) < 17: raise SkipTest - times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), periods=1, freq='D') - ephem_data = solarposition.spa_python(times, golden, pressure=82000, - temperature=11, delta_t=67, + times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), + periods=1, freq='D', tz=golden.tz) + ephem_data = solarposition.spa_python(times, golden.latitude, + golden.longitude, pressure=82000, + temperature=11, delta_t=67, atmos_refract=0.5667, - how='numba', numthreads=1).ix[0] - assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 6) - assert_almost_equals(194.340241, ephem_data['azimuth'], 6) - assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 6) - - -def test_spa_python_localization(): - assert_frame_equal(solarposition.spa_python(times, tus), - solarposition.spa_python(times_localized, tus)) + how='numba', numthreads=1) + this_expected = expected.copy() + this_expected.index = times + assert_frame_equal(this_expected, ephem_data[expected.columns]) def test_get_sun_rise_set_transit(): @@ -143,7 +152,8 @@ def test_get_sun_rise_set_transit(): sunset = pd.DatetimeIndex([datetime.datetime(1996, 7, 5, 17, 1, 4, 479889), datetime.datetime(2004, 12, 4, 19, 2, 2, 499704)] ).tz_localize('UTC').tolist() - result = solarposition.get_sun_rise_set_transit(times, south, + result = solarposition.get_sun_rise_set_transit(times, south.latitude, + south.longitude, delta_t=64.0) frame = pd.DataFrame({'sunrise':sunrise, 'sunset':sunset}, index=times) del result['transit'] @@ -162,36 +172,38 @@ def test_get_sun_rise_set_transit(): sunset = pd.DatetimeIndex([datetime.datetime(2015, 1, 2, 16, 49, 10, 13145), datetime.datetime(2015, 8, 2, 19, 11, 31, 816401) ]).tz_localize('MST').tolist() - result = solarposition.get_sun_rise_set_transit(times, golden, delta_t=64.0) + result = solarposition.get_sun_rise_set_transit(times, golden.latitude, + golden.longitude, + delta_t=64.0) frame = pd.DataFrame({'sunrise':sunrise, 'sunset':sunset}, index=times) del result['transit'] assert_frame_equal(frame, result) - +@requires_ephem def test_pyephem_physical(): - times = pd.date_range(datetime.datetime(2003,10,17,12,30,30), periods=1, freq='D') - ephem_data = solarposition.pyephem(times, golden_mst, pressure=82000, temperature=11).ix[0] - - assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 2) - assert_almost_equals(194.340241, ephem_data['apparent_azimuth'], 2) - assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 2) - - - + times = pd.date_range(datetime.datetime(2003,10,17,12,30,30), + periods=1, freq='D', tz=golden_mst.tz) + ephem_data = solarposition.pyephem(times, golden_mst.latitude, + golden_mst.longitude, pressure=82000, + temperature=11) + this_expected = expected.copy() + this_expected.index = times + assert_frame_equal(this_expected.round(2), + ephem_data[this_expected.columns].round(2)) + +@requires_ephem def test_pyephem_physical_dst(): - times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), periods=1, freq='D') - ephem_data = solarposition.pyephem(times, golden, pressure=82000, temperature=11).ix[0] - - assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 2) - assert_almost_equals(194.340241, ephem_data['apparent_azimuth'], 2) - assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 2) - - - -def test_pyephem_localization(): - assert_frame_equal(solarposition.pyephem(times, tus), solarposition.pyephem(times_localized, tus)) - - + times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), periods=1, + freq='D', tz=golden.tz) + ephem_data = solarposition.pyephem(times, golden.latitude, + golden.longitude, pressure=82000, + temperature=11) + this_expected = expected.copy() + this_expected.index = times + assert_frame_equal(this_expected.round(2), + ephem_data[this_expected.columns].round(2)) + +@requires_ephem def test_calc_time(): import pytz import math @@ -199,49 +211,130 @@ def test_calc_time(): epoch = datetime.datetime(1970,1,1) epoch_dt = pytz.utc.localize(epoch) - + loc = tus loc.pressure = 0 actual_time = pytz.timezone(loc.tz).localize(datetime.datetime(2014, 10, 10, 8, 30)) - lb = pytz.timezone(loc.tz).localize(datetime.datetime(2014, 10, 10, 6)) + lb = pytz.timezone(loc.tz).localize(datetime.datetime(2014, 10, 10, tol)) ub = pytz.timezone(loc.tz).localize(datetime.datetime(2014, 10, 10, 10)) - alt = solarposition.calc_time(lb, ub, loc, 'alt', math.radians(24.7)) - az = solarposition.calc_time(lb, ub, loc, 'az', math.radians(116.3)) + alt = solarposition.calc_time(lb, ub, loc.latitude, loc.longitude, + 'alt', math.radians(24.7)) + az = solarposition.calc_time(lb, ub, loc.latitude, loc.longitude, + 'az', math.radians(116.3)) actual_timestamp = (actual_time - epoch_dt).total_seconds() - - assert_almost_equals((alt.replace(second=0, microsecond=0) - + + assert_almost_equals((alt.replace(second=0, microsecond=0) - epoch_dt).total_seconds(), actual_timestamp) - assert_almost_equals((az.replace(second=0, microsecond=0) - + assert_almost_equals((az.replace(second=0, microsecond=0) - epoch_dt).total_seconds(), actual_timestamp) - - -def test_earthsun_distance(): - times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), periods=1, freq='D') - assert_almost_equals(1, solarposition.pyephem_earthsun_distance(times).values[0], 0) - -def test_ephemeris_functional(): - solarposition.get_solarposition( - time=times, location=golden_mst, method='ephemeris') +@requires_ephem +def test_earthsun_distance(): + times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), + periods=1, freq='D') + distance = solarposition.pyephem_earthsun_distance(times).values[0] + assert_almost_equals(1, distance, 0) def test_ephemeris_physical(): times = pd.date_range(datetime.datetime(2003,10,17,12,30,30), - periods=1, freq='D') - ephem_data = solarposition.ephemeris(times, golden_mst, pressure=82000, - temperature=11).ix[0] - - assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 2) - assert_almost_equals(194.340241, ephem_data['azimuth'], 2) - assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 2) + periods=1, freq='D', tz=golden_mst.tz) + ephem_data = solarposition.ephemeris(times, golden_mst.latitude, + golden_mst.longitude, + pressure=82000, + temperature=11) + this_expected = expected.copy() + this_expected.index = times + this_expected = np.round(this_expected, 2) + ephem_data = np.round(ephem_data, 2) + assert_frame_equal(this_expected, ephem_data[this_expected.columns]) def test_ephemeris_physical_dst(): times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), - periods=1, freq='D') - ephem_data = solarposition.ephemeris(times, golden, pressure=82000, - temperature=11).ix[0] - - assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 2) - assert_almost_equals(194.340241, ephem_data['azimuth'], 2) - assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 2) + periods=1, freq='D', tz=golden.tz) + ephem_data = solarposition.ephemeris(times, golden.latitude, + golden.longitude, pressure=82000, + temperature=11) + this_expected = expected.copy() + this_expected.index = times + this_expected = np.round(this_expected, 2) + ephem_data = np.round(ephem_data, 2) + assert_frame_equal(this_expected, ephem_data[this_expected.columns]) + +@raises(ValueError) +def test_get_solarposition_error(): + times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), + periods=1, freq='D', tz=golden.tz) + ephem_data = solarposition.get_solarposition(times, golden.latitude, + golden.longitude, + pressure=82000, + temperature=11, + method='error this') + +def test_get_solarposition_pressure(): + times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), + periods=1, freq='D', tz=golden.tz) + ephem_data = solarposition.get_solarposition(times, golden.latitude, + golden.longitude, + pressure=82000, + temperature=11) + this_expected = expected.copy() + this_expected.index = times + this_expected = np.round(this_expected, 5) + ephem_data = np.round(ephem_data, 5) + assert_frame_equal(this_expected, ephem_data[this_expected.columns]) + + ephem_data = solarposition.get_solarposition(times, golden.latitude, + golden.longitude, + pressure=0.0, + temperature=11) + this_expected = expected.copy() + this_expected.index = times + this_expected = np.round(this_expected, 5) + ephem_data = np.round(ephem_data, 5) + try: + assert_frame_equal(this_expected, ephem_data[this_expected.columns]) + except AssertionError: + pass + else: + raise AssertionError + +def test_get_solarposition_altitude(): + times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), + periods=1, freq='D', tz=golden.tz) + ephem_data = solarposition.get_solarposition(times, golden.latitude, + golden.longitude, + altitude=golden.altitude, + temperature=11) + this_expected = expected.copy() + this_expected.index = times + this_expected = np.round(this_expected, 5) + ephem_data = np.round(ephem_data, 5) + assert_frame_equal(this_expected, ephem_data[this_expected.columns]) + + ephem_data = solarposition.get_solarposition(times, golden.latitude, + golden.longitude, + altitude=0.0, + temperature=11) + this_expected = expected.copy() + this_expected.index = times + this_expected = np.round(this_expected, 5) + ephem_data = np.round(ephem_data, 5) + try: + assert_frame_equal(this_expected, ephem_data[this_expected.columns]) + except AssertionError: + pass + else: + raise AssertionError + +def test_get_solarposition_no_kwargs(): + times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), + periods=1, freq='D', tz=golden.tz) + ephem_data = solarposition.get_solarposition(times, golden.latitude, + golden.longitude) + this_expected = expected.copy() + this_expected.index = times + this_expected = np.round(this_expected, 2) + ephem_data = np.round(ephem_data, 2) + assert_frame_equal(this_expected, ephem_data[this_expected.columns]) diff --git a/pvlib/test/test_tmy.py b/pvlib/test/test_tmy.py index de7b2d2e75..fdb1c2771f 100644 --- a/pvlib/test/test_tmy.py +++ b/pvlib/test/test_tmy.py @@ -1,6 +1,3 @@ -import logging -pvl_logger = logging.getLogger('pvlib') - import inspect import os diff --git a/pvlib/test/test_tracking.py b/pvlib/test/test_tracking.py index c6285b9471..488edf65a6 100644 --- a/pvlib/test/test_tracking.py +++ b/pvlib/test/test_tracking.py @@ -4,6 +4,7 @@ import datetime import numpy as np +from numpy import nan import pandas as pd from nose.tools import raises, assert_almost_equals @@ -162,4 +163,106 @@ def test_index_mismatch(): tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, axis_tilt=0, axis_azimuth=90, max_angle=90, backtrack=True, - gcr=2.0/7.0) \ No newline at end of file + gcr=2.0/7.0) + + +def test_SingleAxisTracker_creation(): + system = tracking.SingleAxisTracker(max_angle=45, + gcr=.25, + module='blah', + inverter='blarg') + + assert system.max_angle == 45 + assert system.gcr == .25 + assert system.module == 'blah' + assert system.inverter == 'blarg' + + +def test_SingleAxisTracker_tracking(): + system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, + axis_azimuth=180, gcr=2.0/7.0, + backtrack=True) + + apparent_zenith = pd.Series([30]) + apparent_azimuth = pd.Series([135]) + + tracker_data = system.singleaxis(apparent_zenith, apparent_azimuth) + + expect = pd.DataFrame({'aoi': 7.286245, 'surface_azimuth': 37.3427, + 'surface_tilt': 35.98741, 'tracker_theta': -20.88121}, + index=[0], dtype=np.float64) + + assert_frame_equal(expect, tracker_data) + + +def test_LocalizedSingleAxisTracker_creation(): + localized_system = tracking.LocalizedSingleAxisTracker(latitude=32, + longitude=-111, + module='blah', + inverter='blarg') + + assert localized_system.module == 'blah' + assert localized_system.inverter == 'blarg' + assert localized_system.latitude == 32 + assert localized_system.longitude == -111 + + +def test_SingleAxisTracker_localize(): + system = tracking.SingleAxisTracker(max_angle=45, gcr=.25, + module='blah', inverter='blarg') + + localized_system = system.localize(latitude=32, longitude=-111) + + assert localized_system.module == 'blah' + assert localized_system.inverter == 'blarg' + assert localized_system.latitude == 32 + assert localized_system.longitude == -111 + + +def test_SingleAxisTracker_localize_location(): + system = tracking.SingleAxisTracker(max_angle=45, gcr=.25, + module='blah', inverter='blarg') + location = Location(latitude=32, longitude=-111) + localized_system = system.localize(location=location) + + assert localized_system.module == 'blah' + assert localized_system.inverter == 'blarg' + assert localized_system.latitude == 32 + assert localized_system.longitude == -111 + + +def test_get_irradiance(): + system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, + axis_azimuth=180, gcr=2.0/7.0, + backtrack=True) + times = pd.DatetimeIndex(start='20160101 1200-0700', + end='20160101 1800-0700', freq='6H') + location = Location(latitude=32, longitude=-111) + solar_position = location.get_solarposition(times) + irrads = pd.DataFrame({'dni':[900,0], 'ghi':[600,0], 'dhi':[100,0]}, + index=times) + solar_zenith = solar_position['apparent_zenith'] + solar_azimuth = solar_position['azimuth'] + tracker_data = system.singleaxis(solar_zenith, solar_azimuth) + + irradiance = system.get_irradiance(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']) + + expected = pd.DataFrame(data=np.array( + [[ 142.71652464, 87.50125991, 55.21526473, 44.68768982, + 10.52757492], + [ nan, nan, nan, nan, + nan]]), + columns=['poa_global', 'poa_direct', + 'poa_diffuse', 'poa_sky_diffuse', + 'poa_ground_diffuse'], + index=times) + + irradiance = np.round(irradiance, 4) + expected = np.round(expected, 4) + assert_frame_equal(irradiance, expected) diff --git a/pvlib/tmy.py b/pvlib/tmy.py index 9e5d1678de..55a8ae42fd 100644 --- a/pvlib/tmy.py +++ b/pvlib/tmy.py @@ -292,13 +292,13 @@ def readtmy2(filename): ============= ================================== key description ============= ================================== - SiteID Site identifier code (WBAN number) - StationName Station name - StationState Station state 2 letter designator - SiteTimeZone Hours from Greenwich + WBAN Site identifier code (WBAN number) + City Station name + State Station state 2 letter designator + TZ Hours from Greenwich latitude Latitude in decimal degrees longitude Longitude in decimal degrees - SiteElevation Site elevation in meters + altitude Site elevation in meters ============= ================================== ============================ ========================================================================================================================================================================== diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 0c49f17c2d..0b37224a60 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -7,6 +7,157 @@ import pandas as pd from pvlib.tools import cosd, sind +from pvlib.pvsystem import PVSystem +from pvlib.location import Location +from pvlib import irradiance, atmosphere + +# should this live next to PVSystem? Is this even a good idea? +# possibly should inherit from an abstract base class Tracker +# All children would define their own ``track`` method +class SingleAxisTracker(PVSystem): + """ + Inherits all of the PV modeling methods from PVSystem. + """ + + def __init__(self, axis_tilt=0, axis_azimuth=0, + max_angle=90, backtrack=True, gcr=2.0/7.0, **kwargs): + + self.axis_tilt = axis_tilt + self.axis_azimuth = axis_azimuth + self.max_angle = max_angle + self.backtrack = backtrack + self.gcr = gcr + + super(SingleAxisTracker, self).__init__(**kwargs) + + + def singleaxis(self, apparent_zenith, apparent_azimuth): + tracking_data = singleaxis(apparent_zenith, apparent_azimuth, + self.axis_tilt, self.axis_azimuth, + self.max_angle, + self.backtrack, self.gcr) + + return tracking_data + + + def localize(self, location=None, latitude=None, longitude=None, + **kwargs): + """Creates a :py:class:`LocalizedSingleAxisTracker` + object using this object and location data. + Must supply either location object or + latitude, longitude, and any location kwargs + + Parameters + ---------- + location : None or Location + latitude : None or float + longitude : None or float + **kwargs : see Location + + Returns + ------- + localized_system : LocalizedSingleAxisTracker + """ + + if location is None: + location = Location(latitude, longitude, **kwargs) + + return LocalizedSingleAxisTracker(pvsystem=self, location=location) + + + def get_irradiance(self, 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``. + + Parameters + ---------- + solar_zenith : float or Series. + Solar zenith angle. + solar_azimuth : float or Series. + Solar azimuth angle. + dni : float or Series + Direct Normal Irradiance + ghi : float or Series + Global horizontal irradiance + dhi : float or Series + Diffuse horizontal irradiance + dni_extra : float or Series + Extraterrestrial direct normal irradiance + airmass : float or Series + Airmass + model : String + Irradiance model. + + **kwargs + Passed to :func:`irradiance.total_irrad`. + + Returns + ------- + poa_irradiance : DataFrame + 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) + + return irradiance.total_irrad(surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + dni, ghi, dhi, + dni_extra=dni_extra, airmass=airmass, + model=model, + albedo=self.albedo, + **kwargs) + + +class LocalizedSingleAxisTracker(SingleAxisTracker, Location): + """Highly experimental.""" + + def __init__(self, pvsystem=None, location=None, **kwargs): + + # get and combine attributes from the pvsystem and/or location + # with the rest of the kwargs + + if pvsystem is not None: + pv_dict = pvsystem.__dict__ + else: + pv_dict = {} + + if location is not None: + loc_dict = location.__dict__ + else: + loc_dict = {} + + new_kwargs = dict(list(pv_dict.items()) + + list(loc_dict.items()) + + list(kwargs.items())) + + super(LocalizedSingleAxisTracker, self).__init__(**new_kwargs) def singleaxis(apparent_zenith, apparent_azimuth, @@ -354,4 +505,4 @@ def singleaxis(apparent_zenith, apparent_azimuth, df_out[apparent_zenith > 90] = np.nan - return df_out \ No newline at end of file + return df_out diff --git a/pvlib/version.py b/pvlib/version.py index b5fdc75308..c7d7df65b8 100644 --- a/pvlib/version.py +++ b/pvlib/version.py @@ -1 +1 @@ -__version__ = "0.2.2" +__version__ = "0.3.0dev"