From c33d65d2ca1534359132db36a625546abf1a1611 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Wed, 28 Oct 2015 09:07:28 -0700 Subject: [PATCH 001/100] change location default tz to utc --- pvlib/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/location.py b/pvlib/location.py index 9157766d20..0953af9a6c 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -43,7 +43,7 @@ class Location(object): Sets the name attribute of the Location object. """ - def __init__(self, latitude, longitude, tz='US/Mountain', altitude=100, + def __init__(self, latitude, longitude, tz='UTC', altitude=100, name=None): pvl_logger.debug('creating Location object') From d965f26cd91fe4c42d5a9629175b29e6810ca4de Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Wed, 28 Oct 2015 10:08:02 -0700 Subject: [PATCH 002/100] a mess of pvsystem and location stuff thats not working --- pvlib/location.py | 30 ++++++++++-- pvlib/pvsystem.py | 108 +++++++++++++++++++++++++++++++++++++++++ pvlib/solarposition.py | 30 ++++++++---- 3 files changed, 155 insertions(+), 13 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 0953af9a6c..b71370ae6a 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -11,6 +11,8 @@ import pytz +from pvlib import solarposition + class Location(object): """ Location objects are convenient containers for latitude, longitude, @@ -19,8 +21,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. @@ -41,9 +43,13 @@ class Location(object): Altitude from sea level in meters. name : None or string. Sets the name attribute of the Location object. + + See also + -------- + pvsystem.PVSystem """ - def __init__(self, latitude, longitude, tz='UTC', altitude=100, + def __init__(self, latitude, longitude, tz='UTC', altitude=0, name=None): pvl_logger.debug('creating Location object') @@ -69,4 +75,20 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=100, 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): + """ + Create an object based on a metadata + dictionary from tmy2 or tmy3 data readers. + """ + return cls(**tmy_metadata) + + + def get_solarposition(self, times, **kwargs): + return solarposition.get_solarposition(times, latitude=self.latitude, + longitude=self.longitude, + **kwargs) + \ No newline at end of file diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 66bff02a1f..b4b2688fa5 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -18,6 +18,114 @@ import pandas as pd from pvlib import tools +from pvlib.location import Location + + +def PVSystem(Location): + """ + PVSystem objects are convenient containers for latitude, longitude, + timezone, and altitude data associated with a particular + geographic location. You can also assign a name to a location object. + + PVSystem objects have two timezone attributes: + + * ``tz`` is a IANA timezone string. + * ``pytz`` is a pytz timezone object. + + PVSystem objects support the print method. + + Parameters + ---------- + 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. + 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. + alitude : float. + Altitude from sea level in meters. + name : None or string. + Sets the name attribute of the PVSystem object. + kwargs : dict + Any other data that you want to attach as an attribute. + + See also + -------- + location.Location + """ + + def __init__(self, latitude, longitude, tz='UTC', altitude=0, + name=None, **kwargs): + + super(PVSystem, self).__init__(latitude, longitude, tz=tz, + altitude=altitude, name=name) + + [setattr(self, k, v) for k, v in kwargs.items()] + + + def __str__(self): + return ('{}: latitude={}, longitude={}, tz={}, altitude={}' + .format(self.name, self.latitude, self.longitude, + self.tz, self.altitude)) + + + # defaults to kwargs, falls back to attributes. complicated. + # harder to support? + def ashraeiam(self, **kwargs): + + return ashraeiam(kwargs.pop('b', self.b), kwargs.pop('aoi', self.aoi)) + + + # thin wrappers of other pvsystem functions + def physicaliam(self, K, L, n, aoi): + + return physicaliam(K, L, n, aoi) + + + def calcparams_desoto(self, poa_global, temp_cell, alpha_isc, + module_parameters, + EgRef, dEgdT, M=1, irrad_ref=1000, temp_ref=25): + + return calcparams_desoto(poa_global, temp_cell, alpha_isc, + module_parameters, + EgRef, dEgdT, M, irrad_ref, temp_ref): + + + def sapm(self, module, poa_direct, poa_diffuse, + temp_cell, airmass_absolute, aoi): + + return sapm(module, poa_direct, poa_diffuse, + temp_cell, airmass_absolute, aoi) + + + def sapm_celltemp(self, irrad, wind, temp, + model='open_rack_cell_glassback'): + + return sapm_celltemp(irrad, wind, temp, model) + + + def singlediode(self, module, photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth): + + return singlediode(module, photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth) + + + def i_from_v(self, resistance_shunt, resistance_series, nNsVth, voltage, + saturation_current, photocurrent): + + return i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, + saturation_current, photocurrent) + + + def snlinverter(self, inverter, v_dc, p_dc): + + return snlinverter(inverter, v_dc, p_dc) def systemdef(meta, surface_tilt, surface_azimuth, albedo, series_modules, diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 1420cf92e8..fffaee7345 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -20,6 +20,7 @@ except ImportError: pass +import warnings import numpy as np import pandas as pd @@ -28,7 +29,8 @@ 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, location=None, latitude=None, longitude=None, + method='nrel_numpy', pressure=101325, temperature=12, **kwargs): """ A convenience wrapper for the solar position calculators. @@ -36,7 +38,9 @@ def get_solarposition(time, location, method='nrel_numpy', pressure=101325, Parameters ---------- time : pandas.DatetimeIndex - location : pvlib.Location object + location : None or pvlib.Location object + latitude : None or float + longitude : None or float method : string 'pyephem' uses the PyEphem package: :func:`pyephem` @@ -66,30 +70,38 @@ def get_solarposition(time, location, method='nrel_numpy', pressure=101325, [3] NREL SPA code: http://rredc.nrel.gov/solar/codesandalgorithms/spa/ """ - + + if location is not None: + warnings.warn("The location argument is deprecated. Use " + + "Location.get_solarposition or specify the " + + "latitude and longitude arguments", DeprecationWarning) + 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, pressure, temperature, how='numba', **kwargs) elif method == 'nrel_numpy': - ephem_df = spa_python(time, location, pressure, temperature, + ephem_df = spa_python(time, latitude, longitude, 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, temperature=12, delta_t=67.0, raw_spa_output=False): """ Calculate the solar position using the C implementation of the NREL From e1ad1893e78211ecba42c3895310e9b28ed24fa1 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Wed, 28 Oct 2015 11:30:11 -0700 Subject: [PATCH 003/100] add SingleAxisTracker. more brainstorming --- pvlib/location.py | 44 ++++++++++++++++-- pvlib/pvsystem.py | 111 +++++++++++++++++++++++++++++++++++----------- pvlib/tracking.py | 37 ++++++++++++++++ 3 files changed, 161 insertions(+), 31 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index b71370ae6a..02b2ed9b90 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -79,14 +79,50 @@ def __str__(self): @classmethod - def from_tmy(cls, tmy_metadata): + 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). """ - return cls(**tmy_metadata) - - + # 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('StationName', False) + + latitude = tmy_metadata['latitude'] + longitude = tmy_metadata['longitude'] + + if tmy2: + altitude = tmy_metadata['SiteElevation'] + name = tmy_metadata['StationName'] + tz = tmy_metadata['SiteTimeZone'] + else: + altitude = tmy_metadata['alititude'] + name = tmy_metadata['Name'] + tz = tmy_metadata['TZ'] + + new_object = cls(latitude, longitude, tz, altitude, 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, **kwargs): return solarposition.get_solarposition(times, latitude=self.latitude, longitude=self.longitude, diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index b4b2688fa5..cf540aea6e 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -21,68 +21,123 @@ from pvlib.location import Location -def PVSystem(Location): +# 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(Location): """ - PVSystem objects are convenient containers for latitude, longitude, - timezone, and altitude data associated with a particular - geographic location. You can also assign a name to a location object. + The PVSystem class defines a standard set of system attributes and + wraps the module-level modeling functions. The class is complementary + to the module-level functions. Some modeling applications benefit from + the structure imposed by classes. - PVSystem objects have two timezone attributes: + 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. - * ``tz`` is a IANA timezone string. - * ``pytz`` is a pytz timezone object. - - PVSystem objects support the print method. + Inherits the ``from_tmy`` constructor from Location. + + Inherits the ``get_solarposition`` method from Location. Parameters ---------- 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. 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. + alitude : float. Altitude from sea level in meters. + name : None or string. Sets the name attribute of the PVSystem object. - kwargs : dict - Any other data that you want to attach as an attribute. + + surface_tilt: None, float, or array-like + Tilt angle of the module surface. + Up=0, horizon=90. + + surface_azimuth: None, float, or array-like + Azimuth angle of the module surface. + North=0, East=90, South=180, West=270. + + 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 : anything + Any other data that you want to assign as an attribute + that may be used by the PVSystem instance methods. See also -------- location.Location + tracking.SingleAxisTracker """ def __init__(self, latitude, longitude, tz='UTC', altitude=0, - name=None, **kwargs): + name=None, module=None, module_parameters=None, + inverter=None, inverter_parameters=None, + racking_model=None, **kwargs): super(PVSystem, self).__init__(latitude, longitude, tz=tz, altitude=altitude, name=name) + + self.surface_tilt = surface_tilt + self.surface_azimuth = surface_azimuth + + # could tie these together with @property + self.module = module + self.module_parameters = module_parameters + + self.inverter = inverter + self.inverter_parameters = inverter_parameters + + self.racking_model = racking_model + # makes self.some_parameter = some_value for everything in kwargs [setattr(self, k, v) for k, v in kwargs.items()] - - - def __str__(self): - return ('{}: latitude={}, longitude={}, tz={}, altitude={}' - .format(self.name, self.latitude, self.longitude, - self.tz, self.altitude)) # defaults to kwargs, falls back to attributes. complicated. # harder to support? def ashraeiam(self, **kwargs): - + """Wrapper around the ashraeiam function. + + Parameters + ---------- + kwargs : None, b, a + See pvsystem.ashraeiam for details + """ return ashraeiam(kwargs.pop('b', self.b), kwargs.pop('aoi', self.aoi)) # thin wrappers of other pvsystem functions - def physicaliam(self, K, L, n, aoi): + def physicaliam(self, aoi): return physicaliam(K, L, n, aoi) @@ -103,16 +158,17 @@ def sapm(self, module, poa_direct, poa_diffuse, temp_cell, airmass_absolute, aoi) - def sapm_celltemp(self, irrad, wind, temp, - model='open_rack_cell_glassback'): + # model now specified by self.racking_model + def sapm_celltemp(self, irrad, wind, temp): - return sapm_celltemp(irrad, wind, temp, model) + return sapm_celltemp(irrad, wind, temp, self.racking_model) - def singlediode(self, module, photocurrent, saturation_current, + def singlediode(self, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): - return singlediode(module, photocurrent, saturation_current, + return singlediode(self.module_parameters, photocurrent, + saturation_current, resistance_series, resistance_shunt, nNsVth) @@ -123,9 +179,10 @@ def i_from_v(self, resistance_shunt, resistance_series, nNsVth, voltage, saturation_current, photocurrent) - def snlinverter(self, inverter, v_dc, p_dc): + # inverter now specified by self.inverter_parameters + def snlinverter(self, v_dc, p_dc): - return snlinverter(inverter, v_dc, p_dc) + return snlinverter(self.inverter_parameters, v_dc, p_dc) def systemdef(meta, surface_tilt, surface_azimuth, albedo, series_modules, diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 0c49f17c2d..4be3f45875 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -9,6 +9,43 @@ from pvlib.tools import cosd, sind +# 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 the ``from_tmy`` constructor from Location. + + Inherits the ``get_solarposition`` method from Location. + + Inherits all of the PV modeling methods from PVSystem. + """ + # should make better use of kwargs in the inheritance + def __init__(self, latitude, longitude, tz='UTC', altitude=0, + name=None, module=None, module_parameters=None, + inverter=None, inverter_parameters=None, + racking_model=None, axis_tilt=0, axis_azimuth=0, + max_angle=90, backtrack=True, gcr=2.0/7.0, **kwargs): + + super(SingleAxisTracker, self).__init__( + latitude, longitude, tz=tz, altitude=altitude, name=name, + module=module, module_parameters=module_parameters, + inverter=inverter, inverter_parameters=inverter_parameters, + racking_model=None) + + self.axis_tilt = axis_tilt + self.axis_azimuth = axis_azimuth + self.max_angle = max_angle + self.backtrack = backtrack + self.gcr = gcr + + + def singleaxis(self, apparent_zenith, apparent_azimuth): + return singleaxis(apparent_azimuth, apparent_zenith, + self.axis_tilt, self.axis_azimuth, self.max_angle, + self.backtrack, self.gcr) + + def singleaxis(apparent_zenith, apparent_azimuth, axis_tilt=0, axis_azimuth=0, max_angle=90, backtrack=True, gcr=2.0/7.0): From b4cc4b798f7f12a7a191d185e71f2014d00280e8 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 9 Nov 2015 17:43:00 -0700 Subject: [PATCH 004/100] fix import error --- pvlib/tracking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 4be3f45875..865b8ab62d 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -7,7 +7,7 @@ import pandas as pd from pvlib.tools import cosd, sind - +from pvlib.pvsystem import PVSystem # should this live next to PVSystem? Is this even a good idea? # possibly should inherit from an abstract base class Tracker From e92acddbd9bc01a9369fe388f254f885ba1837e2 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 9 Nov 2015 17:43:29 -0700 Subject: [PATCH 005/100] add get_solarposition and get_clearsky --- pvlib/location.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/pvlib/location.py b/pvlib/location.py index 02b2ed9b90..411e618674 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -12,6 +12,8 @@ import pytz from pvlib import solarposition +from pvlib import clearsky + class Location(object): """ @@ -124,7 +126,45 @@ def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): def get_solarposition(self, times, **kwargs): + """ + Uses the :func:`solarposition.get_solarposition` function + to calculate the solar zenith, azimuth, etc. at this location. + + Parameters + ---------- + times : DatetimeIndex + + kwargs passed to :func:`solarposition.get_solarposition` + + Returns + ------- + solarposition : DataFrame + Columns depend on the ``method`` kwarg, but always include + ``zenith`` and ``azimuth``. + """ return solarposition.get_solarposition(times, latitude=self.latitude, longitude=self.longitude, **kwargs) - \ No newline at end of file + + + def get_clearsky(self, times, **kwargs): + """ + Uses the :func:`clearsky.ineichen` function to calculate + the clear sky estimates of GHI, DNI, and DHI at this location. + + Parameters + ---------- + times : DatetimeIndex + + kwargs passed to :func:`clearsky.ineichen` + + Returns + ------- + clearsky : DataFrame + Column names are: ``ghi, dni, dhi``. + """ + return clearsky.ineichen(times, latitude=self.latitude, + longitude=self.longitude, + **kwargs) + + \ No newline at end of file From 27fd18e8eef5e762a1c0c0a6c1e041b383397a38 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 9 Nov 2015 17:44:21 -0700 Subject: [PATCH 006/100] add get_irradiance, simple doc strings, and a few kwargs --- pvlib/pvsystem.py | 161 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 148 insertions(+), 13 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index cf540aea6e..ebee50a25a 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -19,6 +19,7 @@ from pvlib import tools from pvlib.location import Location +from pvlib import irradiance # not sure if this belongs in the pvsystem module. @@ -62,14 +63,23 @@ class PVSystem(Location): name : None or string. Sets the name attribute of the PVSystem object. - surface_tilt: None, float, or array-like + surface_tilt: float or array-like Tilt angle of the module surface. Up=0, horizon=90. - surface_azimuth: None, float, or array-like + 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 @@ -100,9 +110,13 @@ class PVSystem(Location): """ def __init__(self, latitude, longitude, tz='UTC', altitude=0, - name=None, module=None, module_parameters=None, + name=None, + surface_tilt=0, surface_azimuth=180, + albedo=None, surface_type=None, + module=None, module_parameters=None, inverter=None, inverter_parameters=None, - racking_model=None, **kwargs): + racking_model=None, + **kwargs): super(PVSystem, self).__init__(latitude, longitude, tz=tz, altitude=altitude, name=name) @@ -110,6 +124,13 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0, 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 @@ -121,8 +142,55 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0, # makes self.some_parameter = some_value for everything in kwargs [setattr(self, k, v) for k, v in kwargs.items()] - - + + + def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, + dni_extra=None, airmass=None, model='isotropic', + **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``. + """ + + 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) + + # defaults to kwargs, falls back to attributes. complicated. # harder to support? def ashraeiam(self, **kwargs): @@ -132,41 +200,90 @@ def ashraeiam(self, **kwargs): ---------- kwargs : None, b, a See pvsystem.ashraeiam for details + + Returns + ------- + See pvsystem.ashraeiam for details """ return ashraeiam(kwargs.pop('b', self.b), kwargs.pop('aoi', self.aoi)) # thin wrappers of other pvsystem functions def physicaliam(self, aoi): - + """Wrapper around the physicaliam function. + + Parameters + ---------- + See pvsystem.physicaliam for details + + Returns + ------- + See pvsystem.physicaliam for details + """ return physicaliam(K, L, n, aoi) def calcparams_desoto(self, poa_global, temp_cell, alpha_isc, module_parameters, EgRef, dEgdT, M=1, irrad_ref=1000, temp_ref=25): - + """Wrapper around the calcparams_desoto function. + + Parameters + ---------- + See pvsystem.calcparams_desoto for details + + Returns + ------- + See pvsystem.calcparams_desoto for details + """ return calcparams_desoto(poa_global, temp_cell, alpha_isc, module_parameters, - EgRef, dEgdT, M, irrad_ref, temp_ref): + EgRef, dEgdT, M, irrad_ref, temp_ref) def sapm(self, module, poa_direct, poa_diffuse, temp_cell, airmass_absolute, aoi): + """Wrapper around the sapm function. + + Parameters + ---------- + See pvsystem.sapm for details + Returns + ------- + See pvsystem.sapm for details + """ return sapm(module, poa_direct, poa_diffuse, temp_cell, airmass_absolute, aoi) # model now specified by self.racking_model def sapm_celltemp(self, irrad, wind, temp): - + """Wrapper around the sapm_celltemp function. + + 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 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) @@ -174,14 +291,32 @@ def singlediode(self, photocurrent, saturation_current, def i_from_v(self, resistance_shunt, resistance_series, nNsVth, voltage, saturation_current, photocurrent): - + """Wrapper around the 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): - + """Wrapper around the snlinverter function. + + Parameters + ---------- + See pvsystem.snlinverter for details + + Returns + ------- + See pvsystem.snlinverter for details + """ return snlinverter(self.inverter_parameters, v_dc, p_dc) From 82f28ba8c80e112a589887481b22035610297c52 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 9 Nov 2015 17:45:14 -0700 Subject: [PATCH 007/100] make solarposition.spa_python work without Location --- pvlib/clearsky.py | 10 +++++++--- pvlib/solarposition.py | 22 +++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index 7980194d45..bd502ca978 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -20,7 +20,7 @@ -def ineichen(time, location, linke_turbidity=None, +def ineichen(time, latitude, longitude, linke_turbidity=None, solarposition_method='pyephem', zenith_data=None, airmass_model='young1994', airmass_data=None, interp_turbidity=True): @@ -40,7 +40,9 @@ def ineichen(time, location, linke_turbidity=None, ----------- time : pandas.DatetimeIndex - location : pvlib.Location + latitude : float + + longitude : float linke_turbidity : None or float If None, uses ``LinkeTurbidities.mat`` lookup table. @@ -101,7 +103,9 @@ 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, method=solarposition_method) time = ephem_data.index # fixes issue with time possibly not being tz-aware try: diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index fffaee7345..dbaaa7e4f4 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -101,7 +101,8 @@ def get_solarposition(time, location=None, latitude=None, longitude=None, return ephem_df -def spa_c(time, latitude, longitude, pressure=101325, temperature=12, delta_t=67.0, +def spa_c(time, latitude, longitude, pressure=101325, + temperature=12, delta_t=67.0, raw_spa_output=False): """ Calculate the solar position using the C implementation of the NREL @@ -223,7 +224,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 @@ -237,7 +239,9 @@ def spa_python(time, location, pressure=101325, temperature=12, delta_t=None, Parameters ---------- time : pandas.DatetimeIndex - location : pvlib.Location object + latitude : float + longitude : float + altitude : float pressure : int or float, optional avg. yearly air pressure in Pascals. temperature : int or float, optional @@ -286,9 +290,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 @@ -299,7 +303,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) @@ -314,9 +318,9 @@ def spa_python(time, location, pressure=101325, temperature=12, delta_t=None, index=time) try: - result = result.tz_convert(location.tz) + result = result.tz_convert(time.tz) except TypeError: - result = result.tz_localize(location.tz) + result = result.tz_localize(time.tz) return result From cb22f24d4674794a487a9306990adcd3fb6bee02 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 9 Nov 2015 17:52:14 -0700 Subject: [PATCH 008/100] add doc links and n_modules kwargs --- pvlib/pvsystem.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index ebee50a25a..fbae4c0705 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -114,6 +114,7 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0, 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=None, **kwargs): @@ -135,6 +136,9 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0, 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 @@ -194,7 +198,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, # defaults to kwargs, falls back to attributes. complicated. # harder to support? def ashraeiam(self, **kwargs): - """Wrapper around the ashraeiam function. + """Wrapper around the :func:`ashraeiam` function. Parameters ---------- @@ -210,7 +214,7 @@ def ashraeiam(self, **kwargs): # thin wrappers of other pvsystem functions def physicaliam(self, aoi): - """Wrapper around the physicaliam function. + """Wrapper around the :func:`physicaliam` function. Parameters ---------- @@ -226,7 +230,7 @@ def physicaliam(self, aoi): def calcparams_desoto(self, poa_global, temp_cell, alpha_isc, module_parameters, EgRef, dEgdT, M=1, irrad_ref=1000, temp_ref=25): - """Wrapper around the calcparams_desoto function. + """Wrapper around the :func:`calcparams_desoto` function. Parameters ---------- @@ -243,7 +247,7 @@ def calcparams_desoto(self, poa_global, temp_cell, alpha_isc, def sapm(self, module, poa_direct, poa_diffuse, temp_cell, airmass_absolute, aoi): - """Wrapper around the sapm function. + """Wrapper around the :func:`sapm` function. Parameters ---------- @@ -259,7 +263,7 @@ def sapm(self, module, poa_direct, poa_diffuse, # model now specified by self.racking_model def sapm_celltemp(self, irrad, wind, temp): - """Wrapper around the sapm_celltemp function. + """Wrapper around the :func:`sapm_celltemp` function. Parameters ---------- @@ -274,7 +278,7 @@ def sapm_celltemp(self, irrad, wind, temp): def singlediode(self, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): - """Wrapper around the singlediode function. + """Wrapper around the :func:`singlediode` function. Parameters ---------- @@ -291,7 +295,7 @@ def singlediode(self, photocurrent, saturation_current, def i_from_v(self, resistance_shunt, resistance_series, nNsVth, voltage, saturation_current, photocurrent): - """Wrapper around the i_from_v function. + """Wrapper around the :func:`i_from_v` function. Parameters ---------- @@ -307,7 +311,7 @@ def i_from_v(self, resistance_shunt, resistance_series, nNsVth, voltage, # inverter now specified by self.inverter_parameters def snlinverter(self, v_dc, p_dc): - """Wrapper around the snlinverter function. + """Wrapper around the :func:`snlinverter` function. Parameters ---------- From ca0c6badb069e7681a904b160a3057f0389be36e Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 9 Nov 2015 18:00:02 -0700 Subject: [PATCH 009/100] add altitude --- pvlib/location.py | 2 ++ pvlib/pvsystem.py | 5 +++-- pvlib/solarposition.py | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 411e618674..4e044ba99f 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -144,6 +144,7 @@ def get_solarposition(self, times, **kwargs): """ return solarposition.get_solarposition(times, latitude=self.latitude, longitude=self.longitude, + altitude=self.altitude, **kwargs) @@ -165,6 +166,7 @@ def get_clearsky(self, times, **kwargs): """ return clearsky.ineichen(times, latitude=self.latitude, longitude=self.longitude, + altitude=self.altitude, **kwargs) \ No newline at end of file diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index fbae4c0705..4f71a9771d 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -37,9 +37,10 @@ class PVSystem(Location): methods accept arguments for things that do change, such as irradiance and temperature. - Inherits the ``from_tmy`` constructor from Location. + Inherits the ``from_tmy`` constructor from ``Location``. - Inherits the ``get_solarposition`` method from Location. + Inherits the ``get_solarposition`` and ``get_clearsky`` + method from ``Location``. Defines ``get_irradiance``. Parameters ---------- diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index dbaaa7e4f4..e8b25c32ef 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -30,6 +30,7 @@ def get_solarposition(time, location=None, latitude=None, longitude=None, + altitude=None, method='nrel_numpy', pressure=101325, temperature=12, **kwargs): """ @@ -41,6 +42,7 @@ def get_solarposition(time, location=None, latitude=None, longitude=None, location : None or pvlib.Location object latitude : None or float longitude : None or float + altitude : None or float method : string 'pyephem' uses the PyEphem package: :func:`pyephem` @@ -87,7 +89,8 @@ def get_solarposition(time, location=None, latitude=None, longitude=None, ephem_df = spa_python(time, latitude, longitude, pressure, temperature, how='numba', **kwargs) elif method == 'nrel_numpy': - ephem_df = spa_python(time, latitude, longitude, pressure, temperature, + ephem_df = spa_python(time, latitude, longitude, altitude, + pressure, temperature, how='numpy', **kwargs) elif method == 'pyephem': ephem_df = pyephem(time, latitude, longitude, pressure, temperature, From 2524995c6d5b175c6ea5261c5c969dd0b7578ba4 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 9 Nov 2015 18:38:20 -0700 Subject: [PATCH 010/100] get_clearsky works --- pvlib/clearsky.py | 20 +++++++++++--------- pvlib/solarposition.py | 3 ++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index bd502ca978..f43fe81338 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -20,7 +20,7 @@ -def ineichen(time, latitude, longitude, linke_turbidity=None, +def ineichen(time, latitude, longitude, altitude, linke_turbidity=None, solarposition_method='pyephem', zenith_data=None, airmass_model='young1994', airmass_data=None, interp_turbidity=True): @@ -43,6 +43,8 @@ def ineichen(time, latitude, longitude, linke_turbidity=None, latitude : float longitude : float + + altitude : float linke_turbidity : None or float If None, uses ``LinkeTurbidities.mat`` lookup table. @@ -105,7 +107,8 @@ def ineichen(time, latitude, longitude, linke_turbidity=None, if zenith_data is None: ephem_data = solarposition.get_solarposition(time, latitude=latitude, - longitude=longitude, + longitude=longitude, + altitude=altitude, method=solarposition_method) time = ephem_data.index # fixes issue with time possibly not being tz-aware try: @@ -119,8 +122,7 @@ def ineichen(time, latitude, longitude, 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 @@ -130,14 +132,14 @@ def ineichen(time, latitude, longitude, 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/solarposition.py b/pvlib/solarposition.py index e8b25c32ef..403db9d26a 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -86,7 +86,8 @@ def get_solarposition(time, location=None, latitude=None, longitude=None, ephem_df = spa_c(time, latitude, longitude, pressure, temperature, **kwargs) elif method == 'nrel_numba': - ephem_df = spa_python(time, latitude, longitude, pressure, temperature, + ephem_df = spa_python(time, latitude, longitude, altitude, + pressure, temperature, how='numba', **kwargs) elif method == 'nrel_numpy': ephem_df = spa_python(time, latitude, longitude, altitude, From c5d55d360d2cd574732be05597828557c40b3542 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 9 Nov 2015 18:51:46 -0700 Subject: [PATCH 011/100] add v0.3.0 description --- docs/sphinx/source/whatsnew.rst | 1 + docs/sphinx/source/whatsnew/v0.3.0.txt | 27 ++++++++++++++++++++++++++ pvlib/version.py | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 docs/sphinx/source/whatsnew/v0.3.0.txt 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..5c19fb2532 --- /dev/null +++ b/docs/sphinx/source/whatsnew/v0.3.0.txt @@ -0,0 +1,27 @@ +.. _whatsnew_0300: + +v0.3.0 (2016) +----------------------- + +This is a major release from 0.2.2. +We recommend that all users upgrade to this version after testing +their code for compatibility and updating as necessary. + +Enhancements +~~~~~~~~~~~~ + +* Adds ``PVSystem`` and ``SingleAxisTracker`` classes. (:issue:`17`) +* Replaces ``location`` arguments with ``latitude``, ``longitude``, + ``altitude``, and ``tz`` as appropriate. + This separates the object-oriented API from the procedural API. + (:issue:`17`) + + +Bug fixes +~~~~~~~~~ + + +Contributors +~~~~~~~~~~~~ + +* Will Holmgren diff --git a/pvlib/version.py b/pvlib/version.py index 2b3414c7b4..c7d7df65b8 100644 --- a/pvlib/version.py +++ b/pvlib/version.py @@ -1 +1 @@ -__version__ = "0.2.2dev" +__version__ = "0.3.0dev" From 697e8d2ae1d04b2a47c86657643a4c9478096a2c Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Thu, 12 Nov 2015 18:46:22 -0700 Subject: [PATCH 012/100] remove Location inheritance from PVSystem --- pvlib/pvsystem.py | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 4f71a9771d..11732d057e 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -25,7 +25,7 @@ # 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(Location): +class PVSystem(object): """ The PVSystem class defines a standard set of system attributes and wraps the module-level modeling functions. The class is complementary @@ -37,33 +37,8 @@ class PVSystem(Location): methods accept arguments for things that do change, such as irradiance and temperature. - Inherits the ``from_tmy`` constructor from ``Location``. - - Inherits the ``get_solarposition`` and ``get_clearsky`` - method from ``Location``. Defines ``get_irradiance``. - Parameters ---------- - 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. - 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. - - alitude : float. - Altitude from sea level in meters. - - name : None or string. - Sets the name attribute of the PVSystem object. - surface_tilt: float or array-like Tilt angle of the module surface. Up=0, horizon=90. @@ -110,8 +85,7 @@ class PVSystem(Location): tracking.SingleAxisTracker """ - def __init__(self, latitude, longitude, tz='UTC', altitude=0, - name=None, + def __init__(self, surface_tilt=0, surface_azimuth=180, albedo=None, surface_type=None, module=None, module_parameters=None, @@ -119,9 +93,6 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0, inverter=None, inverter_parameters=None, racking_model=None, **kwargs): - - super(PVSystem, self).__init__(latitude, longitude, tz=tz, - altitude=altitude, name=name) self.surface_tilt = surface_tilt self.surface_azimuth = surface_azimuth From ff0fb3bca1fefd543c322bfdfaef39e1d0a90c9a Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Thu, 12 Nov 2015 18:48:58 -0700 Subject: [PATCH 013/100] remove Location inheritance from SAT --- pvlib/tracking.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 865b8ab62d..433fef9be8 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -20,24 +20,17 @@ class SingleAxisTracker(PVSystem): Inherits all of the PV modeling methods from PVSystem. """ - # should make better use of kwargs in the inheritance - def __init__(self, latitude, longitude, tz='UTC', altitude=0, - name=None, module=None, module_parameters=None, - inverter=None, inverter_parameters=None, - racking_model=None, axis_tilt=0, axis_azimuth=0, + + def __init__(self, axis_tilt=0, axis_azimuth=0, max_angle=90, backtrack=True, gcr=2.0/7.0, **kwargs): - super(SingleAxisTracker, self).__init__( - latitude, longitude, tz=tz, altitude=altitude, name=name, - module=module, module_parameters=module_parameters, - inverter=inverter, inverter_parameters=inverter_parameters, - racking_model=None) - 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): @@ -391,4 +384,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 From e2a22cf2b7c4956f22a27f29468ddfa043fdc9fc Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Thu, 12 Nov 2015 18:49:31 -0700 Subject: [PATCH 014/100] remove Location inheritance from SAT --- pvlib/tracking.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 433fef9be8..f07e363c3a 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -14,10 +14,6 @@ # All children would define their own ``track`` method class SingleAxisTracker(PVSystem): """ - Inherits the ``from_tmy`` constructor from Location. - - Inherits the ``get_solarposition`` method from Location. - Inherits all of the PV modeling methods from PVSystem. """ From b9d41c81aae3fd76caa211a703aad9d1c848cb0f Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Wed, 11 Nov 2015 10:23:13 -0700 Subject: [PATCH 015/100] fix klucher spelling --- docs/sphinx/source/whatsnew/v0.2.2.txt | 5 +++++ pvlib/irradiance.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.2.2.txt b/docs/sphinx/source/whatsnew/v0.2.2.txt index 28a9e524ac..390129d86d 100644 --- a/docs/sphinx/source/whatsnew/v0.2.2.txt +++ b/docs/sphinx/source/whatsnew/v0.2.2.txt @@ -17,6 +17,11 @@ Enhancements Bug fixes ~~~~~~~~~ +* ``irradiance.total_irrad`` had a typo that required the + Klucher model to be accessed with ``'klutcher'``. Both spellings will work + for the remaining 0.2.* versions of pvlib, + but the misspelled method will be removed in 0.3. + (:issue:`97`) * Fixes an import and KeyError in the IPython notebook tutorials (:issue:`94`). * Uses the ``logging`` module properly by replacing ``format`` diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 4c03e16ba5..0d0e7d5ad4 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -376,7 +376,7 @@ def total_irrad(surface_tilt, surface_azimuth, model = model.lower() if model == 'isotropic': sky = isotropic(surface_tilt, dhi) - elif model == 'klutcher': + elif model in ['klucher', 'klutcher']: sky = klucher(surface_tilt, surface_azimuth, dhi, ghi, solar_zenith, solar_azimuth) elif model == 'haydavies': From b01dc5a257e6b3abbc3de117173d6cd0afe677a6 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Wed, 11 Nov 2015 11:06:15 -0700 Subject: [PATCH 016/100] add klucher to total_irrad test list --- pvlib/test/test_irradiance.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pvlib/test/test_irradiance.py b/pvlib/test/test_irradiance.py index 47d86a4048..44911253c1 100644 --- a/pvlib/test/test_irradiance.py +++ b/pvlib/test/test_irradiance.py @@ -137,16 +137,18 @@ def test_perez(): dni_et, ephem_data['apparent_zenith'], ephem_data['apparent_azimuth'], AM) - +# klutcher (misspelling) will be removed in 0.3 def test_total_irrad(): - models = ['isotropic', 'klutcher', 'haydavies', 'reindl', 'king', 'perez'] + models = ['isotropic', 'klutcher', 'klucher', + 'haydavies', 'reindl', 'king', 'perez'] AM = atmosphere.relativeairmass(ephem_data['apparent_zenith']) for model in models: total = irradiance.total_irrad( 32, 180, ephem_data['apparent_zenith'], ephem_data['azimuth'], - dni=irrad_data['dni'], ghi=irrad_data['ghi'], dhi=irrad_data['dhi'], + dni=irrad_data['dni'], ghi=irrad_data['ghi'], + dhi=irrad_data['dhi'], dni_extra=dni_et, airmass=AM, model=model, surface_type='urban') From 0c5cf688a81e40e40a567ce8b4aff4faff6213a3 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Fri, 13 Nov 2015 09:33:15 -0700 Subject: [PATCH 017/100] fix bad paper link --- docs/sphinx/source/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index 5ee1574a23..518f86a1a9 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -37,10 +37,10 @@ help updating them! Please see our `PVSC 2014 paper `_ and -`PVSC 2015 abstract `_ -(and the `notebook to reproduce the figures `_ therein) for more information. +`PVSC 2015 paper `_ +(and the `notebook to reproduce the figures `_) for more information. -The wiki also has a page on `Projects and publications that use pvlib python `_. for inspiration and listing of your application. +The GitHub wiki also has a page on `Projects and publications that use pvlib python `_ for inspiration and listing of your application. Installation ============ From 58bb3f53862c980610fb3ca3a20db07e647899cd Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Fri, 13 Nov 2015 16:06:08 -0700 Subject: [PATCH 018/100] add release date --- docs/sphinx/source/whatsnew/v0.2.2.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.2.2.txt b/docs/sphinx/source/whatsnew/v0.2.2.txt index 390129d86d..faa0a522ce 100644 --- a/docs/sphinx/source/whatsnew/v0.2.2.txt +++ b/docs/sphinx/source/whatsnew/v0.2.2.txt @@ -1,6 +1,6 @@ .. _whatsnew_0220: -v0.2.2 (November, 2015) +v0.2.2 (November 13, 2015) ----------------------- This is a minor release from 0.2.1. From 2465ae10df1c37dde93936b4ce845b0a06ac604b Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 15 Nov 2015 11:02:52 -0700 Subject: [PATCH 019/100] more slow progress --- pvlib/location.py | 10 ++++++- pvlib/pvsystem.py | 72 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 4e044ba99f..6fdb446bb5 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -33,18 +33,26 @@ 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. 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. + 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 -------- @@ -52,7 +60,7 @@ class Location(object): """ def __init__(self, latitude, longitude, tz='UTC', altitude=0, - name=None): + name=None, **kwargs): pvl_logger.debug('creating Location object') diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 11732d057e..fa83944fa6 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -75,9 +75,9 @@ class PVSystem(object): racking_model : None or string Used for cell and module temperature calculations. - kwargs : anything - Any other data that you want to assign as an attribute - that may be used by the PVSystem instance methods. + **kwargs + Arbitrary keyword arguments. + Included for compatibility, but not used. See also -------- @@ -116,9 +116,6 @@ def __init__(self, self.racking_model = racking_model - # makes self.some_parameter = some_value for everything in kwargs - [setattr(self, k, v) for k, v in kwargs.items()] - def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, model='isotropic', @@ -149,7 +146,8 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, model : String Irradiance model. - kwargs passed to :func:`irradiance.total_irrad`. + **kwargs + Passed to :func:`irradiance.total_irrad`. Returns ------- @@ -199,43 +197,72 @@ def physicaliam(self, aoi): return physicaliam(K, L, n, aoi) - def calcparams_desoto(self, poa_global, temp_cell, alpha_isc, - module_parameters, - EgRef, dEgdT, M=1, irrad_ref=1000, temp_ref=25): - """Wrapper around the :func:`calcparams_desoto` function. + def calcparams_desoto(self, poa_global, temp_cell, **kwargs): + """ + Use the :func:`calcparams_desoto` function, the input parameters + and ``self.module_parameters`` to calculate the module + currents and resistances. Parameters ---------- - See pvsystem.calcparams_desoto for details + 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, alpha_isc, - module_parameters, - EgRef, dEgdT, M, irrad_ref, temp_ref) + return calcparams_desoto(poa_global, temp_cell, + self.module_parameters, + EgRef, dEgdT, **kwargs) - def sapm(self, module, poa_direct, poa_diffuse, - temp_cell, airmass_absolute, aoi): - """Wrapper around the :func:`sapm` function. + def sapm(self, poa_direct, poa_diffuse, + temp_cell, airmass_absolute, aoi, **kwargs): + """ + Use the :func:`sapm` function, the input parameters, + and ``self.module_parameters`` to calculate + Voc, Isc, Ix, Ixx, Vmp/Imp. Parameters ---------- - See pvsystem.sapm for details + 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(module, poa_direct, poa_diffuse, + 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): - """Wrapper around the :func:`sapm_celltemp` function. + """Uses :func:`sapm_celltemp` to calculate module and cell + temperatures based on ``self.racking_model`` and + the input parameters. Parameters ---------- @@ -283,7 +310,8 @@ def i_from_v(self, resistance_shunt, resistance_series, nNsVth, voltage, # inverter now specified by self.inverter_parameters def snlinverter(self, v_dc, p_dc): - """Wrapper around the :func:`snlinverter` function. + """Uses :func:`snlinverter` to calculate AC power based on + ``self.inverter_parameters`` and the input parameters. Parameters ---------- From ddc88168e694cd5359d60e8a7dc660b4142f08f1 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 15 Nov 2015 13:28:57 -0700 Subject: [PATCH 020/100] addes classes. rename modules --- docs/sphinx/source/classes.rst | 28 +++++++++++++++++++ docs/sphinx/source/index.rst | 3 +- docs/sphinx/source/{pvlib.rst => modules.rst} | 0 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 docs/sphinx/source/classes.rst rename docs/sphinx/source/{pvlib.rst => modules.rst} (100%) diff --git a/docs/sphinx/source/classes.rst b/docs/sphinx/source/classes.rst new file mode 100644 index 0000000000..1ffebd04fa --- /dev/null +++ b/docs/sphinx/source/classes.rst @@ -0,0 +1,28 @@ +Classes +======= + +pvlib-python provides a collection of classes +for users that prefer object-oriented programming. +The classes can help users keep track of data in an 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. + +.. autoclass:: pvlib.location.Location + :members: + :undoc-members: + :show-inheritance: + + +.. autoclass:: pvlib.pvsystem.PVSystem + :members: + :undoc-members: + :show-inheritance: + + +.. autoclass:: pvlib.tracking.SingleAxisTracker + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index 518f86a1a9..8972312c9e 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -60,7 +60,8 @@ Contents self whatsnew comparison_pvlib_matlab - pvlib + modules + classes diff --git a/docs/sphinx/source/pvlib.rst b/docs/sphinx/source/modules.rst similarity index 100% rename from docs/sphinx/source/pvlib.rst rename to docs/sphinx/source/modules.rst From 7e108ecdf2b5187705041b656748e39aae280143 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 15 Nov 2015 13:51:09 -0700 Subject: [PATCH 021/100] add LocalizedPVSystem. improve docs a little --- docs/sphinx/source/classes.rst | 10 +++++++--- pvlib/location.py | 3 +++ pvlib/pvsystem.py | 33 +++++++++++++++++++++++++++++---- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/docs/sphinx/source/classes.rst b/docs/sphinx/source/classes.rst index 1ffebd04fa..8cfe874429 100644 --- a/docs/sphinx/source/classes.rst +++ b/docs/sphinx/source/classes.rst @@ -3,24 +3,28 @@ Classes pvlib-python provides a collection of classes for users that prefer object-oriented programming. -The classes can help users keep track of data in an organized way, +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: - +SingleAxisTracker +----------------- .. autoclass:: pvlib.tracking.SingleAxisTracker :members: :undoc-members: diff --git a/pvlib/location.py b/pvlib/location.py index 6fdb446bb5..5f4fdc49c3 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -80,6 +80,9 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0, self.name = name + # needed for tying together Location and PVSystem in LocalizedPVSystem + super(Location, self).__init__(**kwargs) + def __str__(self): diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index fa83944fa6..af3a6a6b10 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -27,10 +27,17 @@ # import a lot more functionality from other modules. class PVSystem(object): """ - The PVSystem class defines a standard set of system attributes and - wraps the module-level modeling functions. The class is complementary - to the module-level functions. Some modeling applications benefit from - the structure imposed by classes. + 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 ``Location`` and ``ModelChain`` + objects. + + See the :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 @@ -83,6 +90,7 @@ class PVSystem(object): -------- location.Location tracking.SingleAxisTracker + pvsystem.LocalizedPVSystem """ def __init__(self, @@ -115,6 +123,9 @@ def __init__(self, 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_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, @@ -324,6 +335,20 @@ def snlinverter(self, v_dc, p_dc): return snlinverter(self.inverter_parameters, v_dc, p_dc) +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 :class:`PVSystem` class for an object model that + describes an unlocalized PV system. + """ + def __init__(self, **kwargs): + super(LocalizedPVSystem, self).__init__(**kwargs) + + def systemdef(meta, surface_tilt, surface_azimuth, albedo, series_modules, parallel_modules): ''' From 37a36da4de34bc03b3cdc9fd3962c6bf1acbceb5 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 15 Nov 2015 17:06:36 -0700 Subject: [PATCH 022/100] add stub modelchain --- pvlib/modelchain.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 pvlib/modelchain.py diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py new file mode 100644 index 0000000000..f7eda00022 --- /dev/null +++ b/pvlib/modelchain.py @@ -0,0 +1,25 @@ +""" +Stub documentation for the module. +""" + +class ModelChain(object): + """ + Basic class. Consider an abstract base class. + """ + def __init__(self, *args, **kwargs): + pass + + def run_model(self): + pass + + +class MoreSpecificModelChain(ModelChain): + """ + Something more specific. + """ + def __init__(self, *args, **kwargs): + super(MoreSpecificModelChain, self).__init__(**kwargs) + + def run_model(self): + # overrides the parent ModelChain method + pass \ No newline at end of file From cddf91d8e123f3687f4f2e5941454b22ad2a6727 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 15 Nov 2015 17:07:01 -0700 Subject: [PATCH 023/100] make package overview doc --- docs/sphinx/source/classes.rst | 21 ++++++ docs/sphinx/source/index.rst | 1 + docs/sphinx/source/package_overview.rst | 99 +++++++++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 docs/sphinx/source/package_overview.rst diff --git a/docs/sphinx/source/classes.rst b/docs/sphinx/source/classes.rst index 8cfe874429..498839656f 100644 --- a/docs/sphinx/source/classes.rst +++ b/docs/sphinx/source/classes.rst @@ -22,6 +22,27 @@ PVSystem :members: :undoc-members: :show-inheritance: + +ModelChain +---------- +.. autoclass:: pvlib.modelchain.ModelChain + :members: + :undoc-members: + :show-inheritance: + +MoreSpecificModelChain +---------- +.. autoclass:: pvlib.modelchain.MoreSpecificModelChain + :members: + :undoc-members: + :show-inheritance: + +LocalizedPVSystem +----------------- +.. autoclass:: pvlib.pvsystem.LocalizedPVSystem + :members: + :undoc-members: + :show-inheritance: SingleAxisTracker ----------------- diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index 8972312c9e..039871d577 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -60,6 +60,7 @@ Contents self whatsnew comparison_pvlib_matlab + package_overview modules classes diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst new file mode 100644 index 0000000000..5f2c563404 --- /dev/null +++ b/docs/sphinx/source/package_overview.rst @@ -0,0 +1,99 @@ +Package Overview +================ + +The core mission of pvlib-python is to provide open, reliable, +interoperable, and benchmark implementations of PV modeling algorithms. + +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 these algorithms. +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, +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. + +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 :: + + import pandas as pd + + times = pd.DatetimeIndex(start='2015', end='2016', freq='1h') + + # very approximate + coordinates = [(30, -110, 'Tucson'), + (35, -105, 'Albuquerque'), + (40, -120, 'San Francisco'), + (50, 10, 'Berlin')] + +None of these examples are complete! + +Procedural method :: + import pvlib as pvl + + system = {'module': module, 'inverter': inverter, + 'surface_azimuth': 180, **other_params} + + energies = {} + for latitude, longitude, name in coordinates: + cs = pvl.clearsky.ineichen(times, latitude, longitude) + solpos = pvl.solarposition.get_solarposition(times, latitude, longitude) + system['surface_tilt'] = latitude + total_irrad = pvl.irradiance.total_irradiance(**solpos, **cs, **system) + temps = pvl.pvsystem.sapm_celltemp(**total_irrad, **system) + dc = pvl.pvsystem.sapm(**temps, **total_irrad, **system) + ac = pvl.pvsystem.snlinverter(**system, **dc) + annual_energy = power.sum() + energies[name] = annual_energy + + +Object oriented method using +:class:`pvlib.location.Location`, +:class:`pvlib.pvsystem.PVSystem`, and +:class:`pvlib.modelchain.ModelChain` :: + + from pvlib.pvsystem import PVSystem + from pvlib.location import Location + from pvlib.modelchain import ModelChain + + system = PVSystem(module, inverter, **other_params) + + energies = {} + for latitude, longitude, name in coordinates: + location = Location(latitude, longitude) + # not yet clear what, exactly, goes into ModelChain(s) + mc = ModelChain(system, location, times, + 'south_at_latitude', **other_modelchain_params) + output = mc.run_model() + annual_energy = output['power'].sum() + energies[name] = annual_energy + + +Object oriented method using +:class:`pvlib.pvsystem.LocalizedPVSystem` :: + + from pvlib.pvsystem import PVSystem, LocalizedPVSystem + + base_system = PVSystem(module, inverter, **other_system_params) + + energies = {} + for latitude, longitude, name in coordinates: + localized_system = base_system.localize(latitude, longitude, name=name) + localized_system.surface_tilt = latitude + cs = localized_system.get_clearsky(times) + solpos = localized_system.get_solarposition(times) + total_irrad = localized_system.get_irradiance(times, **solpos, **cs) + power = localized_system.get_power(stuff) + annual_energy = power.sum() + energies[name] = annual_energy + From a38104922342654302c96ed919bc5f76febe06e2 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 15 Nov 2015 19:20:28 -0700 Subject: [PATCH 024/100] doc improvements --- docs/sphinx/source/modules.rst | 26 ++++--- docs/sphinx/source/package_overview.rst | 91 ++++++++++++++++++++----- 2 files changed, 90 insertions(+), 27 deletions(-) diff --git a/docs/sphinx/source/modules.rst b/docs/sphinx/source/modules.rst index 8896f6dea2..3ae22dd0f4 100644 --- a/docs/sphinx/source/modules.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 index 5f2c563404..31bc979a0f 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -1,8 +1,11 @@ Package Overview ================ +Introduction +------------ + The core mission of pvlib-python is to provide open, reliable, -interoperable, and benchmark implementations of PV modeling algorithms. +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 @@ -13,7 +16,7 @@ Modeling paradigms ------------------ The backbone of pvlib-python -is well-tested procedural code that implements these algorithms. +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, @@ -37,30 +40,60 @@ configuration at a handful of sites listed below :: (50, 10, 'Berlin')] None of these examples are complete! +Should replace the clear sky assumption with TMY or similar +(or leave as an exercise to the reader?). + + +Procedural +^^^^^^^^^^ + +Procedural code can be used to for all modeling steps in pvlib-python. -Procedural method :: - import pvlib as pvl +The following code demonstrates how to use the procedural code +to accomplish our system modeling goal: :: + + import pvlib system = {'module': module, 'inverter': inverter, 'surface_azimuth': 180, **other_params} energies = {} for latitude, longitude, name in coordinates: - cs = pvl.clearsky.ineichen(times, latitude, longitude) - solpos = pvl.solarposition.get_solarposition(times, latitude, longitude) + cs = pvlib.clearsky.ineichen(times, latitude, longitude) + solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude) system['surface_tilt'] = latitude - total_irrad = pvl.irradiance.total_irradiance(**solpos, **cs, **system) - temps = pvl.pvsystem.sapm_celltemp(**total_irrad, **system) - dc = pvl.pvsystem.sapm(**temps, **total_irrad, **system) - ac = pvl.pvsystem.snlinverter(**system, **dc) + total_irrad = pvlib.irradiance.total_irradiance(**solpos, **cs, **system) + temps = pvlib.pvsystem.sapm_celltemp(**total_irrad, **system) + dc = pvlib.pvsystem.sapm(**temps, **total_irrad, **system) + ac = pvlib.pvsystem.snlinverter(**system, **dc) annual_energy = power.sum() energies[name] = annual_energy - - -Object oriented method using -:class:`pvlib.location.Location`, -:class:`pvlib.pvsystem.PVSystem`, and -:class:`pvlib.modelchain.ModelChain` :: + + #energies = pd.DataFrame(energies) + #energies.plot() + + +Object oriented (Location, PVSystem, ModelChain) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The first object oriented paradigm uses a model where +a :class:`PVSystem ` object represents an +assembled collection of modules, inverters, etc., +a :class:`Location ` object represents a +particular place on the planet, +and a :class:`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 +:class:`Location `, +:class:`PVSystem `, and +:class:`ModelChain ` +objects to accomplish our system modeling goal: :: from pvlib.pvsystem import PVSystem from pvlib.location import Location @@ -77,10 +110,23 @@ Object oriented method using output = mc.run_model() annual_energy = output['power'].sum() energies[name] = annual_energy + + #energies = pd.DataFrame(energies) + #energies.plot() + +Object oriented (LocalizedPVSystem) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Object oriented method using -:class:`pvlib.pvsystem.LocalizedPVSystem` :: +The second object oriented paradigm uses a model where a +:class:`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 +:class:`LocalizedPVSystem ` +object to accomplish our modeling goal: :: from pvlib.pvsystem import PVSystem, LocalizedPVSystem @@ -96,4 +142,13 @@ Object oriented method using power = localized_system.get_power(stuff) annual_energy = power.sum() energies[name] = annual_energy + + #energies = pd.DataFrame(energies) + #energies.plot() + +User extensions +--------------- +There are many other ways to organize PV modeling code. +The pvlib-python developers encourage users to build on one of +these paradigms and to share their experiences. From ed6dc819402811ec34c32f8cf844c7887161a130 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 15 Nov 2015 19:57:58 -0700 Subject: [PATCH 025/100] more doc stuff --- docs/sphinx/source/index.rst | 5 ++-- docs/sphinx/source/package_overview.rst | 36 ++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index 039871d577..fe38f7fa10 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -58,12 +58,11 @@ Contents :maxdepth: 2 self - whatsnew - comparison_pvlib_matlab package_overview modules classes - + comparison_pvlib_matlab + whatsnew Indices and tables diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index 31bc979a0f..5b3a9e8e84 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -51,7 +51,7 @@ Procedural code can be used to for all modeling steps in pvlib-python. The following code demonstrates how to use the procedural code to accomplish our system modeling goal: :: - + import pvlib system = {'module': module, 'inverter': inverter, @@ -127,7 +127,7 @@ a power plant that already exists. The following code demonstrates how to use a :class:`LocalizedPVSystem ` object to accomplish our modeling goal: :: - + from pvlib.pvsystem import PVSystem, LocalizedPVSystem base_system = PVSystem(module, inverter, **other_system_params) @@ -150,5 +150,33 @@ object to accomplish our modeling goal: :: User extensions --------------- There are many other ways to organize PV modeling code. -The pvlib-python developers encourage users to build on one of -these paradigms and to share their experiences. +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. + From 1a677d699129a0449ff26b354aebc5f2b09ecd38 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 15 Nov 2015 20:22:43 -0700 Subject: [PATCH 026/100] make aoi loss models have a consistent signature --- pvlib/pvsystem.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index af3a6a6b10..76aa6660ba 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -178,24 +178,28 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, # defaults to kwargs, falls back to attributes. complicated. # harder to support? - def ashraeiam(self, **kwargs): - """Wrapper around the :func:`ashraeiam` function. + def ashraeiam(self, aoi): + """ + Determine the incidence angle modifier using + ``self.b``, ``aoi``, and the :func:`ashraeiam` function. Parameters ---------- - kwargs : None, b, a - See pvsystem.ashraeiam for details + See pvsystem.ashraeiam for details Returns ------- See pvsystem.ashraeiam for details """ - return ashraeiam(kwargs.pop('b', self.b), kwargs.pop('aoi', self.aoi)) + return ashraeiam(self.b, aoi) # thin wrappers of other pvsystem functions def physicaliam(self, aoi): - """Wrapper around the :func:`physicaliam` function. + """ + Determine the incidence angle modifier using + ``self.K``, ``self.L``, ``self.n``, ``aoi``, and + :func:`physicaliam` function. Parameters ---------- @@ -205,7 +209,7 @@ def physicaliam(self, aoi): ------- See pvsystem.physicaliam for details """ - return physicaliam(K, L, n, aoi) + return physicaliam(self.K, self.L, self.n, aoi) def calcparams_desoto(self, poa_global, temp_cell, **kwargs): From 515d97aa21f2b6df8b8dab7d1b6daea5eb32bc92 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 15 Nov 2015 20:54:27 -0700 Subject: [PATCH 027/100] flesh out modelchain a little --- pvlib/modelchain.py | 101 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 4 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index f7eda00022..1af300d99b 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -4,13 +4,106 @@ class ModelChain(object): """ - Basic class. Consider an abstract base class. + A class that represents all of the modeling steps necessary for + calculating power or energy for a PV system at a given location. + + Consider an abstract base class. + + Parameters + ---------- + system : PVSystem + The connected set of modules, inverters, etc. + + location : location + The physical location at which to evaluate the model. + + times : DatetimeIndex + Times at which to evaluate the model. + + orientation_strategy : str + The strategy for aligning the modules. + + clearsky_method : str + Passed to location.get_clearsky. + + solar_position_method : str + Passed to location.get_solarposition. + + **kwargs + Arbitrary keyword arguments. + Included for compatibility, but not used. + + See also + -------- + location.Location + pvsystem.PVSystem """ - def __init__(self, *args, **kwargs): - pass + + def __init__(self, system, location, times, + orientation_strategy='south_at_latitude', + clearsky_method='ineichen', + solar_position_method='nrel_numpy', + **kwargs): + + self.system = system + self.location = location + self.times = times + self.clearsky_method = clearsky_method + self.solar_position_method = solar_position_method + + self._orientation_strategy = orientation_strategy + + @property + def orientation_strategy(self): + return self._orientation_strategy + + @property.setter + def orientation_strategy(self, strategy): + if strategy == 'south_at_latitude': + self.surface_azimuth = 180 + self.surface_tilt = self.location.latitude + elif strategy == 'flat': + self.surface_azimuth = 0 + self.surface_tilt = 0 + else: + raise ValueError('invalid orientation strategy. strategy must ' + + 'be one of south_at_latitude, flat,') + + self._orientation_strategy = strategy + + def run_model(self): - pass + """ + Run the model. + + Returns + ------- + output : DataFrame + Column names??? + """ + solar_position = self.location.get_solarposition(self.times) + clearsky = self.system.get_clearsky(solar_position, + self.clearsky_method) + final_output = self.system.calculate_final_yield(args) + return final_output + + + def model_system(self): + """ + Model the system? + + I'm just copy/pasting example code... + + Returns + ------- + ??? + """ + + final_output = self.run_model() + input = self.prettify_input() + modeling_steps = self.get_modeling_steps() + class MoreSpecificModelChain(ModelChain): From 3b7cd9a6e27a69b47ff57022b0ee9b8ea5d05d95 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 20 Dec 2015 20:34:04 -0700 Subject: [PATCH 028/100] remove location from solarposition module. enforce localized or utc times. --- pvlib/solarposition.py | 136 ++++++++++---------- pvlib/test/test_solarposition.py | 213 ++++++++++++++++--------------- 2 files changed, 181 insertions(+), 168 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 403db9d26a..0df544e95c 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -29,8 +29,8 @@ from pvlib.tools import localize_to_utc, datetime_to_djd, djd_to_datetime -def get_solarposition(time, location=None, latitude=None, longitude=None, - altitude=None, +def get_solarposition(time, latitude, longitude, + altitude=0, method='nrel_numpy', pressure=101325, temperature=12, **kwargs): """ @@ -39,9 +39,8 @@ def get_solarposition(time, location=None, latitude=None, longitude=None, Parameters ---------- time : pandas.DatetimeIndex - location : None or pvlib.Location object - latitude : None or float - longitude : None or float + latitude : float + longitude : float altitude : None or float method : string 'pyephem' uses the PyEphem package: :func:`pyephem` @@ -73,11 +72,6 @@ def get_solarposition(time, location=None, latitude=None, longitude=None, [3] NREL SPA code: http://rredc.nrel.gov/solar/codesandalgorithms/spa/ """ - if location is not None: - warnings.warn("The location argument is deprecated. Use " + - "Location.get_solarposition or specify the " + - "latitude and longitude arguments", DeprecationWarning) - method = method.lower() if isinstance(time, dt.datetime): time = pd.DatetimeIndex([time, ]) @@ -105,7 +99,7 @@ def get_solarposition(time, location=None, latitude=None, longitude=None, return ephem_df -def spa_c(time, latitude, longitude, pressure=101325, +def spa_c(time, latitude, longitude, pressure=101325, altitude=0, temperature=12, delta_t=67.0, raw_spa_output=False): """ @@ -120,9 +114,13 @@ def spa_c(time, latitude, longitude, pressure=101325, 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 @@ -164,8 +162,8 @@ def spa_c(time, latitude, longitude, pressure=101325, pvl_logger.debug('using built-in spa code to calculate solar position') - time_utc = localize_to_utc(time, location) - + time_utc = time + spa_out = [] for date in time_utc: @@ -175,16 +173,16 @@ def spa_c(time, latitude, longitude, pressure=101325, 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 @@ -243,6 +241,7 @@ def spa_python(time, latitude, longitude, Parameters ---------- time : pandas.DatetimeIndex + Localized or UTC. latitude : float longitude : float altitude : float @@ -321,15 +320,11 @@ def spa_python(time, latitude, longitude, 'equation_of_time': eot}, index=time) - try: - result = result.tz_convert(time.tz) - except TypeError: - result = result.tz_localize(time.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 @@ -344,7 +339,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 @@ -371,8 +367,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): @@ -392,31 +388,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 @@ -425,14 +416,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 @@ -449,7 +445,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 @@ -460,17 +455,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) @@ -483,7 +483,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) @@ -497,13 +497,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. @@ -512,7 +509,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 @@ -556,9 +554,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 @@ -567,8 +562,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) @@ -576,8 +571,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 @@ -664,7 +662,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 @@ -675,8 +673,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 @@ -686,13 +684,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. @@ -719,7 +720,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 @@ -732,7 +734,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/test_solarposition.py b/pvlib/test/test_solarposition.py index 8123f7bfed..2b1ae1caac 100644 --- a/pvlib/test/test_solarposition.py +++ b/pvlib/test/test_solarposition.py @@ -9,7 +9,7 @@ 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 @@ -20,72 +20,79 @@ 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. -def test_spa_physical(): - times = pd.date_range(datetime.datetime(2003,10,17,12,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_mst, 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_physical_dst(): +def test_spa_c_physical_dst(): times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), periods=1, - freq='D') + freq='D', tz=golden.tz) try: - ephem_data = solarposition.spa_c(times, golden, pressure=82000, - temperature=11).ix[0] + ephem_data = solarposition.spa_c(times, golden.latitude, + golden.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) - - -def test_spa_localization(): - try: - assert_frame_equal(solarposition.spa_c(times, tus), - solarposition.spa_c(times_localized, tus)) - 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, + 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, + 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(): @@ -97,15 +104,17 @@ def test_spa_python_numba_physical(): 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, + 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 +126,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, + 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 +149,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,34 +169,36 @@ 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) 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)) 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)) def test_calc_time(): @@ -203,10 +212,12 @@ def test_calc_time(): 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) - @@ -216,32 +227,32 @@ def test_calc_time(): 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') + 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 + assert_frame_equal(this_expected.round(2), + ephem_data[this_expected.columns].round(2)) 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 + assert_frame_equal(this_expected.round(2), + ephem_data[this_expected.columns].round(2)) From b2d33ffdcd9b829624bf7500f8b6f242fcc8d20c Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 20 Dec 2015 21:08:41 -0700 Subject: [PATCH 029/100] move iam args into PVSystems self.module_parameters --- pvlib/pvsystem.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 76aa6660ba..8da4182ddd 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -176,29 +176,33 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, **kwargs) - # defaults to kwargs, falls back to attributes. complicated. - # harder to support? def ashraeiam(self, aoi): """ Determine the incidence angle modifier using - ``self.b``, ``aoi``, and the :func:`ashraeiam` function. + ``self.module_parameters['b']``, ``aoi``, + and the :func:`ashraeiam` function. Parameters ---------- - See pvsystem.ashraeiam for details + aoi : numeric + The angle of incidence in degrees. Returns ------- - See pvsystem.ashraeiam for details + modifier : numeric + The AOI modifier. """ - return ashraeiam(self.b, aoi) - - - # thin wrappers of other pvsystem functions + b = self.module_parameters['b'] + return ashraeiam(b, aoi) + + def physicaliam(self, aoi): """ Determine the incidence angle modifier using - ``self.K``, ``self.L``, ``self.n``, ``aoi``, and + ``self.module_parameters['K']``, + ``self.module_parameters['L']``, + ``self.module_parameters['n']``, + ``aoi``, and the :func:`physicaliam` function. Parameters @@ -209,9 +213,12 @@ def physicaliam(self, aoi): ------- See pvsystem.physicaliam for details """ - return physicaliam(self.K, self.L, self.n, aoi) - - + 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 :func:`calcparams_desoto` function, the input parameters @@ -236,8 +243,8 @@ def calcparams_desoto(self, poa_global, temp_cell, **kwargs): return calcparams_desoto(poa_global, temp_cell, self.module_parameters, EgRef, dEgdT, **kwargs) - - + + def sapm(self, poa_direct, poa_diffuse, temp_cell, airmass_absolute, aoi, **kwargs): """ From dc7569104c717ae01c2765016aa542d214cdf9cb Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 20 Dec 2015 21:09:43 -0700 Subject: [PATCH 030/100] get test_pvsystem working with new ineichen sig. add dummy class tests --- pvlib/clearsky.py | 2 +- pvlib/test/test_pvsystem.py | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index f43fe81338..38394a92df 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -20,7 +20,7 @@ -def ineichen(time, latitude, longitude, altitude, linke_turbidity=None, +def ineichen(time, latitude, longitude, altitude=0, linke_turbidity=None, solarposition_method='pyephem', zenith_data=None, airmass_model='young1994', airmass_data=None, interp_turbidity=True): diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index babe1c0df1..d624d28432 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -19,14 +19,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, @@ -200,3 +206,13 @@ 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_LocalizedPVSystem_creation(): + localized_pv_system = pvsystem.LocalizedPVSystem(latitude=30, + longitude=-110, + module='blah', + inverter='blarg') \ No newline at end of file From 10775e27c1854ffc5832eba0f6b4698520af4bf2 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 20 Dec 2015 21:22:56 -0700 Subject: [PATCH 031/100] make test_clearsky work with new sig. fails due to changes in ephem method and relocation of altitude to a kwarg --- pvlib/test/test_clearsky.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pvlib/test/test_clearsky.py b/pvlib/test/test_clearsky.py index ce9a010039..259f1cc187 100644 --- a/pvlib/test/test_clearsky.py +++ b/pvlib/test/test_clearsky.py @@ -17,7 +17,8 @@ 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) def test_ineichen_required(): @@ -34,7 +35,7 @@ def test_ineichen_required(): [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) @@ -50,12 +51,15 @@ def test_ineichen_supply_linke(): [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') @@ -71,7 +75,8 @@ def test_ineichen_airmass(): [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) From 66b3603e4a29491b1c9250337e3350dcc80c763f Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 20 Dec 2015 21:23:43 -0700 Subject: [PATCH 032/100] make altitude a kwarg --- pvlib/clearsky.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index 38394a92df..a85993dcc2 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -19,7 +19,6 @@ from pvlib import solarposition - def ineichen(time, latitude, longitude, altitude=0, linke_turbidity=None, solarposition_method='pyephem', zenith_data=None, airmass_model='young1994', airmass_data=None, From ad7c990d72119456136c5369f66d3af817ce3ebe Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 20 Dec 2015 21:27:22 -0700 Subject: [PATCH 033/100] make test_irradiance work with new ineichen sig --- pvlib/test/test_irradiance.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pvlib/test/test_irradiance.py b/pvlib/test/test_irradiance.py index 44911253c1..ac993de3f2 100644 --- a/pvlib/test/test_irradiance.py +++ b/pvlib/test/test_irradiance.py @@ -23,9 +23,11 @@ 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='pyephem') -irrad_data = clearsky.ineichen(times, tus, linke_turbidity=3, +irrad_data = clearsky.ineichen(times, tus.latitude, tus.longitude, + altitude=tus.altitude, linke_turbidity=3, solarposition_method='pyephem') dni_et = irradiance.extraradiation(times.dayofyear) @@ -168,9 +170,10 @@ def test_globalinplane(): 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 +190,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) From 358accdad97aa23e6b3865b4a5702cc1a4a53aaa Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 20 Dec 2015 21:29:23 -0700 Subject: [PATCH 034/100] make test_atmosphere work with new solpos sig --- pvlib/test/test_atmosphere.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 603610638fd8b1ed5282aa129e2c8b83abdc5954 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 18 Jan 2016 16:46:43 -0700 Subject: [PATCH 035/100] fix cs test numbers due to new solpos --- pvlib/test/test_clearsky.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/pvlib/test/test_clearsky.py b/pvlib/test/test_clearsky.py index 259f1cc187..880f11dcba 100644 --- a/pvlib/test/test_clearsky.py +++ b/pvlib/test/test_clearsky.py @@ -12,6 +12,8 @@ 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') @@ -20,17 +22,16 @@ 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], + [51.0100314,259.73341927,82.76689082], + [104.99512329,832.22000002,682.43280974], + [121.97931179,901.31646645,1008.00975362], + [112.5726345,867.73434086,824.48415382], + [76.61228483,587.82419004,253.67624301], [0.,0.,0.], [0.,0.,0.]]), columns=['dhi', 'dni', 'ghi'], @@ -42,11 +43,11 @@ def test_ineichen_required(): 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], + [39.81862097,316.23284759,78.48350328], + [95.12705301,876.43232906,703.24156169], + [118.45796469,939.81499487,1042.33401282], + [105.35769022,909.0884868,851.19721202], + [61.83556162,646.45362207,256.55983299], [0.,0.,0.], [0.,0.,0.]]), columns=['dhi', 'dni', 'ghi'], @@ -66,11 +67,11 @@ def test_ineichen_solpos(): 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], + [53.52826665,250.64463008,84.17386592], + [101.32775752,842.86030421,686.14824255], + [117.7568185,909.70199089,1012.03056908], + [108.61662929,877.27820363,828.35817853], + [75.15682967,601.03375193,256.19976209], [0.,0.,0.], [0.,0.,0.]]), columns=['dhi', 'dni', 'ghi'], From 2d14082242bdf9922472049388e014bfe831ebe1 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 18 Jan 2016 17:11:18 -0700 Subject: [PATCH 036/100] add has_ephem, modify tests for min pandas compatibility --- ci/requirements-py27-min.yml | 1 - pvlib/test/__init__.py | 9 +++++++++ pvlib/test/test_solarposition.py | 19 +++++++++++-------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/ci/requirements-py27-min.yml b/ci/requirements-py27-min.yml index 1cd6103267..fcda181476 100644 --- a/ci/requirements-py27-min.yml +++ b/ci/requirements-py27-min.yml @@ -6,6 +6,5 @@ dependencies: - pandas==0.13.1 - nose - pytz - - ephem - pip: - coveralls \ No newline at end of file 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_solarposition.py b/pvlib/test/test_solarposition.py index 2b1ae1caac..c7b75e55b6 100644 --- a/pvlib/test/test_solarposition.py +++ b/pvlib/test/test_solarposition.py @@ -14,6 +14,7 @@ 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), @@ -176,7 +177,7 @@ def test_get_sun_rise_set_transit(): 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', tz=golden_mst.tz) @@ -188,7 +189,7 @@ def test_pyephem_physical(): 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', tz=golden.tz) @@ -200,7 +201,7 @@ def test_pyephem_physical_dst(): assert_frame_equal(this_expected.round(2), ephem_data[this_expected.columns].round(2)) - +@requires_ephem def test_calc_time(): import pytz import math @@ -225,7 +226,7 @@ def test_calc_time(): assert_almost_equals((az.replace(second=0, microsecond=0) - epoch_dt).total_seconds(), actual_timestamp) - +@requires_ephem def test_earthsun_distance(): times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), periods=1, freq='D') @@ -242,8 +243,9 @@ def test_ephemeris_physical(): temperature=11) this_expected = expected.copy() this_expected.index = times - assert_frame_equal(this_expected.round(2), - ephem_data[this_expected.columns].round(2)) + 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(): @@ -254,5 +256,6 @@ def test_ephemeris_physical_dst(): temperature=11) this_expected = expected.copy() this_expected.index = times - assert_frame_equal(this_expected.round(2), - ephem_data[this_expected.columns].round(2)) + this_expected = np.round(this_expected, 2) + ephem_data = np.round(ephem_data, 2) + assert_frame_equal(this_expected, ephem_data[this_expected.columns]) From 125cae2fc3adf11c6e2edcd6b5607a43e36dce6a Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 18 Jan 2016 17:33:13 -0700 Subject: [PATCH 037/100] further fixes to min req and transition to all nrel_numpy defaults --- ci/requirements-py27-min.yml | 1 - pvlib/clearsky.py | 2 +- pvlib/test/test_clearsky.py | 61 +++++++++++++++++++---------------- pvlib/test/test_irradiance.py | 21 +++++++----- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/ci/requirements-py27-min.yml b/ci/requirements-py27-min.yml index fcda181476..fab0813e94 100644 --- a/ci/requirements-py27-min.yml +++ b/ci/requirements-py27-min.yml @@ -2,7 +2,6 @@ name: test_env dependencies: - python=2.7 - numpy==1.8.2 - - scipy - pandas==0.13.1 - nose - pytz diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index a85993dcc2..fb64a5d473 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -20,7 +20,7 @@ def ineichen(time, latitude, longitude, altitude=0, linke_turbidity=None, - solarposition_method='pyephem', zenith_data=None, + solarposition_method='nrel_numpy', zenith_data=None, airmass_model='young1994', airmass_data=None, interp_turbidity=True): ''' diff --git a/pvlib/test/test_clearsky.py b/pvlib/test/test_clearsky.py index 880f11dcba..41ee88131a 100644 --- a/pvlib/test/test_clearsky.py +++ b/pvlib/test/test_clearsky.py @@ -25,15 +25,16 @@ @requires_scipy def test_ineichen_required(): # the clearsky function should call lookup_linke_turbidity by default - expected = pd.DataFrame(np.array([[0.,0.,0.], - [0.,0.,0.], - [51.0100314,259.73341927,82.76689082], - [104.99512329,832.22000002,682.43280974], - [121.97931179,901.31646645,1008.00975362], - [112.5726345,867.73434086,824.48415382], - [76.61228483,587.82419004,253.67624301], - [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_localized, tus.latitude, tus.longitude) @@ -41,15 +42,16 @@ def test_ineichen_required(): def test_ineichen_supply_linke(): - expected = pd.DataFrame(np.array([[0.,0.,0.], - [0.,0.,0.], - [39.81862097,316.23284759,78.48350328], - [95.12705301,876.43232906,703.24156169], - [118.45796469,939.81499487,1042.33401282], - [105.35769022,909.0884868,851.19721202], - [61.83556162,646.45362207,256.55983299], - [0.,0.,0.], - [0.,0.,0.]]), + expected = pd.DataFrame( + np.array([[ 0. , 0. , 0. ], + [ 0. , 0. , 0. ], + [ 40.19492186, 322.1949484 , 80.27218726], + [ 95.14479487, 876.49778895, 703.49655602], + [ 118.45876694, 939.816594 , 1042.34575261], + [ 105.36721216, 909.11474576, 851.33560265], + [ 61.91851386, 647.43752674, 257.50239737], + [ 0. , 0. , 0. ], + [ 0. , 0. , 0. ]]), columns=['dhi', 'dni', 'ghi'], index=times_localized) out = clearsky.ineichen(times_localized, tus.latitude, tus.longitude, @@ -65,15 +67,16 @@ def test_ineichen_solpos(): def test_ineichen_airmass(): - expected = pd.DataFrame(np.array([[0.,0.,0.], - [0.,0.,0.], - [53.52826665,250.64463008,84.17386592], - [101.32775752,842.86030421,686.14824255], - [117.7568185,909.70199089,1012.03056908], - [108.61662929,877.27820363,828.35817853], - [75.15682967,601.03375193,256.19976209], - [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_localized, tus.latitude, tus.longitude, @@ -82,6 +85,7 @@ def test_ineichen_airmass(): 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) @@ -93,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) @@ -103,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) @@ -113,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 ac993de3f2..d044444385 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') @@ -24,11 +26,11 @@ times_localized = times.tz_localize(tus.tz) ephem_data = solarposition.get_solarposition(times, tus.latitude, - tus.longitude, method='pyephem') + tus.longitude, method='nrel_numpy') irrad_data = clearsky.ineichen(times, tus.latitude, tus.longitude, altitude=tus.altitude, linke_turbidity=3, - solarposition_method='pyephem') + solarposition_method='nrel_numpy') dni_et = irradiance.extraradiation(times.dayofyear) @@ -60,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') @@ -111,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(): @@ -137,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(): @@ -158,12 +163,12 @@ def test_total_irrad(): 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) From 04f477f835fe421daa98a979e76f19cf35b9a1d9 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Tue, 19 Jan 2016 14:08:51 -0700 Subject: [PATCH 038/100] pull in some @MoonRaker changes to docs conf.py --- docs/sphinx/source/conf.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 From aa52f5f36ed37dad3065accf28af4a43481a89e1 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Tue, 19 Jan 2016 16:15:38 -0700 Subject: [PATCH 039/100] fix pvlib/pvlib-python#105, add poa_diffuse --- pvlib/irradiance.py | 23 +++++++++++++++-------- pvlib/test/test_irradiance.py | 4 ++++ 2 files changed, 19 insertions(+), 8 deletions(-) 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/test/test_irradiance.py b/pvlib/test/test_irradiance.py index d044444385..2fa2f5b072 100644 --- a/pvlib/test/test_irradiance.py +++ b/pvlib/test/test_irradiance.py @@ -159,6 +159,10 @@ 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(): From 71200828da738beb960c18de203ce5889b714ff7 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Tue, 19 Jan 2016 16:20:56 -0700 Subject: [PATCH 040/100] standardize sapm_celltemp arg names. add dict/Series input --- pvlib/pvsystem.py | 30 +++++++++++++++++------------- pvlib/test/test_pvsystem.py | 10 ++++++++++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 8da4182ddd..4db9d6b695 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1067,7 +1067,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 @@ -1076,16 +1077,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: @@ -1097,7 +1098,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 @@ -1136,25 +1138,27 @@ def sapm_celltemp(irrad, wind, temp, model='open_rack_cell_glassback'): '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] deltaT = model[2] E0 = 1000. # Reference irradiance - - temp_module = pd.Series(irrad*np.exp(a + b*wind) + temp) - temp_cell = temp_module + (irrad / E0)*(deltaT) + temp_module = pd.Series(poa_global*np.exp(a + b*wind_speed) + temp_air) + + temp_cell = temp_module + (poa_global / E0)*(deltaT) return pd.DataFrame({'temp_cell': temp_cell, 'temp_module': temp_module}) - - + + def singlediode(module, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): ''' diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 4a276853e8..3dd94b7895 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -171,6 +171,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) From a3667143f532d77a469426d48ea9ece00e3644f6 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Tue, 19 Jan 2016 16:21:15 -0700 Subject: [PATCH 041/100] get the procedural code example working in the docs --- docs/sphinx/source/package_overview.rst | 53 ++++++++++++++++--------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index 5b3a9e8e84..e40ef5c10a 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -27,7 +27,9 @@ 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 :: +configuration at a handful of sites listed below. + +.. ipython:: python import pandas as pd @@ -38,36 +40,44 @@ configuration at a handful of sites listed below :: (35, -105, 'Albuquerque'), (40, -120, 'San Francisco'), (50, 10, 'Berlin')] - -None of these examples are complete! -Should replace the clear sky assumption with TMY or similar -(or leave as an exercise to the reader?). + + import pvlib + + 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_'] Procedural ^^^^^^^^^^ -Procedural code can be used to for all modeling steps in pvlib-python. +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: :: - - import pvlib +to accomplish our system modeling goal: + +.. ipython:: python system = {'module': module, 'inverter': inverter, - 'surface_azimuth': 180, **other_params} + 'surface_azimuth': 180} energies = {} for latitude, longitude, name in coordinates: + system['surface_tilt'] = latitude cs = pvlib.clearsky.ineichen(times, latitude, longitude) solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude) - system['surface_tilt'] = latitude - total_irrad = pvlib.irradiance.total_irradiance(**solpos, **cs, **system) - temps = pvlib.pvsystem.sapm_celltemp(**total_irrad, **system) - dc = pvlib.pvsystem.sapm(**temps, **total_irrad, **system) - ac = pvlib.pvsystem.snlinverter(**system, **dc) - annual_energy = power.sum() + airmass = pvlib.atmosphere.relativeairmass(solpos['apparent_zenith']) + aoi = pvlib.irradiance.aoi(system['surface_tilt'], system['surface_azimuth'], solpos['apparent_zenith'], solpos['azimuth']) + total_irrad = pvlib.irradiance.total_irrad(**solpos, **cs, **system) + temps = pvlib.pvsystem.sapm_celltemp(total_irrad['poa_global'], 0, 20) + dc = pvlib.pvsystem.sapm(module, total_irrad['poa_direct'], total_irrad['poa_diffuse'], temps['temp_cell'], airmass, aoi) + ac = pvlib.pvsystem.snlinverter(inverter, dc['v_mp'], dc['p_mp']) + annual_energy = ac.sum() energies[name] = annual_energy + + print(energies) #energies = pd.DataFrame(energies) #energies.plot() @@ -93,7 +103,9 @@ The following code demonstrates how to use :class:`Location `, :class:`PVSystem `, and :class:`ModelChain ` -objects to accomplish our system modeling goal: :: +objects to accomplish our system modeling goal: + +.. ipython:: python from pvlib.pvsystem import PVSystem from pvlib.location import Location @@ -126,10 +138,15 @@ a power plant that already exists. The following code demonstrates how to use a :class:`LocalizedPVSystem ` -object to accomplish our modeling goal: :: +object to accomplish our modeling goal: + +.. ipython:: python from pvlib.pvsystem import PVSystem, LocalizedPVSystem + module = + inverter = + other_system_params = {} # sometime helpful to break apart base_system = PVSystem(module, inverter, **other_system_params) energies = {} From c3073f001c690a1e4a068ffb73243a699eecef75 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Tue, 19 Jan 2016 16:28:30 -0700 Subject: [PATCH 042/100] change rtd python to 3.5 to use dict unpacking --- docs/environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/environment.yml b/docs/environment.yml index ca06545e2d..043c199290 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -1,6 +1,6 @@ name: pvlib dependencies: - - python=2.7 + - python=3.5 - numpy - scipy - pandas From 560bf8461750cedec12168b91ae9854b04d06d96 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Tue, 19 Jan 2016 16:33:50 -0700 Subject: [PATCH 043/100] add recommonmark --- docs/environment.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/environment.yml b/docs/environment.yml index 043c199290..bbdf10f7c7 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -11,4 +11,6 @@ dependencies: - sphinx - numpydoc - matplotlib - - seaborn \ No newline at end of file + - seaborn + - pip: + - recommonmark \ No newline at end of file From 96449ba4f24548e587e011cd760a49880ac4ffbf Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Tue, 19 Jan 2016 16:58:48 -0700 Subject: [PATCH 044/100] remove recommonmark. problem was in rtd admin config --- docs/environment.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/environment.yml b/docs/environment.yml index bbdf10f7c7..043c199290 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -11,6 +11,4 @@ dependencies: - sphinx - numpydoc - matplotlib - - seaborn - - pip: - - recommonmark \ No newline at end of file + - seaborn \ No newline at end of file From eae606c51d48c726d0a786742593b9fcfeb2d0c7 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Tue, 19 Jan 2016 17:01:56 -0700 Subject: [PATCH 045/100] back to 2.7... --- docs/environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/environment.yml b/docs/environment.yml index 043c199290..504c0013a2 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -1,6 +1,6 @@ name: pvlib dependencies: - - python=3.5 + - python=2.7 - numpy - scipy - pandas From 3ca9fddcda3d811b14590f42fc8cf6f96ea2667d Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Tue, 19 Jan 2016 17:43:10 -0700 Subject: [PATCH 046/100] fix python 2.7 issues in doc example. make plotting work --- docs/sphinx/source/_static/.gitignore | 0 docs/sphinx/source/package_overview.rst | 30 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 docs/sphinx/source/_static/.gitignore 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/package_overview.rst b/docs/sphinx/source/package_overview.rst index e40ef5c10a..cbc3edf06a 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -32,6 +32,9 @@ configuration at a handful of sites listed below. .. ipython:: python import pandas as pd + import matplotlib.pyplot as plt + import seaborn as sns + sns.set_color_codes() times = pd.DatetimeIndex(start='2015', end='2016', freq='1h') @@ -68,19 +71,34 @@ to accomplish our system modeling goal: system['surface_tilt'] = latitude cs = pvlib.clearsky.ineichen(times, latitude, longitude) 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']) - aoi = pvlib.irradiance.aoi(system['surface_tilt'], system['surface_azimuth'], solpos['apparent_zenith'], solpos['azimuth']) - total_irrad = pvlib.irradiance.total_irrad(**solpos, **cs, **system) + 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'], 0, 20) - dc = pvlib.pvsystem.sapm(module, total_irrad['poa_direct'], total_irrad['poa_diffuse'], temps['temp_cell'], airmass, aoi) + dc = pvlib.pvsystem.sapm(module, total_irrad['poa_direct'], + total_irrad['poa_diffuse'], temps['temp_cell'], + airmass, aoi) ac = pvlib.pvsystem.snlinverter(inverter, dc['v_mp'], dc['p_mp']) annual_energy = ac.sum() energies[name] = annual_energy + + energies = pd.Series(energies) - print(energies) + # based on the parameters specified above, these are in W*hrs + print(energies.round(0)) - #energies = pd.DataFrame(energies) - #energies.plot() + energies.plot(kind='bar', rot=0) + @savefig proc-energies.png width=6in + plt.ylabel('Yearly energy yield (W hr)') Object oriented (Location, PVSystem, ModelChain) From e5d77d57f83328726cc702bba8e814555b1cc17c Mon Sep 17 00:00:00 2001 From: dacoex Date: Thu, 21 Jan 2016 14:09:44 +0100 Subject: [PATCH 047/100] minor markup correction --- docs/sphinx/source/package_overview.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index cbc3edf06a..d76c6e01fe 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -192,7 +192,7 @@ 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`_. +`GitHub issues page `_ . How do I contribute? From 7d493930e6dda53892cd1db189663d7ef20e5c83 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 12:13:15 -0700 Subject: [PATCH 048/100] add get_absoluteairmass --- pvlib/location.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 5f4fdc49c3..b5fe6170b8 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -13,6 +13,7 @@ from pvlib import solarposition from pvlib import clearsky +from pvlib import atmosphere class Location(object): @@ -159,25 +160,45 @@ def get_solarposition(self, times, **kwargs): **kwargs) - def get_clearsky(self, times, **kwargs): + def get_clearsky(self, times, model='ineichen', **kwargs): """ - Uses the :func:`clearsky.ineichen` function to calculate - the clear sky estimates of GHI, DNI, and DHI at this location. + Calculate the clear sky estimates of GHI, DNI, and/or DHI + at this location. Parameters ---------- times : DatetimeIndex - kwargs passed to :func:`clearsky.ineichen` + model : str + The clear sky model to use. + + kwargs passed to the relevant function(s). Returns ------- - clearsky : DataFrame + clearsky : Series or DataFrame Column names are: ``ghi, dni, dhi``. """ - return clearsky.ineichen(times, latitude=self.latitude, - longitude=self.longitude, - altitude=self.altitude, - **kwargs) + + 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('%s is not a valid clear sky model', model) + + return cs + + + def get_absoluteairmass(self, airmass_relative): + + pressure = atmosphere.alt2pres(self.altitude) + am = atmosphere.absoluteairmass(airmass_relative, pressure) + + return am \ No newline at end of file From c541abd4957fb94ff04fc171da2f8185ceeb5051 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 12:14:30 -0700 Subject: [PATCH 049/100] add get_aoi, dni/am calcs --- pvlib/pvsystem.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 4db9d6b695..0880061c29 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -19,7 +19,7 @@ from pvlib import tools from pvlib.location import Location -from pvlib import irradiance +from pvlib import irradiance, atmosphere # not sure if this belongs in the pvsystem module. @@ -99,7 +99,7 @@ def __init__(self, module=None, module_parameters=None, series_modules=None, parallel_modules=None, inverter=None, inverter_parameters=None, - racking_model=None, + racking_model='open_rack_cell_glassback', **kwargs): self.surface_tilt = surface_tilt @@ -128,6 +128,27 @@ def __init__(self, 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='isotropic', **kwargs): @@ -166,6 +187,14 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, 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, From f46ef23385558cc41341eb61af33f5ab703c7ba5 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 12:14:43 -0700 Subject: [PATCH 050/100] run_model finally works --- pvlib/modelchain.py | 97 +++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 1af300d99b..bbb7d5b97b 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -2,70 +2,83 @@ Stub documentation for the module. """ +from pvlib import atmosphere + class ModelChain(object): """ A class that represents all of the modeling steps necessary for calculating power or energy for a PV system at a given location. - + Consider an abstract base class. - + Parameters ---------- system : PVSystem The connected set of modules, inverters, etc. - + location : location The physical location at which to evaluate the model. - + times : DatetimeIndex Times at which to evaluate the model. - - orientation_strategy : str + + orientation_strategy : None or str The strategy for aligning the modules. - - clearsky_method : str + If not None, sets the ``surface_azimuth`` and ``surface_tilt`` + properties of the ``system``. + + 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. - + **kwargs Arbitrary keyword arguments. Included for compatibility, but not used. - + See also -------- location.Location pvsystem.PVSystem """ - def __init__(self, system, location, times, + def __init__(self, system, location, orientation_strategy='south_at_latitude', - clearsky_method='ineichen', + clearsky_model='ineichen', + transposition_model='haydavies', solar_position_method='nrel_numpy', + airmass_model='kastenyoung1989', **kwargs): self.system = system self.location = location - self.times = times - self.clearsky_method = clearsky_method + self.clearsky_model = clearsky_model + self.transposition_model = transposition_model self.solar_position_method = solar_position_method + self.airmass_model = airmass_model - self._orientation_strategy = orientation_strategy + # calls setter + self.orientation_strategy = orientation_strategy @property def orientation_strategy(self): return self._orientation_strategy - @property.setter + @orientation_strategy.setter def orientation_strategy(self, strategy): - if strategy == 'south_at_latitude': - self.surface_azimuth = 180 - self.surface_tilt = self.location.latitude + if strategy is None or strategy == 'None': + pass + elif strategy == 'south_at_latitude': + self.system.surface_azimuth = 180 + self.system.surface_tilt = self.location.latitude elif strategy == 'flat': - self.surface_azimuth = 0 - self.surface_tilt = 0 + self.system.surface_azimuth = 0 + self.system.surface_tilt = 0 else: raise ValueError('invalid orientation strategy. strategy must ' + 'be one of south_at_latitude, flat,') @@ -73,20 +86,46 @@ def orientation_strategy(self, strategy): self._orientation_strategy = strategy - def run_model(self): + def run_model(self, times): """ Run the model. + Problems: + + * assumes clear sky + * assumes 0 m/s wind and 20 C temp. + * only works with SAPM + Returns ------- output : DataFrame - Column names??? + Some combination of AC power, DC power, POA irrad, etc. """ - solar_position = self.location.get_solarposition(self.times) - clearsky = self.system.get_clearsky(solar_position, - self.clearsky_method) - final_output = self.system.calculate_final_yield(args) - return final_output + solar_position = self.location.get_solarposition(times) + clearsky = self.location.get_clearsky(solar_position.index, + self.clearsky_model) + irradiance = self.system.get_irradiance(solar_position['apparent_zenith'], + solar_position['azimuth'], + clearsky['dni'], + clearsky['ghi'], + clearsky['dhi'], + model=self.transposition_model) + temps = self.system.sapm_celltemp(irradiance['poa_global'], 0, 20) + + aoi = self.system.get_aoi(solar_position['apparent_zenith'], + solar_position['azimuth']) + + am_rel = atmosphere.relativeairmass(solar_position['apparent_zenith'], + self.airmass_model) + am_abs = self.location.get_absoluteairmass(am_rel) + + dc = self.system.sapm(irradiance['poa_direct'], + irradiance['poa_diffuse'], temps['temp_cell'], + am_abs, aoi) + + ac = self.system.snlinverter(dc['v_mp'], dc['p_mp']) + + return dc, ac def model_system(self): From 0d5bf83d67f4aff3d77d22aa5081a6cc875ef631 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 12:19:12 -0700 Subject: [PATCH 051/100] update mc part of overview --- docs/sphinx/source/package_overview.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index cbc3edf06a..bc47c78850 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -129,20 +129,25 @@ objects to accomplish our system modeling goal: from pvlib.location import Location from pvlib.modelchain import ModelChain - system = PVSystem(module, inverter, **other_params) + system = PVSystem(module_parameters=module, + inverter_parameters=inverter) energies = {} for latitude, longitude, name in coordinates: location = Location(latitude, longitude) # not yet clear what, exactly, goes into ModelChain(s) - mc = ModelChain(system, location, times, - 'south_at_latitude', **other_modelchain_params) - output = mc.run_model() - annual_energy = output['power'].sum() + mc = ModelChain(system, location, + orientation_strategy='south_at_latitude') + dc, ac = mc.run_model(times) + annual_energy = ac.sum() energies[name] = annual_energy - #energies = pd.DataFrame(energies) - #energies.plot() + # 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) From 7a863fc77a08e00efd7da245a9d26f61e8958992 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 14:35:16 -0700 Subject: [PATCH 052/100] more doc improvements --- docs/sphinx/source/package_overview.rst | 34 +++++++++++++++++++------ 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index bc47c78850..230f671ccc 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -142,6 +142,8 @@ objects to accomplish our system modeling goal: 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)) @@ -167,24 +169,40 @@ object to accomplish our modeling goal: from pvlib.pvsystem import PVSystem, LocalizedPVSystem - module = - inverter = other_system_params = {} # sometime helpful to break apart - base_system = PVSystem(module, inverter, **other_system_params) + base_system = PVSystem(module_parameters=module, + inverter_parameters=inverter) energies = {} for latitude, longitude, name in coordinates: localized_system = base_system.localize(latitude, longitude, name=name) localized_system.surface_tilt = latitude + localized_system.surface_azimuth = 0 cs = localized_system.get_clearsky(times) - solpos = localized_system.get_solarposition(times) + solar_position = localized_system.get_solarposition(times) total_irrad = localized_system.get_irradiance(times, **solpos, **cs) - power = localized_system.get_power(stuff) - annual_energy = power.sum() + temps = localized_system.sapm_celltemp(total_irrad['poa_global'], 0, 20) + aoi = localized_system.get_aoi(solar_position['apparent_zenith'], + solar_position['azimuth']) + am_rel = atmosphere.relativeairmass(solar_position['apparent_zenith']) + am_abs = localized_system.get_absoluteairmass(am_rel) + dc = localized_system.sapm(total_irrad['poa_direct'], + total_irrad['poa_diffuse'], + temps['temp_cell'], + am_abs, aoi) + ac = localized_system.snlinverter(dc['v_mp'], dc['p_mp']) + + annual_energy = ac.sum() energies[name] = annual_energy - #energies = pd.DataFrame(energies) - #energies.plot() + 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 From 2bbc61889356b9adf40807c95f0663ab1fe2ccc0 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 14:39:38 -0700 Subject: [PATCH 053/100] more doc improvements --- docs/sphinx/source/package_overview.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index 230f671ccc..a7bbd68b28 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -171,16 +171,21 @@ object to accomplish our modeling goal: other_system_params = {} # sometime helpful to break apart base_system = PVSystem(module_parameters=module, - inverter_parameters=inverter) + inverter_parameters=inverter, + **other_system_params) energies = {} for latitude, longitude, name in coordinates: localized_system = base_system.localize(latitude, longitude, name=name) localized_system.surface_tilt = latitude localized_system.surface_azimuth = 0 - cs = localized_system.get_clearsky(times) + clearsky = localized_system.get_clearsky(times) solar_position = localized_system.get_solarposition(times) - total_irrad = localized_system.get_irradiance(times, **solpos, **cs) + 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'], 0, 20) aoi = localized_system.get_aoi(solar_position['apparent_zenith'], solar_position['azimuth']) From 0f362340c178a25487e390b105638596728b8bbe Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 15:25:54 -0700 Subject: [PATCH 054/100] create PVSystem.localize, allow LocalizedPVSystem pvsystem and location kwargs --- pvlib/location.py | 5 ++--- pvlib/pvsystem.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index b5fe6170b8..4b7d2660a5 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -62,8 +62,6 @@ class Location(object): def __init__(self, latitude, longitude, tz='UTC', altitude=0, name=None, **kwargs): - - pvl_logger.debug('creating Location object') self.latitude = latitude self.longitude = longitude @@ -82,7 +80,8 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0, self.name = name # needed for tying together Location and PVSystem in LocalizedPVSystem - super(Location, self).__init__(**kwargs) + # if LocalizedPVSystem signature is reversed + # super(Location, self).__init__(**kwargs) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 0880061c29..708ab283c7 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -375,6 +375,30 @@ def snlinverter(self, v_dc, p_dc): 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 @@ -385,8 +409,26 @@ class LocalizedPVSystem(PVSystem, Location): See the :class:`PVSystem` class for an object model that describes an unlocalized PV system. """ - def __init__(self, **kwargs): - super(LocalizedPVSystem, self).__init__(**kwargs) + 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, From 67744b09303c9c1249f598f750adc69baada5865 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 15:26:47 -0700 Subject: [PATCH 055/100] update docs with LocalizedPVSystem improvements --- docs/sphinx/source/package_overview.rst | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index a7bbd68b28..79a5936a39 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -39,10 +39,10 @@ configuration at a handful of sites listed below. times = pd.DatetimeIndex(start='2015', end='2016', freq='1h') # very approximate - coordinates = [(30, -110, 'Tucson'), - (35, -105, 'Albuquerque'), - (40, -120, 'San Francisco'), - (50, 10, 'Berlin')] + coordinates = [(30, -110, 'Tucson', 700), + (35, -105, 'Albuquerque', 1500), + (40, -120, 'San Francisco', 10), + (50, 10, 'Berlin', 34)] import pvlib @@ -67,7 +67,7 @@ to accomplish our system modeling goal: 'surface_azimuth': 180} energies = {} - for latitude, longitude, name in coordinates: + for latitude, longitude, name, altitude in coordinates: system['surface_tilt'] = latitude cs = pvlib.clearsky.ineichen(times, latitude, longitude) solpos = pvlib.solarposition.get_solarposition(times, latitude, longitude) @@ -133,8 +133,8 @@ objects to accomplish our system modeling goal: inverter_parameters=inverter) energies = {} - for latitude, longitude, name in coordinates: - location = Location(latitude, longitude) + for latitude, longitude, name, altitude in coordinates: + location = Location(latitude, longitude, name=name, altitude=altitude) # not yet clear what, exactly, goes into ModelChain(s) mc = ModelChain(system, location, orientation_strategy='south_at_latitude') @@ -175,8 +175,11 @@ object to accomplish our modeling goal: **other_system_params) energies = {} - for latitude, longitude, name in coordinates: - localized_system = base_system.localize(latitude, longitude, name=name) + for latitude, longitude, name, altitude in coordinates: + localized_system = base_system.localize(latitude=latitude, + longitude=longitude, + name=name, + altitude=altitude) localized_system.surface_tilt = latitude localized_system.surface_azimuth = 0 clearsky = localized_system.get_clearsky(times) From 64a8fecc6ab490cff920f1c895f196e8f54accba Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 15:30:15 -0700 Subject: [PATCH 056/100] fix small typos --- docs/sphinx/source/package_overview.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index 79a5936a39..b43b1cef94 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -192,7 +192,7 @@ object to accomplish our modeling goal: temps = localized_system.sapm_celltemp(total_irrad['poa_global'], 0, 20) aoi = localized_system.get_aoi(solar_position['apparent_zenith'], solar_position['azimuth']) - am_rel = atmosphere.relativeairmass(solar_position['apparent_zenith']) + am_rel = pvlib.atmosphere.relativeairmass(solar_position['apparent_zenith']) am_abs = localized_system.get_absoluteairmass(am_rel) dc = localized_system.sapm(total_irrad['poa_direct'], total_irrad['poa_diffuse'], @@ -223,7 +223,7 @@ 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`_. +`GitHub issues page `_. How do I contribute? From 176495cbd644acfabefc5a7559e3ee147eb9a3f3 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 15:35:49 -0700 Subject: [PATCH 057/100] fix small typos --- docs/sphinx/source/package_overview.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index b43b1cef94..e5c7a46d0c 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -199,7 +199,6 @@ object to accomplish our modeling goal: temps['temp_cell'], am_abs, aoi) ac = localized_system.snlinverter(dc['v_mp'], dc['p_mp']) - annual_energy = ac.sum() energies[name] = annual_energy @@ -223,7 +222,7 @@ 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 `_. +`GitHub issues page `_ . How do I contribute? From d030bb2b9b63f65126ed32eb646a1c564d96bc12 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 15:38:55 -0700 Subject: [PATCH 058/100] try short sphinx class refs --- docs/sphinx/source/package_overview.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index e5c7a46d0c..739c9fcd05 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -118,9 +118,9 @@ It can also be helpful if you make extensive use of Location-specific methods for other calculations. The following code demonstrates how to use -:class:`Location `, -:class:`PVSystem `, and -:class:`ModelChain ` +: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 d8b99682ac7741b8c05b7b7b5bc8ff532d89a9a2 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 15:47:50 -0700 Subject: [PATCH 059/100] trying to track down model differences --- docs/sphinx/source/package_overview.rst | 16 +++++++++------- pvlib/pvsystem.py | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index 739c9fcd05..4e5e86a2da 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -74,6 +74,8 @@ to accomplish our system modeling goal: 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'], @@ -86,7 +88,7 @@ to accomplish our system modeling goal: temps = pvlib.pvsystem.sapm_celltemp(total_irrad['poa_global'], 0, 20) dc = pvlib.pvsystem.sapm(module, total_irrad['poa_direct'], total_irrad['poa_diffuse'], temps['temp_cell'], - airmass, aoi) + am_abs, aoi) ac = pvlib.pvsystem.snlinverter(inverter, dc['v_mp'], dc['p_mp']) annual_energy = ac.sum() energies[name] = annual_energy @@ -105,11 +107,11 @@ Object oriented (Location, PVSystem, ModelChain) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The first object oriented paradigm uses a model where -a :class:`PVSystem ` object represents an +a :py:class:`~pvlib.pvsystem.PVSystem` object represents an assembled collection of modules, inverters, etc., -a :class:`Location ` object represents a +a :py:class:`~pvlib.location.Location` object represents a particular place on the planet, -and a :class:`ModelChain ` object describes +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 @@ -156,20 +158,20 @@ Object oriented (LocalizedPVSystem) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The second object oriented paradigm uses a model where a -:class:`LocalizedPVSystem ` represents 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 -:class:`LocalizedPVSystem ` +:py:class:`~pvlib.pvsystem.LocalizedPVSystem` object to accomplish our modeling goal: .. ipython:: python from pvlib.pvsystem import PVSystem, LocalizedPVSystem - other_system_params = {} # sometime helpful to break apart + other_system_params = {} # sometimes helpful to break apart base_system = PVSystem(module_parameters=module, inverter_parameters=inverter, **other_system_params) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 708ab283c7..4945cc8e72 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -150,7 +150,7 @@ def get_aoi(self, solar_zenith, solar_azimuth): def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, - dni_extra=None, airmass=None, model='isotropic', + dni_extra=None, airmass=None, model='haydavies', **kwargs): """ Uses the :func:`irradiance.total_irrad` function to calculate From 55fa894a0e99f4f2cd9d77bd4a8a9830a53808f9 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 15:48:52 -0700 Subject: [PATCH 060/100] fix azimuth prob in docs --- docs/sphinx/source/package_overview.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index 4e5e86a2da..959e35066d 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -183,7 +183,7 @@ object to accomplish our modeling goal: name=name, altitude=altitude) localized_system.surface_tilt = latitude - localized_system.surface_azimuth = 0 + localized_system.surface_azimuth = 180 clearsky = localized_system.get_clearsky(times) solar_position = localized_system.get_solarposition(times) total_irrad = localized_system.get_irradiance(solar_position['apparent_zenith'], From 9901538bc68a6c925d11aba0d8656c0f534e012e Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 15:57:30 -0700 Subject: [PATCH 061/100] maybe fixed discrepancies... --- docs/sphinx/source/package_overview.rst | 27 +++++++++++-------------- pvlib/modelchain.py | 4 ++-- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index 959e35066d..efb2c04166 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -69,7 +69,7 @@ to accomplish our system modeling goal: energies = {} for latitude, longitude, name, altitude in coordinates: system['surface_tilt'] = latitude - cs = pvlib.clearsky.ineichen(times, latitude, longitude) + 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) @@ -137,9 +137,9 @@ objects to accomplish our system modeling goal: energies = {} for latitude, longitude, name, altitude in coordinates: location = Location(latitude, longitude, name=name, altitude=altitude) - # not yet clear what, exactly, goes into ModelChain(s) + # very experimental mc = ModelChain(system, location, - orientation_strategy='south_at_latitude') + orientation_strategy='south_at_latitude_tilt') dc, ac = mc.run_model(times) annual_energy = ac.sum() energies[name] = annual_energy @@ -169,21 +169,18 @@ object to accomplish our modeling goal: .. ipython:: python - from pvlib.pvsystem import PVSystem, LocalizedPVSystem - - other_system_params = {} # sometimes helpful to break apart - base_system = PVSystem(module_parameters=module, - inverter_parameters=inverter, - **other_system_params) + from pvlib.pvsystem import LocalizedPVSystem energies = {} for latitude, longitude, name, altitude in coordinates: - localized_system = base_system.localize(latitude=latitude, - longitude=longitude, - name=name, - altitude=altitude) - localized_system.surface_tilt = latitude - localized_system.surface_azimuth = 180 + 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'], diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index bbb7d5b97b..e44e9ca507 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -47,7 +47,7 @@ class ModelChain(object): """ def __init__(self, system, location, - orientation_strategy='south_at_latitude', + orientation_strategy='south_at_latitude_tilt', clearsky_model='ineichen', transposition_model='haydavies', solar_position_method='nrel_numpy', @@ -73,7 +73,7 @@ def orientation_strategy(self): def orientation_strategy(self, strategy): if strategy is None or strategy == 'None': pass - elif strategy == 'south_at_latitude': + elif strategy == 'south_at_latitude_tilt': self.system.surface_azimuth = 180 self.system.surface_tilt = self.location.latitude elif strategy == 'flat': From a2fc762ced33c668047a0c20ec5d201273b4572d Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 16:16:50 -0700 Subject: [PATCH 062/100] add irradiance and weather kwargs to run_model --- docs/sphinx/source/package_overview.rst | 15 +++++++- pvlib/modelchain.py | 51 ++++++++++++++++--------- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index efb2c04166..3d54fb8c29 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -33,12 +33,15 @@ configuration at a handful of sites listed below. 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), @@ -46,10 +49,15 @@ configuration at a handful of sites listed below. 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 + temp_air = 20 + wind_speed = 0 Procedural @@ -85,7 +93,8 @@ to accomplish our system modeling goal: cs['dni'], cs['ghi'], cs['dhi'], dni_extra=dni_extra, model='haydavies') - temps = pvlib.pvsystem.sapm_celltemp(total_irrad['poa_global'], 0, 20) + 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) @@ -140,6 +149,7 @@ objects to accomplish our system modeling goal: # very experimental mc = ModelChain(system, location, orientation_strategy='south_at_latitude_tilt') + # optional parameters for irradiance and weather data dc, ac = mc.run_model(times) annual_energy = ac.sum() energies[name] = annual_energy @@ -188,7 +198,8 @@ object to accomplish our modeling goal: clearsky['dni'], clearsky['ghi'], clearsky['dhi']) - temps = localized_system.sapm_celltemp(total_irrad['poa_global'], 0, 20) + 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']) am_rel = pvlib.atmosphere.relativeairmass(solar_position['apparent_zenith']) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index e44e9ca507..2e35a5a376 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -86,31 +86,45 @@ def orientation_strategy(self, strategy): self._orientation_strategy = strategy - def run_model(self, times): + def run_model(self, times=None, irradiance=None, weather=None): """ Run the model. - Problems: - - * assumes clear sky - * assumes 0 m/s wind and 20 C temp. - * only works with SAPM - + Parameters + ---------- + times : None or DatetimeIndex + 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 ------- output : DataFrame Some combination of AC power, DC power, POA irrad, etc. """ solar_position = self.location.get_solarposition(times) - clearsky = self.location.get_clearsky(solar_position.index, - self.clearsky_model) - irradiance = self.system.get_irradiance(solar_position['apparent_zenith'], - solar_position['azimuth'], - clearsky['dni'], - clearsky['ghi'], - clearsky['dhi'], - model=self.transposition_model) - temps = self.system.sapm_celltemp(irradiance['poa_global'], 0, 20) + + if irradiance is None: + irradiance = self.location.get_clearsky(solar_position.index, + self.clearsky_model) + + total_irrad = self.system.get_irradiance(solar_position['apparent_zenith'], + solar_position['azimuth'], + irradiance['dni'], + irradiance['ghi'], + irradiance['dhi'], + model=self.transposition_model) + + if weather is None: + weather = {'wind_speed': 0, 'temp_air': 20} + + temps = self.system.sapm_celltemp(total_irrad['poa_global'], + weather['wind_speed'], + weather['temp_air']) aoi = self.system.get_aoi(solar_position['apparent_zenith'], solar_position['azimuth']) @@ -119,8 +133,9 @@ def run_model(self, times): self.airmass_model) am_abs = self.location.get_absoluteairmass(am_rel) - dc = self.system.sapm(irradiance['poa_direct'], - irradiance['poa_diffuse'], temps['temp_cell'], + dc = self.system.sapm(total_irrad['poa_direct'], + total_irrad['poa_diffuse'], + temps['temp_cell'], am_abs, aoi) ac = self.system.snlinverter(dc['v_mp'], dc['p_mp']) From fe5d021bcf8f12c921a49bc556ac28bafd912833 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 21:42:03 -0700 Subject: [PATCH 063/100] clean up merge cruft --- docs/sphinx/source/index.rst | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index 9d2d02c65c..dae85e27b8 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -61,16 +61,11 @@ Contents self package_overview + whatsnew modules classes comparison_pvlib_matlab -<<<<<<< HEAD - whatsnew -======= variables_style_rules - pvlib - ->>>>>>> pvlib/master Indices and tables From 3f96b89205ba255a6bf390ae21176a5a13dcb87d Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 21:42:56 -0700 Subject: [PATCH 064/100] convert get_airmassabsolute to get_airmass. add a bunch of failing stub tests --- docs/sphinx/source/package_overview.rst | 6 +-- pvlib/location.py | 52 ++++++++++++++++++++++--- pvlib/modelchain.py | 22 +++++------ pvlib/test/test_location.py | 15 +++++-- pvlib/test/test_modelchain.py | 24 ++++++++++++ pvlib/test/test_pvsystem.py | 26 ++++++++++++- 6 files changed, 122 insertions(+), 23 deletions(-) create mode 100644 pvlib/test/test_modelchain.py diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index 3d54fb8c29..98ec244ed3 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -202,12 +202,12 @@ object to accomplish our modeling goal: wind_speed, temp_air) aoi = localized_system.get_aoi(solar_position['apparent_zenith'], solar_position['azimuth']) - am_rel = pvlib.atmosphere.relativeairmass(solar_position['apparent_zenith']) - am_abs = localized_system.get_absoluteairmass(am_rel) + airmass = localized_system.get_airmass(solar_position=solar_position) dc = localized_system.sapm(total_irrad['poa_direct'], total_irrad['poa_diffuse'], temps['temp_cell'], - am_abs, aoi) + airmass['airmass_absolute'], + aoi) ac = localized_system.snlinverter(dc['v_mp'], dc['p_mp']) annual_energy = ac.sum() energies[name] = annual_energy diff --git a/pvlib/location.py b/pvlib/location.py index 4b7d2660a5..b8232abc55 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -191,13 +191,55 @@ def get_clearsky(self, times, model='ineichen', **kwargs): raise ValueError('%s is not a valid clear sky model', model) return cs + + + def get_airmass(self, times=None, solar_position=None, + model='kastenyoung1998'): + """ + Calculate the relative and absolute airmass. + + Function will calculate the solar zenith and apparent zenith angles, + and choose the appropriate one. + + 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) + + apparents = ['simple', 'kasten1966', 'kastenyoung1989', + 'gueymard1993', 'pickering2002'] + + trues = ['youngirvine1967', 'young1994'] + + if model in apparents: + zenith = solar_position['apparent_zenith'] + elif model in trues: + zenith = solar_position['zenith'] + else + raise ValueError('invalid model %s', model) + + airmass_relative = atmosphere.relativeairmass(zenith, model) - def get_absoluteairmass(self, airmass_relative): - pressure = atmosphere.alt2pres(self.altitude) - am = atmosphere.absoluteairmass(airmass_relative, pressure) - - return am + 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 index 2e35a5a376..8d39ce4566 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -2,8 +2,6 @@ Stub documentation for the module. """ -from pvlib import atmosphere - class ModelChain(object): """ A class that represents all of the modeling steps necessary for @@ -108,9 +106,14 @@ def run_model(self, times=None, irradiance=None, weather=None): """ solar_position = self.location.get_solarposition(times) + airmass = self.location.get_airmass(solar_position=solar_position, + model=self.airmass_model) + if irradiance is None: irradiance = self.location.get_clearsky(solar_position.index, - self.clearsky_model) + self.clearsky_model, + zenith_data=solar_position['apparent_zenith'], + airmass_data=airmass['airmass_absolute']) total_irrad = self.system.get_irradiance(solar_position['apparent_zenith'], solar_position['azimuth'], @@ -118,30 +121,27 @@ def run_model(self, times=None, irradiance=None, weather=None): irradiance['ghi'], irradiance['dhi'], model=self.transposition_model) - + if weather is None: weather = {'wind_speed': 0, 'temp_air': 20} temps = self.system.sapm_celltemp(total_irrad['poa_global'], weather['wind_speed'], weather['temp_air']) - + aoi = self.system.get_aoi(solar_position['apparent_zenith'], solar_position['azimuth']) - am_rel = atmosphere.relativeairmass(solar_position['apparent_zenith'], - self.airmass_model) - am_abs = self.location.get_absoluteairmass(am_rel) - dc = self.system.sapm(total_irrad['poa_direct'], total_irrad['poa_diffuse'], temps['temp_cell'], - am_abs, aoi) + airmass['airmass_absolute'], + aoi) ac = self.system.snlinverter(dc['v_mp'], dc['p_mp']) return dc, ac - + def model_system(self): """ diff --git a/pvlib/test/test_location.py b/pvlib/test/test_location.py index 9cf24540e2..5097e30623 100644 --- a/pvlib/test/test_location.py +++ b/pvlib/test/test_location.py @@ -1,6 +1,3 @@ -import logging -pvl_logger = logging.getLogger('pvlib') - import pytz from nose.tools import raises from pytz.exceptions import UnknownTimeZoneError @@ -35,3 +32,15 @@ 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(): + raise Exception('test me') + +def test_from_tmy(): + raise Exception('test me') + +def test_get_solarposition(): + raise Exception('test me') + +def test_get_airmass(): + raise Exception('test me') \ No newline at end of file diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py new file mode 100644 index 0000000000..4977c777ed --- /dev/null +++ b/pvlib/test/test_modelchain.py @@ -0,0 +1,24 @@ + + +from pvlib import modelchain + + +def test_ModelChain_creation(): + raise Exception('test me') + + +def test_orientation_strategy(): + # test for None, 'None', 'south_at_latitude_tilt', 'flat', ValueError + raise Exception('test me') + + +def test_run_model(): + raise Exception('test me') + + +def test_run_model_with_irradiance(): + raise Exception('test me') + + +def test_run_model_with_weather(): + raise Exception('test me') \ No newline at end of file diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 3dd94b7895..7a1fe299ff 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -222,8 +222,32 @@ def test_PVSystem_creation(): pv_system = pvsystem.PVSystem(module='blah', inverter='blarg') +def test_PVSystem_get_aoi(): + raise Exception('test me') + + +def test_PVSystem_get_irradiance(): + raise Exception('test me') + + +def test_PVSystem_localize_with_location(): + raise Exception('test me') + + +def test_PVSystem_localize_with_latlon(): + raise Exception('test me') + + +# in principle, we should be retesting each of the models tested above +# when they are attached to PVSystem + + def test_LocalizedPVSystem_creation(): localized_pv_system = pvsystem.LocalizedPVSystem(latitude=30, longitude=-110, module='blah', - inverter='blarg') \ No newline at end of file + inverter='blarg') + + +def test_LocalizedPVSystem_do_stuff(): + raise Exception('test me') From 63da5e6962e00f3f65cc51f01f445111641a9617 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 22:00:38 -0700 Subject: [PATCH 065/100] fix silly syntax error --- pvlib/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/location.py b/pvlib/location.py index b8232abc55..98a23cfa72 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -228,7 +228,7 @@ def get_airmass(self, times=None, solar_position=None, zenith = solar_position['apparent_zenith'] elif model in trues: zenith = solar_position['zenith'] - else + else: raise ValueError('invalid model %s', model) airmass_relative = atmosphere.relativeairmass(zenith, model) From 9d8c37432a4ba7b494c721ba2280c59168d031f9 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 22:08:55 -0700 Subject: [PATCH 066/100] fix pd import, default am model, ValueError text --- pvlib/location.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 98a23cfa72..1377bc62aa 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -9,6 +9,7 @@ import datetime +import pandas as pd import pytz from pvlib import solarposition @@ -188,13 +189,13 @@ def get_clearsky(self, times, model='ineichen', **kwargs): solpos = self.get_solarposition(times, **kwargs) cs = clearsky.haurwitz(solpos['apparent_zenith']) else: - raise ValueError('%s is not a valid clear sky model', model) + raise ValueError('{} is not a valid clear sky model'.format(model)) return cs def get_airmass(self, times=None, solar_position=None, - model='kastenyoung1998'): + model='kastenyoung1989'): """ Calculate the relative and absolute airmass. @@ -229,7 +230,7 @@ def get_airmass(self, times=None, solar_position=None, elif model in trues: zenith = solar_position['zenith'] else: - raise ValueError('invalid model %s', model) + raise ValueError('{} is not a valid airmass model'.format(model)) airmass_relative = atmosphere.relativeairmass(zenith, model) From 8aa8d53bb08e50b9182cde33ce32fc585e153e55 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 30 Jan 2016 22:34:23 -0700 Subject: [PATCH 067/100] editing docs --- docs/sphinx/source/package_overview.rst | 9 +++++---- pvlib/pvsystem.py | 14 ++++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index 98ec244ed3..d2e79b91e5 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -20,10 +20,11 @@ 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, -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. +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 diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index d59581a2df..18e5b0ef70 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -30,10 +30,12 @@ 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 ``Location`` and ``ModelChain`` + It is typically used in combination with + :py:class:`~pvlib.location.Location` and + :py:class:`~pvlib.modelchain.ModelChain` objects. - See the :class:`LocalizedPVSystem` class for an object model that + See the :py:class:`LocalizedPVSystem` class for an object model that describes an installed PV system. The class is complementary @@ -88,9 +90,9 @@ class PVSystem(object): See also -------- - location.Location - tracking.SingleAxisTracker - pvsystem.LocalizedPVSystem + :py:class:`~pvlib.location.Location` + :py:class:`~pvlib.tracking.SingleAxisTracker` + :py:class:`~pvlib.pvsystem.LocalizedPVSystem` """ def __init__(self, @@ -406,7 +408,7 @@ class LocalizedPVSystem(PVSystem, Location): This class combines the attributes and methods of the PVSystem and Location classes. - See the :class:`PVSystem` class for an object model that + See the :py:class:`PVSystem` class for an object model that describes an unlocalized PV system. """ def __init__(self, pvsystem=None, location=None, **kwargs): From 862e0f04259e2291b46f2aa672b50fb8d98b4835 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 31 Jan 2016 10:59:05 -0700 Subject: [PATCH 068/100] editing docs --- docs/sphinx/source/package_overview.rst | 2 +- pvlib/atmosphere.py | 10 +++++----- pvlib/location.py | 11 ++++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index d2e79b91e5..5a0fe9409d 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -56,7 +56,7 @@ configuration at a handful of sites listed below. 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 + # specify constant ambient air temp and wind for simplicity temp_air = 20 wind_speed = 0 diff --git a/pvlib/atmosphere.py b/pvlib/atmosphere.py index 0ba96bf855..7000e1cb66 100644 --- a/pvlib/atmosphere.py +++ b/pvlib/atmosphere.py @@ -143,12 +143,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/location.py b/pvlib/location.py index 1377bc62aa..106b24212f 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -189,7 +189,8 @@ def get_clearsky(self, times, model='ineichen', **kwargs): solpos = self.get_solarposition(times, **kwargs) cs = clearsky.haurwitz(solpos['apparent_zenith']) else: - raise ValueError('{} is not a valid clear sky model'.format(model)) + raise ValueError('{} is not a valid clear sky model' + .format(model)) return cs @@ -199,9 +200,9 @@ def get_airmass(self, times=None, solar_position=None, """ Calculate the relative and absolute airmass. - Function will calculate the solar zenith and apparent zenith angles, - and choose the appropriate one. - + Automatically chooses zenith or apparant zenith + depending on the selected model. + Parameters ---------- times : None or DatetimeIndex @@ -243,4 +244,4 @@ def get_airmass(self, times=None, solar_position=None, airmass['airmass_absolute'] = airmass_absolute return airmass - \ No newline at end of file + \ No newline at end of file From aeb02e89f91fdd168823f4ab217344c4f06ee7b1 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 31 Jan 2016 12:46:16 -0700 Subject: [PATCH 069/100] fix tmy2 metadata label problem --- pvlib/tmy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 ============= ================================== ============================ ========================================================================================================================================================================== From 28ed81268c76b69c3fe30c656a8a7c3122d2c3d5 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 31 Jan 2016 12:47:44 -0700 Subject: [PATCH 070/100] break AIRMASS_MODELS into *_ZENITH_MODELS --- pvlib/atmosphere.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pvlib/atmosphere.py b/pvlib/atmosphere.py index 7000e1cb66..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): ''' From cf1c1f3b5bca247420001264f686623b32ca33e0 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 31 Jan 2016 12:48:36 -0700 Subject: [PATCH 071/100] small clean ups --- pvlib/test/test_solarposition.py | 10 ++++++---- pvlib/test/test_tmy.py | 3 --- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pvlib/test/test_solarposition.py b/pvlib/test/test_solarposition.py index c7b75e55b6..db73c605c6 100644 --- a/pvlib/test/test_solarposition.py +++ b/pvlib/test/test_solarposition.py @@ -56,11 +56,12 @@ def test_spa_c_physical(): 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) + times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), + periods=1, freq='D', tz=golden.tz) try: ephem_data = solarposition.spa_c(times, golden.latitude, - golden.longitude, pressure=82000, + golden.longitude, + pressure=82000, temperature=11) except ImportError: raise SkipTest @@ -87,7 +88,8 @@ def test_spa_python_numpy_physical_dst(): 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, + golden.longitude, + pressure=82000, temperature=11, delta_t=67, atmos_refract=0.5667, how='numpy') 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 From 3391abfd216911e09b177aa8207ca533ce5da22c Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 31 Jan 2016 12:49:26 -0700 Subject: [PATCH 072/100] add pressure, temperature to get_solarposition. make from_tmy work --- pvlib/location.py | 55 +++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 106b24212f..58983b5229 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -40,11 +40,12 @@ class Location(object): 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. @@ -73,6 +74,9 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0, 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') @@ -114,21 +118,21 @@ def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): # 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('StationName', False) + tmy2 = tmy_metadata.get('City', False) latitude = tmy_metadata['latitude'] longitude = tmy_metadata['longitude'] if tmy2: - altitude = tmy_metadata['SiteElevation'] - name = tmy_metadata['StationName'] - tz = tmy_metadata['SiteTimeZone'] - else: - altitude = tmy_metadata['alititude'] + name = tmy_metadata['City'] + else: name = tmy_metadata['Name'] - tz = tmy_metadata['TZ'] - - new_object = cls(latitude, longitude, tz, altitude, name, **kwargs) + + 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: @@ -137,26 +141,36 @@ def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): return new_object - def get_solarposition(self, times, **kwargs): + def get_solarposition(self, times, pressure=None, temperature=12, + **kwargs): """ - Uses the :func:`solarposition.get_solarposition` function + Uses the :py:func:`solarposition.get_solarposition` function to calculate the solar zenith, azimuth, etc. at this location. Parameters ---------- times : DatetimeIndex - - kwargs passed to :func:`solarposition.get_solarposition` + 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 ------- - solarposition : DataFrame + solar_position : DataFrame Columns depend on the ``method`` kwarg, but always include - ``zenith`` and ``azimuth``. + ``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) @@ -221,14 +235,9 @@ def get_airmass(self, times=None, solar_position=None, if solar_position is None: solar_position = self.get_solarposition(times) - apparents = ['simple', 'kasten1966', 'kastenyoung1989', - 'gueymard1993', 'pickering2002'] - - trues = ['youngirvine1967', 'young1994'] - - if model in apparents: + if model in atmosphere.APPARENT_ZENITH_MODELS: zenith = solar_position['apparent_zenith'] - elif model in trues: + elif model in atmosphere.TRUE_ZENITH_MODELS: zenith = solar_position['zenith'] else: raise ValueError('{} is not a valid airmass model'.format(model)) From c547c3c35ded2bd150c1f54a444ab641ba8dcbd4 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 31 Jan 2016 12:49:52 -0700 Subject: [PATCH 073/100] make location tests work --- pvlib/test/test_location.py | 94 ++++++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/pvlib/test/test_location.py b/pvlib/test/test_location.py index 5097e30623..4be6f9ab80 100644 --- a/pvlib/test/test_location.py +++ b/pvlib/test/test_location.py @@ -1,6 +1,13 @@ +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 @@ -18,11 +25,15 @@ def test_location_invalid_tz(): @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' @@ -33,14 +44,85 @@ def test_location_print_pytz(): expected_str = 'Tucson: latitude=32.2, longitude=-111, tz=US/Arizona, altitude=700' assert tus.__str__() == expected_str + def test_get_clearsky(): - raise Exception('test me') + 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.99776128, 763.02009659, 258.93387913], + [ 70.79971557, 957.15432484, 612.08052529], + [ 59.01912609, 879.09965133, 415.32459426], + [ 0. , 0. , 0. ]]), + columns=['dhi', 'dni', 'ghi'], + index=times) + assert_frame_equal(expected, clearsky) + + +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_from_tmy(): - raise Exception('test me') def test_get_solarposition(): - raise Exception('test me') + 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(): - raise Exception('test me') \ No newline at end of file + 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) + \ No newline at end of file From bcb41ae2a1c37267d246b0478afae13f6b84e618 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 31 Jan 2016 13:30:55 -0700 Subject: [PATCH 074/100] update whatsnew. might still be missing a few things --- docs/sphinx/source/whatsnew/v0.3.0.txt | 34 ++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.3.0.txt b/docs/sphinx/source/whatsnew/v0.3.0.txt index 5c19fb2532..6df10c147d 100644 --- a/docs/sphinx/source/whatsnew/v0.3.0.txt +++ b/docs/sphinx/source/whatsnew/v0.3.0.txt @@ -7,19 +7,49 @@ This is a major release from 0.2.2. We recommend that all users upgrade to this version after testing their code for compatibility and updating as necessary. + Enhancements ~~~~~~~~~~~~ -* Adds ``PVSystem`` and ``SingleAxisTracker`` classes. (:issue:`17`) +* Adds ``PVSystem``, ``LocalizedPVSystem``, ``ModelChain``, + and ``SingleAxisTracker`` classes. (:issue:`17`) * Replaces ``location`` arguments with ``latitude``, ``longitude``, ``altitude``, and ``tz`` as appropriate. This separates the object-oriented API from the procedural API. (:issue:`17`) - +* ``Location`` classes can be created from TMY2/TMY3 metadata + using the ``from_tmy`` constructor. +* ``Location`` classes gain the ``get_solarposition``, ``get_clearsky``, + and ``get_airmass`` functions. +* Add Package Overview and Classes pages to documentation. +* 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``. +* Change default ``Location`` timezone to ``'UTC'``. Bug fixes ~~~~~~~~~ +* ``pvsystem.sapm_celltemp`` argument names now follow the + variable conventions. +* ``irradiance.total_irrad`` now follows the variable conventions. + (:issue:`105`) +* Fixed the metadata key specification in documentation of the + readtmy2 function. + Contributors ~~~~~~~~~~~~ From ac6f710e4a5c2f9b4a531aa1ffa92c6a4d8fe835 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 31 Jan 2016 13:42:25 -0700 Subject: [PATCH 075/100] remove sphinx markup from see also numpy doc section --- pvlib/pvsystem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 18e5b0ef70..4209a134d9 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -90,9 +90,9 @@ class PVSystem(object): See also -------- - :py:class:`~pvlib.location.Location` - :py:class:`~pvlib.tracking.SingleAxisTracker` - :py:class:`~pvlib.pvsystem.LocalizedPVSystem` + pvlib.location.Location + pvlib.tracking.SingleAxisTracker + pvlib.pvsystem.LocalizedPVSystem """ def __init__(self, From 49c670d8f3f771d8ff1ae44c12adbb5fc0175201 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 31 Jan 2016 20:16:11 -0700 Subject: [PATCH 076/100] implement modelchain tests --- pvlib/modelchain.py | 6 +-- pvlib/test/test_modelchain.py | 87 ++++++++++++++++++++++++++++++++--- pvlib/test/test_pvsystem.py | 3 -- 3 files changed, 83 insertions(+), 13 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 8d39ce4566..3917efc6df 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -75,7 +75,7 @@ def orientation_strategy(self, strategy): self.system.surface_azimuth = 180 self.system.surface_tilt = self.location.latitude elif strategy == 'flat': - self.system.surface_azimuth = 0 + self.system.surface_azimuth = 180 self.system.surface_tilt = 0 else: raise ValueError('invalid orientation strategy. strategy must ' + @@ -84,13 +84,13 @@ def orientation_strategy(self, strategy): self._orientation_strategy = strategy - def run_model(self, times=None, irradiance=None, weather=None): + def run_model(self, times, irradiance=None, weather=None): """ Run the model. Parameters ---------- - times : None or DatetimeIndex + times : DatetimeIndex irradiance : None or DataFrame If None, calculates clear sky data. Columns must be 'dni', 'ghi', 'dhi' diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 4977c777ed..f2cff90277 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -1,24 +1,97 @@ +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 pvlib import modelchain +from pandas.util.testing import assert_series_equal, assert_frame_equal +from nose.tools import with_setup + +# 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(): - raise Exception('test me') + system, location = mc_setup() + mc = ModelChain(system, location) def test_orientation_strategy(): - # test for None, 'None', 'south_at_latitude_tilt', 'flat', ValueError - raise Exception('test me') + 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) + + assert system.surface_tilt == expected[0] + assert system.surface_azimuth == expected[1] def test_run_model(): - raise Exception('test me') + system, location = mc_setup() + mc = ModelChain(system, location) + times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') + dc, ac = mc.run_model(times) + + expected = pd.Series(np.array([ 1.82033564e+02, -2.00000000e-02]), + index=times) + assert_series_equal(ac, expected) def test_run_model_with_irradiance(): - raise Exception('test me') + 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) + dc, ac = mc.run_model(times, irradiance=irradiance) + + expected = pd.Series(np.array([ 1.90054749e+02, -2.00000000e-02]), + index=times) + assert_series_equal(ac, expected) def test_run_model_with_weather(): - raise Exception('test me') \ No newline at end of file + 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) + dc, ac = mc.run_model(times, weather=weather) + + expected = pd.Series(np.array([ 1.99952400e+02, -2.00000000e-02]), + index=times) + assert_series_equal(ac, expected) \ No newline at end of file diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 7a1fe299ff..0c10b9cda3 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -1,6 +1,3 @@ -import logging -pvl_logger = logging.getLogger('pvlib') - import inspect import os import datetime From df389aa1c33e885ed0d044204e27ce98c99627b1 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 31 Jan 2016 20:46:24 -0700 Subject: [PATCH 077/100] implement PVSystem tests --- pvlib/test/test_pvsystem.py | 70 +++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 0c10b9cda3..d36046685e 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -220,31 +220,73 @@ def test_PVSystem_creation(): def test_PVSystem_get_aoi(): - raise Exception('test me') + 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(): - raise Exception('test me') + 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(): - raise Exception('test me') + 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(): - raise Exception('test me') + 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 -# in principle, we should be retesting each of the models tested above -# when they are attached to PVSystem +# 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_pv_system = pvsystem.LocalizedPVSystem(latitude=30, - longitude=-110, - module='blah', - inverter='blarg') - - -def test_LocalizedPVSystem_do_stuff(): - raise Exception('test me') + 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 From 5fc704e1672b9a93d7498e7f92f271f162c9a2c7 Mon Sep 17 00:00:00 2001 From: dacoex Date: Mon, 1 Feb 2016 16:56:06 +0100 Subject: [PATCH 078/100] Minor changes to the documentation In support of: https://github.com/pvlib/pvlib-python/pull/93 --- docs/sphinx/source/package_overview.rst | 2 ++ docs/sphinx/source/whatsnew/v0.3.0.txt | 4 ++++ pvlib/__init__.py | 1 + 3 files changed, 7 insertions(+) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index 5a0fe9409d..fe28977045 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -1,3 +1,5 @@ +.. _package_overview: + Package Overview ================ diff --git a/docs/sphinx/source/whatsnew/v0.3.0.txt b/docs/sphinx/source/whatsnew/v0.3.0.txt index 6df10c147d..28fb851310 100644 --- a/docs/sphinx/source/whatsnew/v0.3.0.txt +++ b/docs/sphinx/source/whatsnew/v0.3.0.txt @@ -39,6 +39,9 @@ Enhancements automatically determine which type of zenith data to use e.g. ``Location.get_airmass``. * Change default ``Location`` timezone to ``'UTC'``. +* Added new sections to the documentation: + * :ref:`package_overview` + * :ref:`variables_style_rules` Bug fixes ~~~~~~~~~ @@ -49,6 +52,7 @@ Bug fixes (:issue:`105`) * Fixed the metadata key specification in documentation of the readtmy2 function. +* Fixes import of tkinter(:issue:`112`) Contributors 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 From cd0da5ba9feca15ddf4beaf66fa66bb88f140834 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 1 Feb 2016 11:42:43 -0700 Subject: [PATCH 079/100] highly experimental LocalizedSingleAxisTracker --- pvlib/tracking.py | 123 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 3 deletions(-) diff --git a/pvlib/tracking.py b/pvlib/tracking.py index f07e363c3a..8d46b78184 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -8,6 +8,8 @@ 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 @@ -30,9 +32,124 @@ def __init__(self, axis_tilt=0, axis_azimuth=0, def singleaxis(self, apparent_zenith, apparent_azimuth): - return singleaxis(apparent_azimuth, apparent_zenith, - self.axis_tilt, self.axis_azimuth, self.max_angle, - self.backtrack, self.gcr) + 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) + solar_zenith = kwargs.pop('solar_zenith', self.solar_zenith) + solar_azimuth = kwargs.pop('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, From 911ceff7a465590f1a764ca8f16f0d8e0225900d Mon Sep 17 00:00:00 2001 From: DaCoEx Date: Mon, 1 Feb 2016 21:38:10 +0100 Subject: [PATCH 080/100] typo --- docs/sphinx/source/whatsnew/v0.3.0.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.3.0.txt b/docs/sphinx/source/whatsnew/v0.3.0.txt index 28fb851310..a52c94ed92 100644 --- a/docs/sphinx/source/whatsnew/v0.3.0.txt +++ b/docs/sphinx/source/whatsnew/v0.3.0.txt @@ -52,7 +52,7 @@ Bug fixes (:issue:`105`) * Fixed the metadata key specification in documentation of the readtmy2 function. -* Fixes import of tkinter(:issue:`112`) +* Fixes the import of tkinter (:issue:`112`) Contributors From d41053cd5165f69a60576e9d2135e7700bfe564c Mon Sep 17 00:00:00 2001 From: DaCoEx Date: Mon, 1 Feb 2016 21:42:47 +0100 Subject: [PATCH 081/100] contributors --- docs/sphinx/source/whatsnew/v0.3.0.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.3.0.txt b/docs/sphinx/source/whatsnew/v0.3.0.txt index a52c94ed92..3a7ab90148 100644 --- a/docs/sphinx/source/whatsnew/v0.3.0.txt +++ b/docs/sphinx/source/whatsnew/v0.3.0.txt @@ -59,3 +59,5 @@ Contributors ~~~~~~~~~~~~ * Will Holmgren +* pyElena21 +* DaCoEx From 01b5ff5c5f7b2763383087f11eae1685ccb8b2b2 Mon Sep 17 00:00:00 2001 From: DaCoEx Date: Wed, 3 Feb 2016 23:55:26 +0100 Subject: [PATCH 082/100] added python 3 --- docs/sphinx/source/whatsnew/v0.3.0.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.3.0.txt b/docs/sphinx/source/whatsnew/v0.3.0.txt index 3a7ab90148..efc0980a9b 100644 --- a/docs/sphinx/source/whatsnew/v0.3.0.txt +++ b/docs/sphinx/source/whatsnew/v0.3.0.txt @@ -52,7 +52,7 @@ Bug fixes (:issue:`105`) * Fixed the metadata key specification in documentation of the readtmy2 function. -* Fixes the import of tkinter (:issue:`112`) +* Fixes the import of tkinter on Python 3 (:issue:`112`) Contributors From 96a6884d1ddf7712473da65344d09c6586763808 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 7 Feb 2016 11:35:58 -0700 Subject: [PATCH 083/100] add api changes to whatsnew --- docs/sphinx/source/classes.rst | 9 +++++ docs/sphinx/source/whatsnew/v0.3.0.txt | 47 ++++++++++++++++---------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/docs/sphinx/source/classes.rst b/docs/sphinx/source/classes.rst index 498839656f..d9953254e6 100644 --- a/docs/sphinx/source/classes.rst +++ b/docs/sphinx/source/classes.rst @@ -1,3 +1,5 @@ +.. _classes: + Classes ======= @@ -51,3 +53,10 @@ SingleAxisTracker :undoc-members: :show-inheritance: +LocalizedSingleAxisTracker +----------------- +.. autoclass:: pvlib.tracking.LocalizedSingleAxisTracker + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/sphinx/source/whatsnew/v0.3.0.txt b/docs/sphinx/source/whatsnew/v0.3.0.txt index efc0980a9b..e632dbcacf 100644 --- a/docs/sphinx/source/whatsnew/v0.3.0.txt +++ b/docs/sphinx/source/whatsnew/v0.3.0.txt @@ -4,24 +4,44 @@ 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. -Enhancements -~~~~~~~~~~~~ +API changes +~~~~~~~~~~~ -* Adds ``PVSystem``, ``LocalizedPVSystem``, ``ModelChain``, - and ``SingleAxisTracker`` classes. (:issue:`17`) -* Replaces ``location`` arguments with ``latitude``, ``longitude``, +* 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 can be created from TMY2/TMY3 metadata - using the ``from_tmy`` constructor. * ``Location`` classes gain the ``get_solarposition``, ``get_clearsky``, and ``get_airmass`` functions. -* Add Package Overview and Classes pages to documentation. +* 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 @@ -38,20 +58,13 @@ Enhancements ``atmosphere.TRUE_ZENITH_MODELS`` to enable code that can automatically determine which type of zenith data to use e.g. ``Location.get_airmass``. -* Change default ``Location`` timezone to ``'UTC'``. -* Added new sections to the documentation: - * :ref:`package_overview` - * :ref:`variables_style_rules` + Bug fixes ~~~~~~~~~ -* ``pvsystem.sapm_celltemp`` argument names now follow the - variable conventions. -* ``irradiance.total_irrad`` now follows the variable conventions. - (:issue:`105`) * Fixed the metadata key specification in documentation of the - readtmy2 function. + ``readtmy2`` function. * Fixes the import of tkinter on Python 3 (:issue:`112`) From 9e5ca7ce83ab68d820a484a7cec532c7a1b72150 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 8 Feb 2016 13:30:28 -0700 Subject: [PATCH 084/100] fix small doc issues --- docs/sphinx/source/classes.rst | 4 ++-- docs/sphinx/source/whatsnew/v0.3.0.txt | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/classes.rst b/docs/sphinx/source/classes.rst index d9953254e6..43659893d7 100644 --- a/docs/sphinx/source/classes.rst +++ b/docs/sphinx/source/classes.rst @@ -33,7 +33,7 @@ ModelChain :show-inheritance: MoreSpecificModelChain ----------- +---------------------- .. autoclass:: pvlib.modelchain.MoreSpecificModelChain :members: :undoc-members: @@ -54,7 +54,7 @@ SingleAxisTracker :show-inheritance: LocalizedSingleAxisTracker ------------------ +-------------------------- .. autoclass:: pvlib.tracking.LocalizedSingleAxisTracker :members: :undoc-members: diff --git a/docs/sphinx/source/whatsnew/v0.3.0.txt b/docs/sphinx/source/whatsnew/v0.3.0.txt index e632dbcacf..01a897abc4 100644 --- a/docs/sphinx/source/whatsnew/v0.3.0.txt +++ b/docs/sphinx/source/whatsnew/v0.3.0.txt @@ -39,9 +39,11 @@ Enhancements ~~~~~~~~~~~~ * Added new sections to the documentation: + * :ref:`package_overview` * :ref:`variables_style_rules` - * :ref:'classes` + * :ref:`classes` + * Adds support for Appveyor, a Windows continuous integration service. (:issue:`111`) * The readthedocs documentation build now uses conda packages From feef0b5a4d5de62c67ebdf50100adbd7d186b96e Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Wed, 10 Feb 2016 10:33:01 -0700 Subject: [PATCH 085/100] add tests, fix attribute error --- pvlib/test/test_tracking.py | 105 +++++++++++++++++++++++++++++++++++- pvlib/tracking.py | 12 ++++- 2 files changed, 114 insertions(+), 3 deletions(-) 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/tracking.py b/pvlib/tracking.py index 8d46b78184..0b37224a60 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -105,8 +105,16 @@ def get_irradiance(self, dni, ghi, dhi, surface_tilt = kwargs.pop('surface_tilt', self.surface_tilt) surface_azimuth = kwargs.pop('surface_azimuth', self.surface_azimuth) - solar_zenith = kwargs.pop('solar_zenith', self.solar_zenith) - solar_azimuth = kwargs.pop('solar_azimuth', self.solar_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: From 9025cfcdf9f018910eeabf6518d5991a3f8d7c5b Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Wed, 10 Feb 2016 11:49:46 -0700 Subject: [PATCH 086/100] pin to ipython 4.0.1 --- docs/environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/environment.yml b/docs/environment.yml index 504c0013a2..2fd1519aae 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -7,7 +7,7 @@ dependencies: - pytz - ephem - numba - - ipython + - ipython=4.0.1 - sphinx - numpydoc - matplotlib From 96e7eb39de830f5be49df62f89118957b8ce038a Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Wed, 10 Feb 2016 16:23:41 -0700 Subject: [PATCH 087/100] more tests for location --- pvlib/location.py | 7 ++----- pvlib/test/test_location.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 58983b5229..1b0b9db23e 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -2,10 +2,7 @@ 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 @@ -190,7 +187,7 @@ def get_clearsky(self, times, model='ineichen', **kwargs): Returns ------- - clearsky : Series or DataFrame + clearsky : DataFrame Column names are: ``ghi, dni, dhi``. """ diff --git a/pvlib/test/test_location.py b/pvlib/test/test_location.py index 4be6f9ab80..1ba9afdcdb 100644 --- a/pvlib/test/test_location.py +++ b/pvlib/test/test_location.py @@ -62,6 +62,32 @@ def test_get_clearsky(): 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 @@ -125,4 +151,12 @@ def test_get_airmass(): columns=['airmass_relative', 'airmass_absolute'], index=times) assert_frame_equal(expected, airmass) - \ No newline at end of file + + +@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') \ No newline at end of file From 4c3ad9b30c2bc860720a53ff89e87b7e8a0fac28 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Wed, 10 Feb 2016 18:01:53 -0700 Subject: [PATCH 088/100] lots of pvsystem tests --- pvlib/pvsystem.py | 20 ++-- pvlib/test/test_pvsystem.py | 213 ++++++++++++++++++++++++++++++++---- 2 files changed, 201 insertions(+), 32 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 4209a134d9..95bfa579a3 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -155,7 +155,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, model='haydavies', **kwargs): """ - Uses the :func:`irradiance.total_irrad` function to calculate + 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 @@ -211,7 +211,7 @@ def ashraeiam(self, aoi): """ Determine the incidence angle modifier using ``self.module_parameters['b']``, ``aoi``, - and the :func:`ashraeiam` function. + and the :py:func:`ashraeiam` function. Parameters ---------- @@ -234,7 +234,7 @@ def physicaliam(self, aoi): ``self.module_parameters['L']``, ``self.module_parameters['n']``, ``aoi``, and the - :func:`physicaliam` function. + :py:func:`physicaliam` function. Parameters ---------- @@ -252,7 +252,7 @@ def physicaliam(self, aoi): def calcparams_desoto(self, poa_global, temp_cell, **kwargs): """ - Use the :func:`calcparams_desoto` function, the input parameters + Use the :py:func:`calcparams_desoto` function, the input parameters and ``self.module_parameters`` to calculate the module currents and resistances. @@ -272,14 +272,16 @@ def calcparams_desoto(self, poa_global, temp_cell, **kwargs): See pvsystem.calcparams_desoto for details """ return calcparams_desoto(poa_global, temp_cell, + self.module_parameters['alpha_sc'], self.module_parameters, - EgRef, dEgdT, **kwargs) + self.module_parameters['EgRef'], + self.module_parameters['dEgdT'], **kwargs) def sapm(self, poa_direct, poa_diffuse, temp_cell, airmass_absolute, aoi, **kwargs): """ - Use the :func:`sapm` function, the input parameters, + Use the :py:func:`sapm` function, the input parameters, and ``self.module_parameters`` to calculate Voc, Isc, Ix, Ixx, Vmp/Imp. @@ -313,7 +315,7 @@ def sapm(self, poa_direct, poa_diffuse, # model now specified by self.racking_model def sapm_celltemp(self, irrad, wind, temp): - """Uses :func:`sapm_celltemp` to calculate module and cell + """Uses :py:func:`sapm_celltemp` to calculate module and cell temperatures based on ``self.racking_model`` and the input parameters. @@ -330,7 +332,7 @@ def sapm_celltemp(self, irrad, wind, temp): def singlediode(self, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): - """Wrapper around the :func:`singlediode` function. + """Wrapper around the :py:func:`singlediode` function. Parameters ---------- @@ -347,7 +349,7 @@ def singlediode(self, photocurrent, saturation_current, def i_from_v(self, resistance_shunt, resistance_series, nNsVth, voltage, saturation_current, photocurrent): - """Wrapper around the :func:`i_from_v` function. + """Wrapper around the :py:func:`i_from_v` function. Parameters ---------- diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index d36046685e..94281d50a1 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -3,6 +3,7 @@ import datetime import numpy as np +from numpy import nan import pandas as pd from nose.tools import assert_equals, assert_almost_equals @@ -89,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. @@ -108,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(): @@ -132,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, @@ -192,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_' @@ -204,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_' From ab6063f13a87ff68255ed55a86fa7c95577f1389 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Fri, 19 Feb 2016 17:11:20 -0700 Subject: [PATCH 089/100] solarposition.get_solarposition is smart about alt pressure kwargs --- pvlib/solarposition.py | 25 +++++-- pvlib/test/test_clearsky.py | 22 +++--- pvlib/test/test_location.py | 24 +++---- pvlib/test/test_solarposition.py | 119 +++++++++++++++++++++++++------ 4 files changed, 139 insertions(+), 51 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 0df544e95c..cf20106f03 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -25,13 +25,13 @@ 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, latitude, longitude, - altitude=0, - method='nrel_numpy', pressure=101325, + altitude=None, pressure=None, + method='nrel_numpy', temperature=12, **kwargs): """ A convenience wrapper for the solar position calculators. @@ -42,6 +42,11 @@ def get_solarposition(time, latitude, longitude, 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` @@ -54,8 +59,6 @@ def get_solarposition(time, latitude, longitude, 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. @@ -71,7 +74,15 @@ def get_solarposition(time, latitude, longitude, [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, ]) @@ -163,7 +174,7 @@ def spa_c(time, latitude, longitude, pressure=101325, altitude=0, pvl_logger.debug('using built-in spa code to calculate solar position') time_utc = time - + spa_out = [] for date in time_utc: diff --git a/pvlib/test/test_clearsky.py b/pvlib/test/test_clearsky.py index 41ee88131a..b6b50876d4 100644 --- a/pvlib/test/test_clearsky.py +++ b/pvlib/test/test_clearsky.py @@ -39,19 +39,19 @@ def test_ineichen_required(): index=times_localized) 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.19492186, 322.1949484 , 80.27218726], - [ 95.14479487, 876.49778895, 703.49655602], - [ 118.45876694, 939.816594 , 1042.34575261], - [ 105.36721216, 909.11474576, 851.33560265], - [ 61.91851386, 647.43752674, 257.50239737], - [ 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_localized, tus.latitude, tus.longitude, diff --git a/pvlib/test/test_location.py b/pvlib/test/test_location.py index 1ba9afdcdb..9ac4719cd3 100644 --- a/pvlib/test/test_location.py +++ b/pvlib/test/test_location.py @@ -15,18 +15,18 @@ 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]) - + def test_location_pytz_tz(): Location(32.2, -111, aztz) @@ -38,7 +38,7 @@ 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' @@ -52,11 +52,11 @@ def test_get_clearsky(): freq='3H') clearsky = tus.get_clearsky(times) expected = pd.DataFrame(data=np.array( - [[ 0. , 0. , 0. ], - [ 49.99776128, 763.02009659, 258.93387913], - [ 70.79971557, 957.15432484, 612.08052529], - [ 59.01912609, 879.09965133, 415.32459426], - [ 0. , 0. , 0. ]]), + [[ 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) @@ -123,7 +123,7 @@ def test_get_solarposition(): 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') @@ -159,4 +159,4 @@ def test_get_airmass_valueerror(): times = pd.DatetimeIndex(start='20160101T0600-0700', end='20160101T1800-0700', freq='3H') - clearsky = tus.get_airmass(times, model='invalid_model') \ No newline at end of file + clearsky = tus.get_airmass(times, model='invalid_model') diff --git a/pvlib/test/test_solarposition.py b/pvlib/test/test_solarposition.py index db73c605c6..cf3f3b98ac 100644 --- a/pvlib/test/test_solarposition.py +++ b/pvlib/test/test_solarposition.py @@ -17,7 +17,7 @@ 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 @@ -37,7 +37,7 @@ # 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_c_physical(): @@ -46,15 +46,15 @@ def test_spa_c_physical(): try: ephem_data = solarposition.spa_c(times, golden_mst.latitude, golden_mst.longitude, - pressure=82000, + 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_c_physical_dst(): times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), periods=1, freq='D', tz=golden.tz) @@ -64,7 +64,7 @@ def test_spa_c_physical_dst(): pressure=82000, temperature=11) except ImportError: - raise SkipTest + raise SkipTest this_expected = expected.copy() this_expected.index = times assert_frame_equal(this_expected, ephem_data[expected.columns]) @@ -75,8 +75,8 @@ def test_spa_python_numpy_physical(): 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, + pressure=82000, + temperature=11, delta_t=67, atmos_refract=0.5667, how='numpy') this_expected = expected.copy() @@ -89,8 +89,8 @@ def test_spa_python_numpy_physical_dst(): periods=1, freq='D', tz=golden.tz) ephem_data = solarposition.spa_python(times, golden.latitude, golden.longitude, - pressure=82000, - temperature=11, delta_t=67, + pressure=82000, + temperature=11, delta_t=67, atmos_refract=0.5667, how='numpy') this_expected = expected.copy() @@ -106,13 +106,13 @@ 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', tz=golden_mst.tz) ephem_data = solarposition.spa_python(times, golden_mst.latitude, golden_mst.longitude, - pressure=82000, - temperature=11, delta_t=67, + pressure=82000, + temperature=11, delta_t=67, atmos_refract=0.5667, how='numba', numthreads=1) this_expected = expected.copy() @@ -132,8 +132,8 @@ def test_spa_python_numba_physical_dst(): 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, + golden.longitude, pressure=82000, + temperature=11, delta_t=67, atmos_refract=0.5667, how='numba', numthreads=1) this_expected = expected.copy() @@ -153,7 +153,7 @@ def test_get_sun_rise_set_transit(): datetime.datetime(2004, 12, 4, 19, 2, 2, 499704)] ).tz_localize('UTC').tolist() result = solarposition.get_sun_rise_set_transit(times, south.latitude, - south.longitude, + south.longitude, delta_t=64.0) frame = pd.DataFrame({'sunrise':sunrise, 'sunset':sunset}, index=times) del result['transit'] @@ -211,7 +211,7 @@ 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)) @@ -222,12 +222,12 @@ def test_calc_time(): 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) - + @requires_ephem def test_earthsun_distance(): times = pd.date_range(datetime.datetime(2003,10,17,13,30,30), @@ -261,3 +261,80 @@ def test_ephemeris_physical_dst(): 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]) From ee327724e55d9e71e759bbc5f98f36d1b74c627c Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Fri, 19 Feb 2016 17:11:49 -0700 Subject: [PATCH 090/100] add basic_chain to modelchain --- pvlib/modelchain.py | 250 +++++++++++++++++++++++++++------- pvlib/test/test_modelchain.py | 73 +++++++++- 2 files changed, 267 insertions(+), 56 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 3917efc6df..b7dd6d8cfe 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1,7 +1,157 @@ """ -Stub documentation for the module. +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', + **kwargs): + """ + A function that computes all of the modeling steps necessary for + calculating power or energy for a PV system at a given location. + + Parameters + ---------- + system : dict + The connected set of modules, inverters, etc. + keys must include latitude, longitude, module_parameters + + location : dict + The physical location at which to evaluate the model. + + times : DatetimeIndex + Times 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``. + + 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. + + **kwargs + Arbitrary keyword arguments. + See code for details. + """ + + # 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 + + solar_position = solarposition.get_solarposition(times, latitude, + longitude, **kwargs) + + # possible error with using apparent zenith with some models + airmass = atmosphere.relativeairmass(solar_position['apparent_zenith'], + model=airmass_model) + airmass = atmosphere.absoluteairmass(airmass, + kwargs.get('pressure', 101325.)) + 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) + + 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): """ A class that represents all of the modeling steps necessary for @@ -24,6 +174,7 @@ class ModelChain(object): 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. @@ -58,36 +209,29 @@ def __init__(self, system, location, 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 is None or strategy == 'None': - pass - elif strategy == 'south_at_latitude_tilt': - self.system.surface_azimuth = 180 - self.system.surface_tilt = self.location.latitude - elif strategy == 'flat': - self.system.surface_azimuth = 180 - self.system.surface_tilt = 0 - else: - raise ValueError('invalid orientation strategy. strategy must ' + - 'be one of south_at_latitude, flat,') + 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 @@ -103,57 +247,65 @@ def run_model(self, times, irradiance=None, weather=None): ------- output : DataFrame Some combination of AC power, DC power, POA irrad, etc. + + Assigns attributes: times, solar_position, airmass, irradiance, + total_irrad, weather, temps, aoi, dc, ac """ - solar_position = self.location.get_solarposition(times) - - airmass = self.location.get_airmass(solar_position=solar_position, - model=self.airmass_model) + 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(solar_position.index, - self.clearsky_model, - zenith_data=solar_position['apparent_zenith'], - airmass_data=airmass['airmass_absolute']) - - total_irrad = self.system.get_irradiance(solar_position['apparent_zenith'], - solar_position['azimuth'], - irradiance['dni'], - irradiance['ghi'], - irradiance['dhi'], - model=self.transposition_model) + 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 - temps = self.system.sapm_celltemp(total_irrad['poa_global'], - weather['wind_speed'], - weather['temp_air']) + self.temps = self.system.sapm_celltemp(self.total_irrad['poa_global'], + self.weather['wind_speed'], + self.weather['temp_air']) - aoi = self.system.get_aoi(solar_position['apparent_zenith'], - solar_position['azimuth']) + self.aoi = self.system.get_aoi(self.solar_position['apparent_zenith'], + self.solar_position['azimuth']) - dc = self.system.sapm(total_irrad['poa_direct'], - total_irrad['poa_diffuse'], - temps['temp_cell'], - airmass['airmass_absolute'], - aoi) + 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) - ac = self.system.snlinverter(dc['v_mp'], dc['p_mp']) + self.ac = self.system.snlinverter(self.dc['v_mp'], self.dc['p_mp']) - return dc, ac + return self.dc, self.ac def model_system(self): """ Model the system? - + I'm just copy/pasting example code... - + Returns ------- ??? """ - + final_output = self.run_model() input = self.prettify_input() modeling_steps = self.get_modeling_steps() @@ -166,7 +318,7 @@ class MoreSpecificModelChain(ModelChain): """ def __init__(self, *args, **kwargs): super(MoreSpecificModelChain, self).__init__(**kwargs) - + def run_model(self): # overrides the parent ModelChain method pass \ No newline at end of file diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index f2cff90277..83c22ec179 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -7,7 +7,7 @@ from pvlib.location import Location from pandas.util.testing import assert_series_equal, assert_frame_equal -from nose.tools import with_setup +from nose.tools import with_setup, raises # should store this test data locally, but for now... sam_data = {} @@ -33,7 +33,7 @@ def mc_setup(): inverter_parameters=inverter) location = Location(32.2, -111, altitude=700) - + return system, location @@ -46,7 +46,7 @@ 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 @@ -56,7 +56,7 @@ def run_orientation_strategy(strategy, expected): location = Location(32.2, -111, altitude=700) mc = ModelChain(system, location, orientation_strategy=strategy) - + assert system.surface_tilt == expected[0] assert system.surface_azimuth == expected[1] @@ -66,7 +66,7 @@ def test_run_model(): mc = ModelChain(system, location) times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') dc, ac = mc.run_model(times) - + expected = pd.Series(np.array([ 1.82033564e+02, -2.00000000e-02]), index=times) assert_series_equal(ac, expected) @@ -91,7 +91,66 @@ def test_run_model_with_weather(): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') weather = pd.DataFrame({'wind_speed':5, 'temp_air':10}, index=times) dc, ac = mc.run_model(times, weather=weather) - + expected = pd.Series(np.array([ 1.99952400e+02, -2.00000000e-02]), index=times) - assert_series_equal(ac, expected) \ No newline at end of file + assert_series_equal(ac, expected) + +@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, + altitude=altitude) + + expected = pd.Series(np.array([ 1.14484467e+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.79622788e+02, -2.00000000e-02]), + index=times) + assert_series_equal(ac, expected) From 1494117992bc43dec2cbdaf3d7bcffdb0e11ed77 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Fri, 19 Feb 2016 17:47:24 -0700 Subject: [PATCH 091/100] more pep8 and doc line length --- pvlib/pvsystem.py | 471 +++++++++++++++++++++++----------------------- 1 file changed, 236 insertions(+), 235 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 5745c5f932..a3a16f0ae7 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -5,7 +5,6 @@ from __future__ import division -import logging import io try: from urllib2 import urlopen @@ -25,74 +24,73 @@ # 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 + 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 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, @@ -101,64 +99,61 @@ def __init__(self, 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 + defined by ``self.surface_tilt``, ``self.surface_azimuth``, and ``self.albedo``. - + Parameters ---------- solar_zenith : float or Series. @@ -177,10 +172,10 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, Airmass model : String Irradiance model. - + **kwargs Passed to :func:`irradiance.total_irrad`. - + Returns ------- poa_irradiance : DataFrame @@ -204,18 +199,17 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, 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 @@ -224,7 +218,6 @@ def ashraeiam(self, aoi): b = self.module_parameters['b'] return ashraeiam(b, aoi) - def physicaliam(self, aoi): """ Determine the incidence angle modifier using @@ -233,11 +226,11 @@ def physicaliam(self, aoi): ``self.module_parameters['n']``, ``aoi``, and the :py:func:`physicaliam` function. - + Parameters ---------- See pvsystem.physicaliam for details - + Returns ------- See pvsystem.physicaliam for details @@ -247,13 +240,12 @@ def physicaliam(self, aoi): 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. - + 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 @@ -261,28 +253,27 @@ def calcparams_desoto(self, poa_global, temp_cell, **kwargs): 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 @@ -293,49 +284,47 @@ def sapm(self, poa_direct, poa_diffuse, 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 + 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 @@ -343,53 +332,50 @@ def singlediode(self, photocurrent, saturation_current, 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 @@ -403,11 +389,10 @@ def localize(self, location=None, latitude=None, longitude=None, 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. - + 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. """ @@ -442,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 @@ -468,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. @@ -522,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. @@ -554,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 @@ -580,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: @@ -597,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 @@ -625,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 ---------- @@ -676,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, @@ -725,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 @@ -761,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 ---------- @@ -798,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 @@ -910,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 ------- @@ -953,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)) @@ -962,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': @@ -1009,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). @@ -1051,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 @@ -1092,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 -------- @@ -1126,20 +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'] + + 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'] @@ -1187,10 +1186,10 @@ def sapm_celltemp(poa_global, wind_speed, temp_air, * 'insulated_back_polymerback' * 'open_rack_polymer_thinfilm_steel' * '22x_concentrator_tracker' - + If dict, supply the following parameters (if list, in the following order): - + * a : float SAPM module parameter for establishing the upper limit for module temperature at low wind speeds and @@ -1213,8 +1212,9 @@ def sapm_celltemp(poa_global, wind_speed, temp_air, 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 -------- @@ -1229,7 +1229,7 @@ def sapm_celltemp(poa_global, wind_speed, temp_air, '22x_concentrator_tracker': [-3.23, -.130, 13] } - if isinstance(model, str): + if isinstance(model, str): model = temp_models[model.lower()] elif isinstance(model, list): model = model @@ -1240,7 +1240,7 @@ def sapm_celltemp(poa_global, wind_speed, temp_air, b = model[1] deltaT = model[2] - E0 = 1000. # Reference irradiance + E0 = 1000. # Reference irradiance temp_module = pd.Series(poa_global*np.exp(a + b*wind_speed) + temp_air) @@ -1260,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. @@ -1277,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 @@ -1293,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. @@ -1324,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 @@ -1339,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, @@ -1513,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 ------- @@ -1531,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 @@ -1559,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 ---------- @@ -1575,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: @@ -1608,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. @@ -1617,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. From 0002bd0853128a41a4f4cc4cee831103be80a93c Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 6 Mar 2016 19:07:39 -0700 Subject: [PATCH 092/100] clean up modelchain documentation --- pvlib/modelchain.py | 100 ++++++++++++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 37 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index b7dd6d8cfe..dd333f2c85 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -22,35 +22,63 @@ def basic_chain(times, latitude, longitude, airmass_model='kastenyoung1989', **kwargs): """ - A function that computes all of the modeling steps necessary for - calculating power or energy for a PV system at a given location. + 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 ---------- - system : dict - The connected set of modules, inverters, etc. - keys must include latitude, longitude, module_parameters - - location : dict - The physical location at which to evaluate the model. - 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``. - 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. See code for details. @@ -154,27 +182,25 @@ def get_orientation(strategy, **kwargs): class ModelChain(object): """ - A class that represents all of the modeling steps necessary for - calculating power or energy for a PV system at a given location. - - Consider an abstract base class. + 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 - The connected set of modules, inverters, etc. + A :py:class:`~pvlib.pvsystem.PVSystem` object that represents + the connected set of modules, inverters, etc. - location : location - The physical location at which to evaluate the model. - - times : DatetimeIndex - Times at which to evaluate the model. + 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'. + 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. @@ -185,14 +211,12 @@ class ModelChain(object): 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. - - See also - -------- - location.Location - pvsystem.PVSystem """ def __init__(self, system, location, @@ -235,9 +259,12 @@ def run_model(self, times, irradiance=None, weather=None): 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' + 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. @@ -245,8 +272,9 @@ def run_model(self, times, irradiance=None, weather=None): Returns ------- - output : DataFrame - Some combination of AC power, DC power, POA irrad, etc. + output : (ac, dc) + Tuple of AC power (Series) and DC power with SAPM parameters + (DataFrame). Assigns attributes: times, solar_position, airmass, irradiance, total_irrad, weather, temps, aoi, dc, ac @@ -294,7 +322,6 @@ def run_model(self, times, irradiance=None, weather=None): return self.dc, self.ac - def model_system(self): """ Model the system? @@ -311,7 +338,6 @@ def model_system(self): modeling_steps = self.get_modeling_steps() - class MoreSpecificModelChain(ModelChain): """ Something more specific. @@ -321,4 +347,4 @@ def __init__(self, *args, **kwargs): def run_model(self): # overrides the parent ModelChain method - pass \ No newline at end of file + pass From 23d37279c38d1f7b587bb7da3230c202f574bcb1 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 6 Mar 2016 19:08:01 -0700 Subject: [PATCH 093/100] remove unused mc stuff --- pvlib/modelchain.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index dd333f2c85..9e1b53db75 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -321,30 +321,3 @@ def run_model(self, times, irradiance=None, weather=None): self.ac = self.system.snlinverter(self.dc['v_mp'], self.dc['p_mp']) return self.dc, self.ac - - def model_system(self): - """ - Model the system? - - I'm just copy/pasting example code... - - Returns - ------- - ??? - """ - - final_output = self.run_model() - input = self.prettify_input() - modeling_steps = self.get_modeling_steps() - - -class MoreSpecificModelChain(ModelChain): - """ - Something more specific. - """ - def __init__(self, *args, **kwargs): - super(MoreSpecificModelChain, self).__init__(**kwargs) - - def run_model(self): - # overrides the parent ModelChain method - pass From 64481724af9071d127de250ce84d07f89c942786 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 6 Mar 2016 20:19:13 -0700 Subject: [PATCH 094/100] remove removed specificchain from classes doc --- docs/sphinx/source/classes.rst | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/sphinx/source/classes.rst b/docs/sphinx/source/classes.rst index 43659893d7..f1fc154cd2 100644 --- a/docs/sphinx/source/classes.rst +++ b/docs/sphinx/source/classes.rst @@ -24,20 +24,13 @@ PVSystem :members: :undoc-members: :show-inheritance: - + ModelChain ---------- .. autoclass:: pvlib.modelchain.ModelChain :members: :undoc-members: :show-inheritance: - -MoreSpecificModelChain ----------------------- -.. autoclass:: pvlib.modelchain.MoreSpecificModelChain - :members: - :undoc-members: - :show-inheritance: LocalizedPVSystem ----------------- From 67d45f83b86f32ef03e2776cd2232df499a70276 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 6 Mar 2016 20:19:31 -0700 Subject: [PATCH 095/100] add basic_chain to package_overview --- docs/sphinx/source/package_overview.rst | 101 +++++++++++++++--------- 1 file changed, 62 insertions(+), 39 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index fe28977045..e43fab61a7 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -10,7 +10,7 @@ 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 +there are modelers of PV systems, so pvlib-python provides several modeling paradigms. @@ -36,28 +36,28 @@ configuration at a handful of sites listed below. 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 @@ -73,7 +73,7 @@ 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} @@ -104,32 +104,56 @@ to accomplish our system modeling goal: 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 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`, @@ -138,14 +162,14 @@ The following code demonstrates how to use 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) @@ -156,12 +180,12 @@ objects to accomplish our system modeling goal: dc, ac = mc.run_model(times) 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 modelchain-energies.png width=6in plt.ylabel('Yearly energy yield (W hr)') @@ -170,18 +194,17 @@ objects to accomplish our system modeling goal: Object oriented (LocalizedPVSystem) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The second object oriented paradigm uses a model where a +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. +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 = {} @@ -214,12 +237,12 @@ object to accomplish our modeling goal: 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)') @@ -227,9 +250,9 @@ object to accomplish our modeling goal: 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. +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 From 31e323dec438818c481dce0b8c014d9911ad03f3 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 6 Mar 2016 20:20:45 -0700 Subject: [PATCH 096/100] add altitude and pressure kwargs to basic_chain --- pvlib/modelchain.py | 40 +++++++++++++++++++++++++++++------ pvlib/test/test_modelchain.py | 4 ++-- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 9e1b53db75..d6a080bda5 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -20,6 +20,7 @@ def basic_chain(times, latitude, longitude, 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 @@ -79,9 +80,23 @@ def basic_chain(times, latitude, longitude, 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, @@ -97,14 +112,24 @@ def basic_chain(times, latitude, longitude, 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, **kwargs) + 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, - kwargs.get('pressure', 101325.)) + airmass = atmosphere.absoluteairmass(airmass, pressure) dni_extra = pvlib.irradiance.extraradiation(solar_position.index) dni_extra = pd.Series(dni_extra, index=solar_position.index) @@ -118,7 +143,8 @@ def basic_chain(times, latitude, longitude, latitude, longitude, zenith_data=solar_position['apparent_zenith'], - airmass_data=airmass) + airmass_data=airmass, + altitude=altitude) total_irrad = pvlib.irradiance.total_irrad( surface_tilt, @@ -272,9 +298,9 @@ def run_model(self, times, irradiance=None, weather=None): Returns ------- - output : (ac, dc) - Tuple of AC power (Series) and DC power with SAPM parameters - (DataFrame). + output : (dc, ac) + Tuple of DC power (with SAPM parameters) (DataFrame) and AC + power (Series). Assigns attributes: times, solar_position, airmass, irradiance, total_irrad, weather, temps, aoi, dc, ac diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 83c22ec179..b534ff78b8 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -131,7 +131,7 @@ def test_basic_chain_alt_az(): surface_azimuth=surface_azimuth, altitude=altitude) - expected = pd.Series(np.array([ 1.14484467e+02, -2.00000000e-02]), + expected = pd.Series(np.array([ 1.15771428788e+02, -2.00000000e-02]), index=times) assert_series_equal(ac, expected) @@ -151,6 +151,6 @@ def test_basic_chain_strategy(): orientation_strategy='south_at_latitude_tilt', altitude=altitude) - expected = pd.Series(np.array([ 1.79622788e+02, -2.00000000e-02]), + expected = pd.Series(np.array([ 1.82033563543e+02, -2.00000000e-02]), index=times) assert_series_equal(ac, expected) From a17379f59163ce15fb621346448b402f6c2f25ed Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 6 Mar 2016 20:36:45 -0700 Subject: [PATCH 097/100] pin appveyors scipy to 0.16 to avoid yet another continuum package issue --- appveyor.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ad8afa1efc..317c15b216 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 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" From 5280cb9f40d34ec8eb75068866e22affcd05e76f Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 6 Mar 2016 20:53:38 -0700 Subject: [PATCH 098/100] add test for orientation prop, altitude/pressure kwargs --- pvlib/test/test_modelchain.py | 50 ++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index b534ff78b8..b4543ad8e1 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -57,6 +57,9 @@ def run_orientation_strategy(strategy, expected): 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] @@ -96,6 +99,12 @@ def test_run_model_with_weather(): 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', @@ -112,6 +121,7 @@ def test_basic_chain_required(): 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') @@ -128,13 +138,13 @@ def test_basic_chain_alt_az(): dc, ac = modelchain.basic_chain(times, latitude, longitude, module_parameters, inverter_parameters, surface_tilt=surface_tilt, - surface_azimuth=surface_azimuth, - altitude=altitude) + surface_azimuth=surface_azimuth) - expected = pd.Series(np.array([ 1.15771428788e+02, -2.00000000e-02]), + 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') @@ -154,3 +164,37 @@ def test_basic_chain_strategy(): 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) From 8c9c89d812e8a42419f7955c07dd0860d25cccd6 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 6 Mar 2016 21:13:14 -0700 Subject: [PATCH 099/100] correction, appveyor scipy=0.16.0. ugh --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 317c15b216..5d3d1a8ae5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -32,7 +32,7 @@ install: - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # install depenencies - - "conda install --yes --quiet pip numpy scipy=0.16 pandas nose pytz ephem numba" + - "conda install --yes --quiet pip numpy scipy=0.16.0 pandas nose pytz ephem numba" # install pvlib - "python setup.py install" From ab69f9a5ff937e8dee61f537eab6e38225e65479 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 7 Mar 2016 09:05:03 -0700 Subject: [PATCH 100/100] run_model now returns self --- docs/sphinx/source/package_overview.rst | 7 ++++--- pvlib/modelchain.py | 6 ++---- pvlib/test/test_modelchain.py | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst index e43fab61a7..5e5d6eacbe 100644 --- a/docs/sphinx/source/package_overview.rst +++ b/docs/sphinx/source/package_overview.rst @@ -176,9 +176,10 @@ objects to accomplish our system modeling goal: # very experimental mc = ModelChain(system, location, orientation_strategy='south_at_latitude_tilt') - # optional parameters for irradiance and weather data - dc, ac = mc.run_model(times) - annual_energy = ac.sum() + # 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) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index d6a080bda5..09f5336718 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -298,9 +298,7 @@ def run_model(self, times, irradiance=None, weather=None): Returns ------- - output : (dc, ac) - Tuple of DC power (with SAPM parameters) (DataFrame) and AC - power (Series). + self Assigns attributes: times, solar_position, airmass, irradiance, total_irrad, weather, temps, aoi, dc, ac @@ -346,4 +344,4 @@ def run_model(self, times, irradiance=None, weather=None): self.ac = self.system.snlinverter(self.dc['v_mp'], self.dc['p_mp']) - return self.dc, self.ac + return self diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index b4543ad8e1..56d2aef89e 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -68,7 +68,7 @@ def test_run_model(): system, location = mc_setup() mc = ModelChain(system, location) times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') - dc, ac = mc.run_model(times) + ac = mc.run_model(times).ac expected = pd.Series(np.array([ 1.82033564e+02, -2.00000000e-02]), index=times) @@ -81,7 +81,7 @@ def test_run_model_with_irradiance(): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') irradiance = pd.DataFrame({'dni':900, 'ghi':600, 'dhi':150}, index=times) - dc, ac = mc.run_model(times, irradiance=irradiance) + ac = mc.run_model(times, irradiance=irradiance).ac expected = pd.Series(np.array([ 1.90054749e+02, -2.00000000e-02]), index=times) @@ -93,7 +93,7 @@ def test_run_model_with_weather(): 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) - dc, ac = mc.run_model(times, weather=weather) + ac = mc.run_model(times, weather=weather).ac expected = pd.Series(np.array([ 1.99952400e+02, -2.00000000e-02]), index=times)