diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 45ef9ec50a..8af2bf0f40 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -221,6 +221,16 @@ AOI modifiers pvsystem.ashraeiam pvsystem.sapm_aoi_loss +PV temperature models +--------------------- + +.. autosummary:: + :toctree: generated/ + + temperature.sapm_cell + temperature.sapm_module + temperature.pvsyst_cell + Single diode models ------------------- @@ -258,29 +268,31 @@ Functions relevant for the SAPM model. pvsystem.sapm pvsystem.sapm_effective_irradiance - pvsystem.sapm_celltemp pvsystem.sapm_spectral_loss pvsystem.sapm_aoi_loss pvsystem.snlinverter + temperature.sapm_cell -PVWatts model +Pvsyst model ------------- +Functions relevant for the Pvsyst model. + .. autosummary:: :toctree: generated/ - pvsystem.pvwatts_dc - pvsystem.pvwatts_ac - pvsystem.pvwatts_losses - pvsystem.pvwatts_losses + temperature.pvsyst_cell -PVsyst model ------------- +PVWatts model +------------- .. autosummary:: :toctree: generated/ - pvsystem.pvsyst_celltemp + pvsystem.pvwatts_dc + pvsystem.pvwatts_ac + pvsystem.pvwatts_losses + pvsystem.pvwatts_losses Other ----- @@ -472,7 +484,7 @@ ModelChain properties that are aliases for your specific modeling functions. modelchain.ModelChain.ac_model modelchain.ModelChain.aoi_model modelchain.ModelChain.spectral_model - modelchain.ModelChain.temp_model + modelchain.ModelChain.temperature_model modelchain.ModelChain.losses_model modelchain.ModelChain.effective_irradiance_model @@ -500,6 +512,7 @@ ModelChain model definitions. modelchain.ModelChain.sapm_spectral_loss modelchain.ModelChain.no_spectral_loss modelchain.ModelChain.sapm_temp + modelchain.ModelChain.pvsyst_temp modelchain.ModelChain.pvwatts_losses modelchain.ModelChain.no_extra_losses @@ -516,7 +529,7 @@ on the information in the associated :py:class:`~pvsystem.PVSystem` object. modelchain.ModelChain.infer_ac_model modelchain.ModelChain.infer_aoi_model modelchain.ModelChain.infer_spectral_model - modelchain.ModelChain.infer_temp_model + modelchain.ModelChain.infer_temperature_model modelchain.ModelChain.infer_losses_model Functions diff --git a/docs/sphinx/source/clearsky.rst b/docs/sphinx/source/clearsky.rst index c47212f42b..669ca07ca0 100644 --- a/docs/sphinx/source/clearsky.rst +++ b/docs/sphinx/source/clearsky.rst @@ -68,7 +68,7 @@ returns a :py:class:`pandas.DataFrame`. In [1]: tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - In [1]: times = pd.DatetimeIndex(start='2016-07-01', end='2016-07-04', freq='1min', tz=tus.tz) + In [1]: times = pd.date_range(start='2016-07-01', end='2016-07-04', freq='1min', tz=tus.tz) In [1]: cs = tus.get_clearsky(times) # ineichen with climatology table by default @@ -168,7 +168,7 @@ varies from 300 m to 1500 m. .. ipython:: - In [1]: times = pd.DatetimeIndex(start='2015-01-01', end='2016-01-01', freq='1D') + In [1]: times = pd.date_range(start='2015-01-01', end='2016-01-01', freq='1D') In [1]: sites = [(32, -111, 'Tucson1'), (32.2, -110.9, 'Tucson2'), ...: (33.5, -112.1, 'Phoenix'), (35.1, -106.6, 'Albuquerque')] @@ -608,7 +608,7 @@ GHI data. We first generate and plot the clear sky and measured data. abq = Location(35.04, -106.62, altitude=1619) - times = pd.DatetimeIndex(start='2012-04-01 10:30:00', tz='Etc/GMT+7', periods=30, freq='1min') + times = pd.date_range(start='2012-04-01 10:30:00', tz='Etc/GMT+7', periods=30, freq='1min') cs = abq.get_clearsky(times) diff --git a/docs/sphinx/source/forecasts.rst b/docs/sphinx/source/forecasts.rst index f959982e2c..56e88acf74 100644 --- a/docs/sphinx/source/forecasts.rst +++ b/docs/sphinx/source/forecasts.rst @@ -440,6 +440,7 @@ for details. .. ipython:: python from pvlib.pvsystem import PVSystem, retrieve_sam + from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS from pvlib.tracking import SingleAxisTracker from pvlib.modelchain import ModelChain @@ -447,12 +448,10 @@ for details. cec_inverters = retrieve_sam('cecinverter') module = sandia_modules['Canadian_Solar_CS5P_220M___2009_'] inverter = cec_inverters['SMA_America__SC630CP_US_315V__CEC_2012_'] + temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'] # model a big tracker for more fun - system = SingleAxisTracker(module_parameters=module, - inverter_parameters=inverter, - modules_per_string=15, - strings_per_inverter=300) + system = SingleAxisTracker(module_parameters=module, inverter_parameters=inverter, temperature_model_parameters=temperature_model_parameters, modules_per_string=15, strings_per_inverter=300) # fx is a common abbreviation for forecast fx_model = GFS() @@ -480,9 +479,9 @@ Here's the forecast plane of array irradiance... .. ipython:: python - mc.temps.plot(); + mc.cell_temperature.plot(); @savefig pv_temps.png width=6in - plt.ylabel('Temperature (C)'); + plt.ylabel('Cell Temperature (C)'); @suppress plt.close(); diff --git a/docs/sphinx/source/introexamples.rst b/docs/sphinx/source/introexamples.rst index 6cd0d54f63..e1b01d94ca 100644 --- a/docs/sphinx/source/introexamples.rst +++ b/docs/sphinx/source/introexamples.rst @@ -30,7 +30,7 @@ configuration at a handful of sites listed below. import pandas as pd import matplotlib.pyplot as plt - naive_times = pd.DatetimeIndex(start='2015', end='2016', freq='1h') + naive_times = pd.date_range(start='2015', end='2016', freq='1h') # very approximate # latitude, longitude, name, altitude, timezone @@ -46,6 +46,7 @@ configuration at a handful of sites listed below. sapm_inverters = pvlib.pvsystem.retrieve_sam('cecinverter') module = sandia_modules['Canadian_Solar_CS5P_220M___2009_'] inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'] + temperature_model_parameters = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'] # specify constant ambient air temp and wind for simplicity temp_air = 20 @@ -88,12 +89,13 @@ 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'], - wind_speed, temp_air) + tcell = pvlib.temperature.sapm_cell(total_irrad['poa_global'], + temp_air, wind_speed, + **temperature_model_parameters) effective_irradiance = pvlib.pvsystem.sapm_effective_irradiance( total_irrad['poa_direct'], total_irrad['poa_diffuse'], am_abs, aoi, module) - dc = pvlib.pvsystem.sapm(effective_irradiance, temps['temp_cell'], module) + dc = pvlib.pvsystem.sapm(effective_irradiance, tcell, module) ac = pvlib.pvsystem.snlinverter(dc['v_mp'], dc['p_mp'], inverter) annual_energy = ac.sum() energies[name] = annual_energy @@ -149,7 +151,8 @@ by examining the parameters defined for the module. from pvlib.modelchain import ModelChain system = PVSystem(module_parameters=module, - inverter_parameters=inverter) + inverter_parameters=inverter, + temperature_model_parameters=temperature_model_parameters) energies = {} for latitude, longitude, name, altitude, timezone in coordinates: @@ -214,6 +217,7 @@ modeling goal: for latitude, longitude, name, altitude, timezone in coordinates: localized_system = LocalizedPVSystem(module_parameters=module, inverter_parameters=inverter, + temperature_model_parameters=temperature_model_parameters, surface_tilt=latitude, surface_azimuth=180, latitude=latitude, @@ -229,15 +233,15 @@ modeling goal: clearsky['dni'], clearsky['ghi'], clearsky['dhi']) - temps = localized_system.sapm_celltemp(total_irrad['poa_global'], - wind_speed, temp_air) + tcell = localized_system.sapm_celltemp(total_irrad['poa_global'], + temp_air, wind_speed) aoi = localized_system.get_aoi(solar_position['apparent_zenith'], solar_position['azimuth']) airmass = localized_system.get_airmass(solar_position=solar_position) effective_irradiance = localized_system.sapm_effective_irradiance( total_irrad['poa_direct'], total_irrad['poa_diffuse'], airmass['airmass_absolute'], aoi) - dc = localized_system.sapm(effective_irradiance, temps['temp_cell']) + dc = localized_system.sapm(effective_irradiance, tcell) ac = localized_system.snlinverter(dc['v_mp'], dc['p_mp']) annual_energy = ac.sum() energies[name] = annual_energy diff --git a/docs/sphinx/source/modelchain.rst b/docs/sphinx/source/modelchain.rst index 1b324801b3..4025451cda 100644 --- a/docs/sphinx/source/modelchain.rst +++ b/docs/sphinx/source/modelchain.rst @@ -45,6 +45,8 @@ objects, module data, and inverter data. from pvlib.pvsystem import PVSystem from pvlib.location import Location from pvlib.modelchain import ModelChain + from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS + temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'] # load some module and inverter specifications sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod') @@ -61,7 +63,8 @@ object. location = Location(latitude=32.2, longitude=-110.9) system = PVSystem(surface_tilt=20, surface_azimuth=200, module_parameters=sandia_module, - inverter_parameters=cec_inverter) + inverter_parameters=cec_inverter, + temperature_model_parameters=temperature_model_parameters) mc = ModelChain(system, location) Printing a ModelChain object will display its models. @@ -87,6 +90,10 @@ examples are shown below. mc.aoi +.. ipython:: python + + mc.cell_temperature + .. ipython:: python mc.dc @@ -141,8 +148,11 @@ model, AC model, AOI loss model, and spectral loss model. .. ipython:: python - sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter) - mc = ModelChain(system, location) + sapm_system = PVSystem( + module_parameters=sandia_module, + inverter_parameters=cec_inverter, + temperature_model_parameters=temperature_model_parameters) + mc = ModelChain(sapm_system, location) print(mc) .. ipython:: python @@ -160,7 +170,10 @@ information to determine which of those models to choose. .. ipython:: python - pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004}) + pvwatts_system = PVSystem( + module_parameters={'pdc0': 240, 'gamma_pdc': -0.004}, + inverter_parameters={'pdc0': 240}, + temperature_model_parameters=temperature_model_parameters) mc = ModelChain(pvwatts_system, location, aoi_model='physical', spectral_model='no_loss') print(mc) @@ -176,8 +189,11 @@ functions for a PVSystem that contains SAPM-specific parameters. .. ipython:: python - sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter) - mc = ModelChain(system, location, aoi_model='physical', spectral_model='no_loss') + sapm_system = PVSystem( + module_parameters=sandia_module, + inverter_parameters=cec_inverter, + temperature_model_parameters=temperature_model_parameters) + mc = ModelChain(sapm_system, location, aoi_model='physical', spectral_model='no_loss') print(mc) .. ipython:: python @@ -264,21 +280,24 @@ the ModelChain.pvwatts_dc method is shown below. Its only argument is The ModelChain.pvwatts_dc method calls the pvwatts_dc method of the PVSystem object that we supplied using data that is stored in its own -``effective_irradiance`` and ``temps`` attributes. Then it assigns the +``effective_irradiance`` and ``cell_temperature`` attributes. Then it assigns the result to the ``dc`` attribute of the ModelChain object. The code below shows a simple example of this. .. ipython:: python # make the objects - pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004}) + pvwatts_system = PVSystem( + module_parameters={'pdc0': 240, 'gamma_pdc': -0.004}, + inverter_parameters={'pdc0': 240}, + temperature_model_parameters=temperature_model_parameters) mc = ModelChain(pvwatts_system, location, aoi_model='no_loss', spectral_model='no_loss') # manually assign data to the attributes that ModelChain.pvwatts_dc will need. # for standard workflows, run_model would assign these attributes. mc.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')]) - mc.temps = pd.DataFrame({'temp_cell': 50, 'temp_module': 50}, index=[pd.Timestamp('20170401 1200-0700')]) + mc.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')]) # run ModelChain.pvwatts_dc and look at the result mc.pvwatts_dc(); @@ -304,13 +323,16 @@ PVSystem.scale_voltage_current_power method. .. ipython:: python # make the objects - sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter) + sapm_system = PVSystem( + module_parameters=sandia_module, + inverter_parameters=cec_inverter, + temperature_model_parameters=temperature_model_parameters) mc = ModelChain(sapm_system, location) # manually assign data to the attributes that ModelChain.sapm will need. # for standard workflows, run_model would assign these attributes. mc.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')]) - mc.temps = pd.DataFrame({'temp_cell': 50, 'temp_module': 50}, index=[pd.Timestamp('20170401 1200-0700')]) + mc.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')]) # run ModelChain.sapm and look at the result mc.sapm(); @@ -333,7 +355,10 @@ section. .. ipython:: python - pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004}) + pvwatts_system = PVSystem( + module_parameters={'pdc0': 240, 'gamma_pdc': -0.004}, + inverter_parameters={'pdc0': 240}, + temperature_model_parameters=temperature_model_parameters) mc = ModelChain(pvwatts_system, location, aoi_model='no_loss', spectral_model='no_loss') mc.dc_model.__func__ @@ -403,18 +428,26 @@ function if you wanted to. return mc - def pvusa_ac_mc_wrapper(mc): + def pvusa_ac_mc(mc): # keep it simple mc.ac = mc.dc return mc + + def no_loss_temperature(mc): + # keep it simple + mc.cell_temperature = mc.weather['temp_air'] + return mc + + .. ipython:: python module_parameters = {'a': 0.2, 'b': 0.00001, 'c': 0.001, 'd': -0.00005} pvusa_system = PVSystem(module_parameters=module_parameters) mc = ModelChain(pvusa_system, location, - dc_model=pvusa_mc_wrapper, ac_model=pvusa_ac_mc_wrapper, + dc_model=pvusa_mc_wrapper, ac_model=pvusa_ac_mc, + temperature_model=no_loss_temperature, aoi_model='no_loss', spectral_model='no_loss') A ModelChain object uses Python’s functools.partial function to assign diff --git a/docs/sphinx/source/whatsnew/v0.7.0.rst b/docs/sphinx/source/whatsnew/v0.7.0.rst index 20668a3040..9dfd58ecfd 100644 --- a/docs/sphinx/source/whatsnew/v0.7.0.rst +++ b/docs/sphinx/source/whatsnew/v0.7.0.rst @@ -1,7 +1,7 @@ .. _whatsnew_0700: v0.7.0 (MONTH DAY, YEAR) ---------------------- +------------------------ This is a major release that drops support for Python 2 and Python 3.4. We recommend all users of v0.6.3 upgrade to this release after checking API @@ -12,6 +12,63 @@ compatibility notes. API Changes ~~~~~~~~~~~ +* Changes related to cell temperature models (:issue:`678`): + * Changes to functions + - Moved functions for cell temperature from `pvsystem.py` to `temperature.py`. + - Renamed `pvsystem.sapm_celltemp` and `pvsystem.pvsyst_celltemp` + to `temperature.sapm_cell` and `temperature.pvsyst_cell`. + - `temperature.sapm_cell` returns only the cell temperature, whereas the + old `pvsystem.sapm_celltemp` returned a `DataFrame` with both cell and module temperatures. + - Created `temperature.sapm_module` to return module temperature using the SAPM temperature model. + - Changed the order of arguments for`pvsystem.sapm_celltemp`, + `pvsystem.pvsyst_celltemp` and `PVSystem.sapm_celltemp` to be consistent + among cell temperature model functions. + - Removed `model` as a kwarg from `temperature.sapm_cell` and + `temperature.pvsyst_cell`. These functions now require model-specific parameters. + - Added the argument `irrad_ref`, default value 1000, to `temperature.sapm_cell`. + * Changes to named temperature model parameter sets + - Renamed `pvsystem.TEMP_MODEL_PARAMS` to `temperature.TEMPERATURE_MODEL_PARAMETERS`. + - `temperature.TEMPERATURE_MODEL_PARAMETERS` uses dict rather than + tuple for a parameter set. + - Names for parameter sets in `temperature.TEMPERATURE_MODEL_PARAMETERS` have changed. + - Parameter sets for the SAPM cell temperature model named + 'open_rack_polymer_thinfilm_steel' and '22x_concentrator_tracker' + are considered obsolete and have been removed. + * Changes to `PVSystem` class + - Changed the `model` kwarg in `PVSystem.sapm_celltemp` and + `PVSystem.pvsyst_celltemp` to `parameter_set`. `parameter_set` expects + a str which is a valid key for `temperature.TEMPERATURE_MODEL_PARAMETERS` + for the corresponding temperature model. + - Added an attribute `PVSystem.module_type` (str) to record module + front and back materials, default is `glass_polymer`. + - Changed meaning of `PVSystem.racking_model` to describe racking + only, e.g., default is `open_rack`. + - Added an attribute `PVSystem.temperature_model_parameters` (dict). + to contain temperature model parameters. + - If `PVSystem.temperature_model_parameters` is not specified and + `PVSystem.racking_model` and `PVSystem.module_type` combine to a valid + parameter set name for the SAPM cell temperature model, that parameter + set is assigned to `PVSystem.temperature_model_parameters`. Otherwise + `PVSystem.temperature_model_parameters` is assigned an empty dict. The + result is that the default parameter set for SAPM cell temperature model + is `open_rack_glass_polymer`; the old default was `open_rack_glass_glass`. + * Changes to `ModelChain` class + - `ModelChain.temp_model` renamed to `ModelChain.temperature_model`. + - `ModelChain.temperature_model` now defaults to `None`. The temperature + model can be inferred from `PVSystem.temperature_model_parameters`. + - `ModelChain.temperature_model_parameters` now defaults to `None`. The temperature + model can be inferred from `PVSystem.temperature_model_parameters`. + - `ModelChain.temps` attribute renamed to `ModelChain.cell_temperature`, + and its datatype is now `numeric` rather than `DataFrame`. + - If `PVSystem.temperature_model_parameters` is not specified, `ModelChain` + defaults to old behavior, using the SAPM temperature model with parameter + set `open_rack_glass_glass`. This behavior is deprecated, and will be + removed in v0.8. In v0.8 `PVSystem.temperature_model_parameters` will + be required for `ModelChain`. + - Implemented `pvsyst` as an option for `ModelChain.temperature_model`. + - `modelchain.basic_chain` has a new required argument + `temperature_model_parameters`. + * Calling :py:func:`pvlib.pvsystem.retrieve_sam` with no parameters will raise an exception instead of displaying a dialog. @@ -34,8 +91,14 @@ Bug fixes Testing ~~~~~~~ -* Added 30 minutes to timestamps in `test_psm3.csv` to match change in NSRDB (:issue:`733`) +* Added 30 minutes to timestamps in `test_psm3.csv` to match change + in NSRDB (:issue:`733`) * Added tests for methods in bifacial.py. +* Added tests for changes to cell temperature models. + +Documentation +~~~~~~~~~~~~~ +* Corrected docstring for `pvsystem.PVSystem.sapm` Removal of prior version deprecations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -55,4 +118,5 @@ Contributors * Oscar Dowson (:ghuser:`odow`) * Anton Driesse (:ghuser:`adriesse`) * Alexander Morgan (:ghuser:`alexandermorgan`) +* Cliff Hansen (:ghuser:`cwhanse`) * Miguel Sánchez de León Peque (:ghuser:`Peque`) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 1455a57ad3..3244dc2977 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -10,7 +10,8 @@ import warnings import pandas as pd -from pvlib import (solarposition, pvsystem, clearsky, atmosphere, tools) +from pvlib import (atmosphere, clearsky, pvsystem, solarposition, temperature, + tools) from pvlib.tracking import SingleAxisTracker import pvlib.irradiance # avoid name conflict with full import from pvlib.pvsystem import DC_MODEL_PARAMS @@ -18,7 +19,8 @@ def basic_chain(times, latitude, longitude, - module_parameters, inverter_parameters, + module_parameters, temperature_model_parameters, + inverter_parameters, irradiance=None, weather=None, surface_tilt=None, surface_azimuth=None, orientation_strategy=None, @@ -46,10 +48,16 @@ def basic_chain(times, latitude, longitude, Use decimal degrees notation. module_parameters : None, dict or Series - Module parameters as defined by the SAPM. + Module parameters as defined by the SAPM. See pvsystem.sapm for + details. + + temperature_model_parameters : None, dict or Series. + Temperature model parameters as defined by the SAPM. + See temperature.sapm_cell for details. inverter_parameters : None, dict or Series - Inverter parameters as defined by the CEC. + Inverter parameters as defined by the CEC. See pvsystem.snlinverter for + details. irradiance : None or DataFrame, default None If None, calculates clear sky data. @@ -163,15 +171,16 @@ def basic_chain(times, latitude, longitude, 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']) + cell_temperature = temperature.sapm_cell( + total_irrad['poa_global'], weather['temp_air'], weather['wind_speed'], + temperature_model_parameters['a'], temperature_model_parameters['b'], + temperature_model_parameters['deltaT']) effective_irradiance = pvsystem.sapm_effective_irradiance( total_irrad['poa_direct'], total_irrad['poa_diffuse'], airmass, aoi, module_parameters) - dc = pvsystem.sapm(effective_irradiance, temps['temp_cell'], + dc = pvsystem.sapm(effective_irradiance, cell_temperature, module_parameters) ac = pvsystem.snlinverter(dc['v_mp'], dc['p_mp'], inverter_parameters) @@ -272,9 +281,9 @@ class ModelChain(object): 'first_solar', 'no_loss'. The ModelChain instance will be passed as the first argument to a user-defined function. - temp_model: str or function, default 'sapm' - Valid strings are 'sapm'. The ModelChain instance will be passed - as the first argument to a user-defined function. + temperature_model: None, str or function, default None + Valid strings are 'sapm' and 'pvsyst'. The ModelChain instance will be + passed as the first argument to a user-defined function. losses_model: str or function, default 'no_loss' Valid strings are 'pvwatts', 'no_loss'. The ModelChain instance @@ -295,11 +304,12 @@ def __init__(self, system, location, solar_position_method='nrel_numpy', airmass_model='kastenyoung1989', dc_model=None, ac_model=None, aoi_model=None, - spectral_model=None, temp_model='sapm', + spectral_model=None, temperature_model=None, losses_model='no_loss', name=None, **kwargs): self.name = name self.system = system + self.location = location self.clearsky_model = clearsky_model self.transposition_model = transposition_model @@ -311,7 +321,24 @@ def __init__(self, system, location, self.ac_model = ac_model self.aoi_model = aoi_model self.spectral_model = spectral_model - self.temp_model = temp_model + + # TODO: deprecated kwarg temp_model. Remove use of temp_model in v0.8 + temp_model = kwargs.pop('temp_model', None) + if temp_model is not None: + warnings.warn('The temp_model keyword argument is deprecated. Use ' + 'temperature_model instead', pvlibDeprecationWarning) + if temperature_model is None: + temperature_model = temp_model + elif temp_model == temperature_model: + warnings.warn('Provide only one of temperature_model or ' + 'temp_model (deprecated).') + else: + raise ValueError( + 'Conflicting temperature_model {} and temp_model {}. ' + 'temp_model is deprecated. Specify only temperature_model.' + .format(temperature_model, temp_model)) + self.temperature_model = temperature_model + self.losses_model = losses_model self.orientation_strategy = orientation_strategy @@ -324,7 +351,7 @@ def __repr__(self): 'name', 'orientation_strategy', 'clearsky_model', 'transposition_model', 'solar_position_method', 'airmass_model', 'dc_model', 'ac_model', 'aoi_model', - 'spectral_model', 'temp_model', 'losses_model' + 'spectral_model', 'temperature_model', 'losses_model' ] def getmcattr(self, attr): @@ -420,7 +447,7 @@ def infer_dc_model(self): def sapm(self): self.dc = self.system.sapm(self.effective_irradiance/1000., - self.temps['temp_cell']) + self.cell_temperature) self.dc = self.system.scale_voltage_current_power(self.dc) @@ -430,7 +457,7 @@ def desoto(self): (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) = ( self.system.calcparams_desoto(self.effective_irradiance, - self.temps['temp_cell'])) + self.cell_temperature)) self.diode_params = (photocurrent, saturation_current, resistance_series, @@ -448,7 +475,7 @@ def cec(self): (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) = ( self.system.calcparams_cec(self.effective_irradiance, - self.temps['temp_cell'])) + self.cell_temperature)) self.diode_params = (photocurrent, saturation_current, resistance_series, @@ -466,7 +493,7 @@ def pvsyst(self): (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) = ( self.system.calcparams_pvsyst(self.effective_irradiance, - self.temps['temp_cell'])) + self.cell_temperature)) self.diode_params = (photocurrent, saturation_current, resistance_series, @@ -485,7 +512,7 @@ def singlediode(self): (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) = ( self.system.calcparams_desoto(self.effective_irradiance, - self.temps['temp_cell'])) + self.cell_temperature)) self.desoto = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) @@ -500,7 +527,7 @@ def singlediode(self): def pvwatts_dc(self): self.dc = self.system.pvwatts_dc(self.effective_irradiance, - self.temps['temp_cell']) + self.cell_temperature) return self @property @@ -659,29 +686,53 @@ def no_spectral_loss(self): return self @property - def temp_model(self): - return self._temp_model + def temperature_model(self): + return self._temperature_model - @temp_model.setter - def temp_model(self, model): + @temperature_model.setter + def temperature_model(self, model): if model is None: - self._temp_model = self.infer_temp_model() + self._temperature_model = self.infer_temperature_model() elif isinstance(model, str): model = model.lower() if model == 'sapm': - self._temp_model = self.sapm_temp + self._temperature_model = self.sapm_temp + elif model == 'pvsyst': + self._temperature_model = self.pvsyst_temp else: - raise ValueError(model + ' is not a valid temp model') + raise ValueError(model + ' is not a valid temperature model') + # check system.temperature_model_parameters for consistency + name_from_params = self.infer_temperature_model().__name__ + if self._temperature_model.__name__ != name_from_params: + raise ValueError( + 'Temperature model {} is inconsistent with ' + 'PVsystem.temperature_model_parameters {}'.format( + self._temperature_model.__name__, + self.system.temperature_model_parameters)) else: - self._temp_model = partial(model, self) - - def infer_temp_model(self): - raise NotImplementedError + self._temperature_model = partial(model, self) + + def infer_temperature_model(self): + params = set(self.system.temperature_model_parameters.keys()) + if set(['a', 'b', 'deltaT']) <= params: + return self.sapm_temp + elif set(['u_c', 'u_v']) <= params: + return self.pvsyst_temp + else: + raise ValueError('could not infer temperature model from ' + 'system.temperature_module_parameters {}.' + .format(self.system.temperature_model_parameters)) def sapm_temp(self): - self.temps = self.system.sapm_celltemp(self.total_irrad['poa_global'], - self.weather['wind_speed'], - self.weather['temp_air']) + self.cell_temperature = self.system.sapm_celltemp( + self.total_irrad['poa_global'], self.weather['temp_air'], + self.weather['wind_speed']) + return self + + def pvsyst_temp(self): + self.cell_temperature = self.system.pvsyst_celltemp( + self.total_irrad['poa_global'], self.weather['temp_air'], + self.weather['wind_speed']) return self @property @@ -918,7 +969,7 @@ def run_model(self, times=None, weather=None): self Assigns attributes: times, solar_position, airmass, irradiance, - total_irrad, effective_irradiance, weather, temps, aoi, + total_irrad, effective_irradiance, weather, cell_temperature, aoi, aoi_modifier, spectral_modifier, dc, ac, losses. """ @@ -926,7 +977,7 @@ def run_model(self, times=None, weather=None): self.aoi_model() self.spectral_model() self.effective_irradiance_model() - self.temp_model() + self.temperature_model() self.dc_model() self.losses_model() self.ac_model() diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 1a3969d725..5a00907b18 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -7,12 +7,17 @@ import io import os from urllib.request import urlopen +import warnings import numpy as np import pandas as pd -from pvlib import atmosphere, irradiance, tools, singlediode as _singlediode -from pvlib.tools import _build_kwargs, cosd +from pvlib._deprecation import deprecated + +from pvlib import (atmosphere, irradiance, singlediode as _singlediode, + temperature) +from pvlib.tools import _build_kwargs, cosd, asind, sind, tand from pvlib.location import Location +from pvlib._deprecation import pvlibDeprecationWarning # a dict of required parameter names for each DC power model @@ -40,16 +45,6 @@ } -TEMP_MODEL_PARAMS = { - 'sapm': {'open_rack_cell_glassback': (-3.47, -.0594, 3), - 'roof_mount_cell_glassback': (-2.98, -.0471, 1), - 'open_rack_cell_polymerback': (-3.56, -.0750, 3), - 'insulated_back_polymerback': (-2.81, -.0455, 0), - 'open_rack_polymer_thinfilm_steel': (-3.58, -.113, 3), - '22x_concentrator_tracker': (-3.23, -.130, 13)}, - 'pvsyst': {'freestanding': (29.0, 0), 'insulated': (15.0, 0)} -} - # 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. @@ -107,9 +102,16 @@ class PVSystem(object): May be used to look up the module_parameters dictionary via some other method. + module_type : None or string, default 'glass_polymer' + Describes the module's construction. Valid strings are 'glass_polymer' + and 'glass_glass'. Used for cell and module temperature calculations. + module_parameters : None, dict or Series, default None Module parameters as defined by the SAPM, CEC, or other. + temperature_model_parameters : None, dict or Series, default None. + Temperature model parameters as defined by the SAPM, Pvsyst, or other. + modules_per_string: int or float, default 1 See system topology discussion above. @@ -124,8 +126,9 @@ class PVSystem(object): inverter_parameters : None, dict or Series, default None Inverter parameters as defined by the SAPM, CEC, or other. - racking_model : None or string, default 'open_rack_cell_glassback' - Used for cell and module temperature calculations. + racking_model : None or string, default 'open_rack' + Valid strings are 'open_rack', 'close_mount', and 'insulated_back'. + Used to identify a parameter set for the SAPM cell temperature model. losses_parameters : None, dict or Series, default None Losses parameters as defined by PVWatts or other. @@ -146,13 +149,13 @@ class PVSystem(object): def __init__(self, surface_tilt=0, surface_azimuth=180, albedo=None, surface_type=None, - module=None, module_parameters=None, + module=None, module_type='glass_polymer', + module_parameters=None, + temperature_model_parameters=None, modules_per_string=1, strings_per_inverter=1, inverter=None, inverter_parameters=None, - racking_model='open_rack_cell_glassback', - losses_parameters=None, name=None, **kwargs): - - self.name = name + racking_model='open_rack', losses_parameters=None, name=None, + **kwargs): self.surface_tilt = surface_tilt self.surface_azimuth = surface_azimuth @@ -171,6 +174,31 @@ def __init__(self, else: self.module_parameters = module_parameters + self.module_type = module_type + self.racking_model = racking_model + + if temperature_model_parameters is None: + self.temperature_model_parameters = \ + self._infer_temperature_model_params() + # TODO: in v0.8 check if an empty dict is returned and raise error + else: + self.temperature_model_parameters = temperature_model_parameters + + # TODO: deprecated behavior if PVSystem.temperature_model_parameters + # are not specified. Remove in v0.8 + if not any(self.temperature_model_parameters): + warnings.warn( + 'Required temperature_model_parameters is not specified ' + 'and parameters are not inferred from racking_model and ' + 'module_type. Reverting to deprecated default: SAPM cell ' + 'temperature model parameters for a glass/glass module in ' + 'open racking. In the future ' + 'PVSystem.temperature_model_parameters will be required', + pvlibDeprecationWarning) + params = temperature._temperature_model_params( + 'sapm', 'open_rack_glass_glass') + self.temperature_model_parameters = params + self.modules_per_string = modules_per_string self.strings_per_inverter = strings_per_inverter @@ -185,7 +213,7 @@ def __init__(self, else: self.losses_parameters = losses_parameters - self.racking_model = racking_model + self.name = name def __repr__(self): attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module', @@ -404,24 +432,15 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`sapm` function, the input parameters, and ``self.module_parameters`` to calculate - Voc, Isc, Ix, Ixx, Vmp/Imp. + Voc, Isc, Ix, Ixx, Vmp, and Imp. Parameters ---------- - poa_direct : Series - The direct irradiance incident upon the module (W/m^2). - - poa_diffuse : Series - The diffuse irradiance incident on module. - - temp_cell : Series - The cell temperature (degrees C). - - airmass_absolute : Series - Absolute airmass. + effective_irradiance : numeric + The irradiance (W/m2) that is converted to photocurrent. - aoi : Series - Angle of incidence (degrees). + temp_cell : float or Series + The average cell temperature of cells within a module in C. kwargs See pvsystem.sapm for details @@ -432,20 +451,44 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): """ return sapm(effective_irradiance, temp_cell, self.module_parameters) - def sapm_celltemp(self, irrad, wind, temp): - """Uses :py:func:`sapm_celltemp` to calculate module and cell - temperatures based on ``self.racking_model`` and - the input parameters. + def sapm_celltemp(self, poa_global, temp_air, wind_speed): + """Uses :py:func:`temperature.sapm_cell` to calculate cell + temperatures. Parameters ---------- - See pvsystem.sapm_celltemp for details + poa_global : numeric + Total incident irradiance in W/m^2. + + temp_air : numeric + Ambient dry bulb temperature in degrees C. + + wind_speed : numeric + Wind speed in m/s at a height of 10 meters. Returns ------- - See pvsystem.sapm_celltemp for details + numeric, values in degrees C. """ - return sapm_celltemp(irrad, wind, temp, self.racking_model) + kwargs = _build_kwargs(['a', 'b', 'deltaT'], + self.temperature_model_parameters) + return temperature.sapm_cell(poa_global, temp_air, wind_speed, + **kwargs) + + def _infer_temperature_model_params(self): + # try to infer temperature model parameters from from racking_model + # and module_type + param_set = self.racking_model + '_' + self.module_type + if param_set in temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']: + return temperature._temperature_model_params('sapm', param_set) + elif 'freestanding' in param_set: + return temperature._temperature_model_params('pvsyst', + 'freestanding') + elif 'insulated' in param_set: # after SAPM to avoid confusing keys + return temperature._temperature_model_params('pvsyst', + 'insulated') + else: + return {} def sapm_spectral_loss(self, airmass_absolute): """ @@ -516,21 +559,39 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, self.module_parameters, reference_irradiance=reference_irradiance) def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): - """Uses :py:func:`pvsyst_celltemp` to calculate module temperatures - based on ``self.racking_model`` and the input parameters. + """Uses :py:func:`temperature.pvsyst_cell` to calculate cell + temperature. Parameters ---------- - See pvsystem.pvsyst_celltemp for details + poa_global : numeric + Total incident irradiance in W/m^2. + + temp_air : numeric + Ambient dry bulb temperature in degrees C. + + wind_speed : numeric, default 1.0 + Wind speed in m/s measured at the same height for which the wind + loss factor was determined. The default value is 1.0, which is + the wind speed at module height used to determine NOCT. + + eta_m : numeric, default 0.1 + Module external efficiency as a fraction, i.e., + DC power / poa_global. + + alpha_absorption : numeric, default 0.9 + Absorption coefficient Returns ------- - See pvsystem.pvsyst_celltemp for details + numeric, values in degrees C. """ kwargs = _build_kwargs(['eta_m', 'alpha_absorption'], self.module_parameters) - return pvsyst_celltemp(poa_global, temp_air, wind_speed, - model_params=self.racking_model, **kwargs) + kwargs.update(_build_kwargs(['u_c', 'u_v'], + self.temperature_model_parameters)) + return temperature.pvsyst_cell(poa_global, temp_air, wind_speed, + **kwargs) def first_solar_spectral_loss(self, pw, airmass_absolute): @@ -1020,20 +1081,18 @@ def physicaliam(aoi, n=1.526, K=4., L=0.002): aoi = np.where(aoi == 0, zeroang, aoi) # angle of reflection - thetar_deg = tools.asind(1.0 / n*(tools.sind(aoi))) + thetar_deg = asind(1.0 / n * (sind(aoi))) # reflectance and transmittance for normal incidence light rho_zero = ((1-n) / (1+n)) ** 2 tau_zero = np.exp(-K*L) # reflectance for parallel and perpendicular polarized light - rho_para = (tools.tand(thetar_deg - aoi) / - tools.tand(thetar_deg + aoi)) ** 2 - rho_perp = (tools.sind(thetar_deg - aoi) / - tools.sind(thetar_deg + aoi)) ** 2 + rho_para = (tand(thetar_deg - aoi) / tand(thetar_deg + aoi)) ** 2 + rho_perp = (sind(thetar_deg - aoi) / sind(thetar_deg + aoi)) ** 2 # transmittance for non-normal light - tau = np.exp(-K*L / tools.cosd(thetar_deg)) + tau = np.exp(-K * L / cosd(thetar_deg)) # iam is ratio of non-normal to normal incidence transmitted light # after deducting the reflected portion of each @@ -1901,7 +1960,8 @@ def sapm(effective_irradiance, temp_cell, module): See Also -------- retrieve_sam - sapm_celltemp + temperature.sapm_cell + temperature.sapm_module ''' T0 = 25 @@ -1965,174 +2025,83 @@ def sapm(effective_irradiance, temp_cell, module): return out -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 - irradiance, wind speed, ambient temperature, and SAPM module - parameters. - - Parameters - ---------- - poa_global : float or Series - Total incident irradiance in W/m^2. - - wind_speed : float or Series - Wind speed in m/s at a height of 10 meters. - - temp_air : float or Series - Ambient dry bulb temperature in degrees C. - - model : string, list, or dict, default 'open_rack_cell_glassback' - Model to be used. - - If string, can be: - - * 'open_rack_cell_glassback' (default) - * 'roof_mount_cell_glassback' - * 'open_rack_cell_polymerback' - * '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 - high solar irradiance. - - * b : float - SAPM module parameter for establishing the rate at - which the module temperature drops as wind speed increases - (see SAPM eqn. 11). - - * deltaT : float - SAPM module parameter giving the temperature difference - between the cell and module back surface at the - reference irradiance, E0. - - Returns - -------- - DataFrame with columns 'temp_cell' and 'temp_module'. - Values in degrees C. - - References - ---------- - [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance - Model", SAND Report 3535, Sandia National Laboratories, Albuquerque, - NM. - - See Also - -------- - sapm - ''' - - temp_models = TEMP_MODEL_PARAMS['sapm'] - - if isinstance(model, str): - model = temp_models[model.lower()] - - 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(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 pvsyst_celltemp(poa_global, temp_air, wind_speed=1.0, eta_m=0.1, - alpha_absorption=0.9, model_params='freestanding'): - """ - Calculate cell temperature using an emperical heat loss factor model - as implemented in PVsyst. - - The heat loss factors provided through the 'model_params' argument - represent the combined effect of convection, radiation and conduction, - and their values are experimentally determined. - - Parameters - ---------- - poa_global : numeric - Total incident irradiance in W/m^2. - - temp_air : numeric - Ambient dry bulb temperature in degrees C. - - wind_speed : numeric, default 1.0 - Wind speed in m/s measured at the same height for which the wind loss - factor was determined. The default value is 1.0, which is the wind - speed at module height used to determine NOCT. - - eta_m : numeric, default 0.1 - Module external efficiency as a fraction, i.e., DC power / poa_global. - - alpha_absorption : numeric, default 0.9 - Absorption coefficient - - model_params : string, tuple, or list (no dict), default 'freestanding' - Heat loss factors to be used. - - If string, can be: - - * 'freestanding' (default) - Modules with rear surfaces exposed to open air (e.g. rack - mounted). - * 'insulated' - Modules with rear surfaces in close proximity to another - surface (e.g. roof mounted). - - If tuple/list, supply parameters in the following order: - - * constant_loss_factor : float - Combined heat loss factor coefficient. Freestanding - default is 29, fully insulated arrays is 15. - - * wind_loss_factor : float - Combined heat loss factor influenced by wind. Default is 0. - - Returns - ------- - temp_cell : numeric or Series - Cell temperature in degrees Celsius - - References - ---------- - [1]"PVsyst 6 Help", Files.pvsyst.com, 2018. [Online]. Available: - http://files.pvsyst.com/help/index.html. [Accessed: 10- Dec- 2018]. - - [2] Faiman, D. (2008). "Assessing the outdoor operating temperature of - photovoltaic modules." Progress in Photovoltaics 16(4): 307-315. - """ - - pvsyst_presets = TEMP_MODEL_PARAMS['pvsyst'] - - if isinstance(model_params, str): - model_params = model_params.lower() - constant_loss_factor, wind_loss_factor = pvsyst_presets[model_params] - elif isinstance(model_params, (tuple, list)): - constant_loss_factor, wind_loss_factor = model_params - else: - raise TypeError( - "Please provide model_params as a str, or tuple/list." - ) - - total_loss_factor = wind_loss_factor * wind_speed + constant_loss_factor - heat_input = poa_global * alpha_absorption * (1 - eta_m) - temp_difference = heat_input / total_loss_factor - temp_cell = temp_air + temp_difference - - return temp_cell +def _sapm_celltemp_translator(*args, **kwargs): + # TODO: remove this function after deprecation period for sapm_celltemp + new_kwargs = {} + # convert position arguments to kwargs + old_arg_list = ['poa_global', 'wind_speed', 'temp_air', 'model'] + for pos in range(len(args)): + new_kwargs[old_arg_list[pos]] = args[pos] + # determine value for new kwarg 'model' + try: + param_set = new_kwargs['model'] + new_kwargs.pop('model') # model is not a new kwarg + except KeyError: + # 'model' not in positional arguments, check kwargs + try: + param_set = kwargs['model'] + kwargs.pop('model') + except KeyError: + # 'model' not in kwargs, use old default value + param_set = 'open_rack_glass_glass' + if type(param_set) is list: + new_kwargs.update({'a': param_set[0], + 'b': param_set[1], + 'deltaT': param_set[2]}) + elif type(param_set) is dict: + new_kwargs.update(param_set) + else: # string + params = temperature._temperature_model_params('sapm', param_set) + new_kwargs.update(params) + new_kwargs.update(kwargs) # kwargs with unchanged names + new_kwargs['irrad_ref'] = 1000 # default for new kwarg + # convert old positional arguments to named kwargs + return temperature.sapm_cell(**new_kwargs) + + +sapm_celltemp = deprecated('0.7', alternative='temperature.sapm_cell', + name='sapm_celltemp', removal='0.8', + addendum='Note that the arguments and argument ' + 'order for temperature.sapm_cell are different ' + 'than for sapm_celltemp')(_sapm_celltemp_translator) + + +def _pvsyst_celltemp_translator(*args, **kwargs): + # TODO: remove this function after deprecation period for pvsyst_celltemp + new_kwargs = {} + # convert position arguments to kwargs + old_arg_list = ['poa_global', 'temp_air', 'wind_speed', 'eta_m', + 'alpha_absorption', 'model_params'] + for pos in range(len(args)): + new_kwargs[old_arg_list[pos]] = args[pos] + # determine value for new kwarg 'model' + try: + param_set = new_kwargs['model_params'] + new_kwargs.pop('model_params') # model_params is not a new kwarg + except KeyError: + # 'model_params' not in positional arguments, check kwargs + try: + param_set = kwargs['model_params'] + kwargs.pop('model_params') + except KeyError: + # 'model_params' not in kwargs, use old default value + param_set = 'freestanding' + if type(param_set) in (list, tuple): + new_kwargs.update({'u_c': param_set[0], + 'u_v': param_set[1]}) + else: # string + params = temperature._temperature_model_params('pvsyst', param_set) + new_kwargs.update(params) + new_kwargs.update(kwargs) # kwargs with unchanged names + # convert old positional arguments to named kwargs + return temperature.pvsyst_cell(**new_kwargs) + + +pvsyst_celltemp = deprecated( + '0.7', alternative='temperature.pvsyst_cell', name='pvsyst_celltemp', + removal='0.8', addendum='Note that the argument names for ' + 'temperature.pvsyst_cell are different than ' + 'for pvsyst_celltemp')(_pvsyst_celltemp_translator) def sapm_spectral_loss(airmass_absolute, module): diff --git a/pvlib/temperature.py b/pvlib/temperature.py new file mode 100644 index 0000000000..f43d372a3a --- /dev/null +++ b/pvlib/temperature.py @@ -0,0 +1,271 @@ +""" +The ``temperature`` module contains functions for modeling temperature of +PV modules and cells. +""" + +import numpy as np + + +TEMPERATURE_MODEL_PARAMETERS = { + 'sapm': { + 'open_rack_glass_glass': {'a': -3.47, 'b': -.0594, 'deltaT': 3}, + 'close_mount_glass_glass': {'a': -2.98, 'b': -.0471, 'deltaT': 1}, + 'open_rack_glass_polymer': {'a': -3.56, 'b': -.0750, 'deltaT': 3}, + 'insulated_back_glass_polymer': {'a': -2.81, 'b': -.0455, 'deltaT': 0}, + }, + 'pvsyst': {'freestanding': {'u_c': 29.0, 'u_v': 0}, + 'insulated': {'u_c': 15.0, 'u_v': 0}} +} + + +def _temperature_model_params(model, parameter_set): + try: + params = TEMPERATURE_MODEL_PARAMETERS[model] + return params[parameter_set] + except KeyError: + msg = ('{} is not a named set of parameters for the {} cell' + ' temperature model.' + ' See pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS' + ' for names'.format(parameter_set, model)) + raise KeyError(msg) + + +def sapm_cell(poa_global, temp_air, wind_speed, a, b, deltaT, + irrad_ref=1000): + r''' + Calculate cell temperature per the Sandia PV Array Performance Model [1]. + + Parameters + ---------- + poa_global : numeric + Total incident irradiance [W/m^2]. + + temp_air : numeric + Ambient dry bulb temperature [C]. + + wind_speed : numeric + Wind speed at a height of 10 meters [m/s]. + + a : float + Parameter :math:`a` in :eq:`sapm1`. + + b : float + Parameter :math:`b` in :eq:`sapm1`. + + deltaT : float + Parameter :math:`\Delta T` in :eq:`sapm2` [C]. + + irrad_ref : float, default 1000 + Reference irradiance, parameter :math:`E_{0}` in + :eq:`sapm2` [W/m^2]. + + Returns + ------- + numeric, values in degrees C. + + Notes + ----- + The model for cell temperature :math:`T_{C}` is given by a pair of + equations (Eq. 11 and 12 in [1]). + + .. math:: + :label: sapm1 + + T_{m} = E \times \exp (a + b \times WS) + T_{a} + + .. math:: + :label: sapm2 + + T_{C} = T_{m} + \frac{E}{E_{0}} \Delta T + + The module back surface temperature :math:`T_{m}` is implemented in + :py:func:`~pvlib.temperature.sapm_module`. + + Inputs to the model are plane-of-array irradiance :math:`E` (W/m2) and + ambient air temperature :math:`T_{a}` (C). Model parameters depend both on + the module construction and its mounting. Parameter sets are provided in + [1] for representative modules and mounting, and are coded for convenience + in ``pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS``. + + +---------------+----------------+-------+---------+---------------------+ + | Module | Mounting | a | b | :math:`\Delta T [C]`| + +===============+================+=======+=========+=====================+ + | glass/glass | open rack | -3.47 | -0.0594 | 3 | + +---------------+----------------+-------+---------+---------------------+ + | glass/glass | close roof | -2.98 | -0.0471 | 1 | + +---------------+----------------+-------+---------+---------------------+ + | glass/polymer | open rack | -3.56 | -0.075 | 3 | + +---------------+----------------+-------+---------+---------------------+ + | glass/polymer | insulated back | -2.81 | -0.0455 | 0 | + +---------------+----------------+-------+---------+---------------------+ + + References + ---------- + [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance + Model", SAND Report 3535, Sandia National Laboratories, Albuquerque, + NM. + + Examples + -------- + >>> from pvlib.temperature import sapm_cell, TEMPERATURE_MODEL_PARAMETERS + >>> params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'] + >>> sapm_cell(1000, 10, 0, **params) + 44.11703066106086 + ''' + module_temperature = sapm_module(poa_global, temp_air, wind_speed, + a, b) + return module_temperature + (poa_global / irrad_ref) * deltaT + + +def sapm_module(poa_global, temp_air, wind_speed, a, b): + r''' + Calculate module back surface temperature per the Sandia PV Array + Performance Model [1]. + + Parameters + ---------- + poa_global : numeric + Total incident irradiance [W/m^2]. + + temp_air : numeric + Ambient dry bulb temperature [C]. + + wind_speed : numeric + Wind speed at a height of 10 meters [m/s]. + + a : float + Parameter :math:`a` in :eq:`sapm1mod`. + + b : float + Parameter :math:`b` in :eq:`sapm1mod`. + + Returns + ------- + numeric, values in degrees C. + + Notes + ----- + The model for module temperature :math:`T_{m}` is given by Eq. 11 in [1]. + + .. math:: + :label: sapm1mod + + T_{m} = E \times \exp (a + b \times WS) + T_{a} + + Inputs to the model are plane-of-array irradiance :math:`E` (W/m2) and + ambient air temperature :math:`T_{a}` (C). Model outputs are surface + temperature at the back of the module :math:`T_{m}` and cell temperature + :math:`T_{C}`. Model parameters depend both on the module construction and + its mounting. Parameter sets are provided in [1] for representative modules + and mounting, and are coded for convenience in + ``temperature.TEMPERATURE_MODEL_PARAMETERS``. + + +---------------+----------------+-------+---------+---------------------+ + | Module | Mounting | a | b | :math:`\Delta T [C]`| + +===============+================+=======+=========+=====================+ + | glass/glass | open rack | -3.47 | -0.0594 | 3 | + +---------------+----------------+-------+---------+---------------------+ + | glass/glass | close roof | -2.98 | -0.0471 | 1 | + +---------------+----------------+-------+---------+---------------------+ + | glass/polymer | open rack | -3.56 | -0.075 | 3 | + +---------------+----------------+-------+---------+---------------------+ + | glass/polymer | insulated back | -2.81 | -0.0455 | 0 | + +---------------+----------------+-------+---------+---------------------+ + + References + ---------- + [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance + Model", SAND Report 3535, Sandia National Laboratories, Albuquerque, + NM. + + ''' + return poa_global * np.exp(a + b * wind_speed) + temp_air + + +def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0, + eta_m=0.1, alpha_absorption=0.9): + r""" + Calculate cell temperature using an empirical heat loss factor model + as implemented in PVsyst. + + Parameters + ---------- + poa_global : numeric + Total incident irradiance [W/m^2]. + + temp_air : numeric + Ambient dry bulb temperature [C]. + + wind_speed : numeric, default 1.0 + Wind speed in m/s measured at the same height for which the wind loss + factor was determined. The default value 1.0 m/2 is the wind + speed at module height used to determine NOCT. [m/s] + + u_c : float, default 29.0 + Combined heat loss factor coefficient. The default value is + representative of freestanding modules with the rear surfaces exposed + to open air (e.g., rack mounted). Parameter :math:`U_{c}` in + :eq:`pvsyst` [W/(m^2 C)]. + + u_v : float, default 0.0 + Combined heat loss factor influenced by wind. Parameter :math:`U_{v}` + in :eq:`pvsyst` [(W/m^2 C)(m/s)]. + + eta_m : numeric, default 0.1 + Module external efficiency as a fraction, i.e., DC power / poa_global. + Parameter :math:`\eta_{m}` in :eq:`pvsyst`. + + alpha_absorption : numeric, default 0.9 + Absorption coefficient. Parameter :math:`\alpha` in :eq:`pvsyst`. + + Returns + ------- + numeric, values in degrees Celsius + + Notes + ----- + The Pvsyst model for cell temperature :math:`T_{C}` is given by + + .. math:: + :label: pvsyst + + T_{C} = T_{a} + \frac{\alpha E (1 - \eta_{m})}{U_{c} + U_{v} \times WS} + + Inputs to the model are plane-of-array irradiance :math:`E` (W/m2), ambient + air temperature :math:`T_{a}` (C) and wind speed :math:`WS` (m/s). Model + output is cell temperature :math:`T_{C}`. Model parameters depend both on + the module construction and its mounting. Parameters are provided in + [1] for open (freestanding) and close (insulated) mounting configurations, + , and are coded for convenience in + ``temperature.TEMPERATURE_MODEL_PARAMETERS``. The heat loss factors + provided represent the combined effect of convection, radiation and + conduction, and their values are experimentally determined. + + +--------------+---------------+---------------+ + | Mounting | :math:`U_{c}` | :math:`U_{v}` | + +==============+===============+===============+ + | freestanding | 29.0 | 0.0 | + +--------------+---------------+---------------+ + | insulated | 15.0 | 0.0 | + +--------------+---------------+---------------+ + + References + ---------- + [1]"PVsyst 6 Help", Files.pvsyst.com, 2018. [Online]. Available: + http://files.pvsyst.com/help/index.html. [Accessed: 10- Dec- 2018]. + + [2] Faiman, D. (2008). "Assessing the outdoor operating temperature of + photovoltaic modules." Progress in Photovoltaics 16(4): 307-315. + + Examples + -------- + >>> from pvlib.temperature import pvsyst_cell, TEMPERATURE_MODEL_PARAMETERS + >>> params = TEMPERATURE_MODEL_PARAMETERS['pvsyst']['freestanding'] + >>> pvsyst_cell(1000, 10, **params) + 37.93103448275862 + """ + + total_loss_factor = u_c + u_v * wind_speed + heat_input = poa_global * alpha_absorption * (1 - eta_m) + temp_difference = heat_input / total_loss_factor + return temp_air + temp_difference diff --git a/pvlib/test/conftest.py b/pvlib/test/conftest.py index ee50b5277e..1c690c2555 100644 --- a/pvlib/test/conftest.py +++ b/pvlib/test/conftest.py @@ -349,3 +349,10 @@ def cec_module_fs_495(): 'Technology': 'CdTe', } return parameters + + +@pytest.fixture(scope='function') +def sapm_temperature_cs5p_220m(): + # SAPM temperature model parameters for Canadian_Solar_CS5P_220M + # (glass/polymer) in open rack + return {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3} diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index fd2ef40f79..20a8a956bc 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -3,7 +3,7 @@ import numpy as np import pandas as pd -from pvlib import modelchain, pvsystem +from pvlib import modelchain, pvsystem, temperature from pvlib.modelchain import ModelChain from pvlib.pvsystem import PVSystem from pvlib.tracking import SingleAxisTracker @@ -17,63 +17,77 @@ @pytest.fixture -def system(sam_data, cec_inverter_parameters): +def system(sam_data, cec_inverter_parameters, sapm_temperature_cs5p_220m): modules = sam_data['sandiamod'] module = 'Canadian_Solar_CS5P_220M___2009_' module_parameters = modules[module].copy() + temp_model_params = sapm_temperature_cs5p_220m.copy() system = PVSystem(surface_tilt=32.2, surface_azimuth=180, module=module, module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, inverter_parameters=cec_inverter_parameters) return system @pytest.fixture -def cec_dc_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters): +def cec_dc_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters, + sapm_temperature_cs5p_220m): module_parameters = cec_module_cs5p_220m.copy() module_parameters['b'] = 0.05 module_parameters['EgRef'] = 1.121 module_parameters['dEgdT'] = -0.0002677 + temp_model_params = sapm_temperature_cs5p_220m.copy() system = PVSystem(surface_tilt=32.2, surface_azimuth=180, module=module_parameters['Name'], module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, inverter_parameters=cec_inverter_parameters) return system @pytest.fixture -def cec_dc_native_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters): +def cec_dc_native_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters, + sapm_temperature_cs5p_220m): module_parameters = cec_module_cs5p_220m.copy() + temp_model_params = sapm_temperature_cs5p_220m.copy() system = PVSystem(surface_tilt=32.2, surface_azimuth=180, module=module_parameters['Name'], module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, inverter_parameters=cec_inverter_parameters) return system @pytest.fixture -def pvsyst_dc_snl_ac_system(pvsyst_module_params, cec_inverter_parameters): +def pvsyst_dc_snl_ac_system(pvsyst_module_params, cec_inverter_parameters, + sapm_temperature_cs5p_220m): module = 'PVsyst test module' module_parameters = pvsyst_module_params module_parameters['b'] = 0.05 + temp_model_params = sapm_temperature_cs5p_220m.copy() system = PVSystem(surface_tilt=32.2, surface_azimuth=180, module=module, module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, inverter_parameters=cec_inverter_parameters) return system @pytest.fixture -def cec_dc_adr_ac_system(sam_data, cec_module_cs5p_220m): +def cec_dc_adr_ac_system(sam_data, cec_module_cs5p_220m, + sapm_temperature_cs5p_220m): module_parameters = cec_module_cs5p_220m.copy() module_parameters['b'] = 0.05 module_parameters['EgRef'] = 1.121 module_parameters['dEgdT'] = -0.0002677 + temp_model_params = sapm_temperature_cs5p_220m.copy() inverters = sam_data['adrinverter'] inverter = inverters['Zigor__Sunzet_3_TL_US_240V__CEC_2011_'].copy() system = PVSystem(surface_tilt=32.2, surface_azimuth=180, module=module_parameters['Name'], module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, inverter_parameters=inverter) return system @@ -88,11 +102,13 @@ def pvwatts_dc_snl_ac_system(cec_inverter_parameters): @pytest.fixture -def pvwatts_dc_pvwatts_ac_system(sam_data): +def pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m): module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} + temp_model_params = sapm_temperature_cs5p_220m.copy() inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} system = PVSystem(surface_tilt=32.2, surface_azimuth=180, module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, inverter_parameters=inverter_parameters) return system @@ -136,7 +152,7 @@ def test_run_model(system, location): with pytest.warns(pvlibDeprecationWarning): ac = mc.run_model(times).ac - expected = pd.Series(np.array([ 183.522449305, -2.00000000e-02]), + expected = pd.Series(np.array([183.522449305, -2.00000000e-02]), index=times) assert_series_equal(ac, expected, check_less_precise=1) @@ -148,7 +164,7 @@ def test_run_model_with_irradiance(system, location): index=times) ac = mc.run_model(times, weather=irradiance).ac - expected = pd.Series(np.array([ 1.90054749e+02, -2.00000000e-02]), + expected = pd.Series(np.array([187.80746495, -2.00000000e-02]), index=times) assert_series_equal(ac, expected) @@ -160,7 +176,7 @@ def test_run_model_perez(system, location): index=times) ac = mc.run_model(times, weather=irradiance).ac - expected = pd.Series(np.array([ 190.194545796, -2.00000000e-02]), + expected = pd.Series(np.array([187.94295642, -2.00000000e-02]), index=times) assert_series_equal(ac, expected) @@ -173,28 +189,46 @@ def test_run_model_gueymard_perez(system, location): index=times) ac = mc.run_model(times, weather=irradiance).ac - expected = pd.Series(np.array([ 190.194760203, -2.00000000e-02]), + expected = pd.Series(np.array([187.94317405, -2.00000000e-02]), index=times) assert_series_equal(ac, expected) def test_run_model_with_weather(system, location, weather, mocker): - mc = ModelChain(system, location) - m = mocker.spy(system, 'sapm_celltemp') weather['wind_speed'] = 5 weather['temp_air'] = 10 + # test with sapm cell temperature model + system.racking_model = 'open_rack' + system.module_type = 'glass_glass' + mc = ModelChain(system, location) + mc.temperature_model = 'sapm' + m_sapm = mocker.spy(system, 'sapm_celltemp') mc.run_model(weather.index, weather=weather) - assert m.call_count == 1 + assert m_sapm.call_count == 1 # assert_called_once_with cannot be used with series, so need to use # assert_series_equal on call_args - assert_series_equal(m.call_args[0][1], weather['wind_speed']) # wind - assert_series_equal(m.call_args[0][2], weather['temp_air']) # temp + assert_series_equal(m_sapm.call_args[0][1], weather['temp_air']) # temp + assert_series_equal(m_sapm.call_args[0][2], weather['wind_speed']) # wind + assert not mc.ac.empty + # test with pvsyst cell temperature model + system.racking_model = 'freestanding' + system.temperature_model_parameters = \ + temperature._temperature_model_params('pvsyst', 'freestanding') + mc = ModelChain(system, location) + mc.temperature_model = 'pvsyst' + m_pvsyst = mocker.spy(system, 'pvsyst_celltemp') + mc.run_model(weather.index, weather=weather) + assert m_pvsyst.call_count == 1 + assert_series_equal(m_pvsyst.call_args[0][1], weather['temp_air']) + assert_series_equal(m_pvsyst.call_args[0][2], weather['wind_speed']) assert not mc.ac.empty def test_run_model_tracker(system, location, weather, mocker): - system = SingleAxisTracker(module_parameters=system.module_parameters, - inverter_parameters=system.inverter_parameters) + system = SingleAxisTracker( + module_parameters=system.module_parameters, + temperature_model_parameters=system.temperature_model_parameters, + inverter_parameters=system.inverter_parameters) mocker.spy(system, 'singleaxis') mc = ModelChain(system, location) mc.run_model(weather.index, weather=weather) @@ -232,13 +266,24 @@ def test_infer_dc_model(system, cec_dc_snl_ac_system, pvsyst_dc_snl_ac_system, 'pvsyst': 'calcparams_pvsyst', 'singlediode': 'calcparams_desoto', 'pvwatts_dc': 'pvwatts_dc'} + temp_model_function = {'sapm': 'sapm', + 'cec': 'sapm', + 'desoto': 'sapm', + 'pvsyst': 'pvsyst', + 'singlediode': 'sapm', + 'pvwatts_dc': 'sapm'} + temp_model_params = {'sapm': {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3}, + 'pvsyst': {'u_c': 29.0, 'u_v': 0}} system = dc_systems[dc_model] + system.temperature_model_parameters = temp_model_params[ + temp_model_function[dc_model]] # remove Adjust from model parameters for desoto, singlediode if dc_model in ['desoto', 'singlediode']: system.module_parameters.pop('Adjust') m = mocker.spy(system, dc_model_function[dc_model]) mc = ModelChain(system, location, - aoi_model='no_loss', spectral_model='no_loss') + aoi_model='no_loss', spectral_model='no_loss', + temperature_model=temp_model_function[dc_model]) mc.run_model(weather.index, weather=weather) assert m.call_count == 1 assert isinstance(mc.dc, (pd.Series, pd.DataFrame)) @@ -259,6 +304,35 @@ def test_infer_spectral_model(location, system, cec_dc_snl_ac_system, assert isinstance(mc, ModelChain) +@pytest.mark.parametrize('temp_model', [ + 'sapm', pytest.param('pvsyst', marks=requires_scipy)]) +def test_infer_temp_model(location, system, pvsyst_dc_snl_ac_system, + temp_model): + dc_systems = {'sapm': system, + 'pvsyst': pvsyst_dc_snl_ac_system} + system = dc_systems[temp_model] + mc = ModelChain(system, location, + orientation_strategy='None', aoi_model='physical', + spectral_model='no_loss') + assert isinstance(mc, ModelChain) + + +@requires_scipy +def test_infer_temp_model_invalid(location, system): + system.temperature_model_parameters.pop('a') + with pytest.raises(ValueError): + ModelChain(system, location, orientation_strategy='None', + aoi_model='physical', spectral_model='no_loss') + + +@requires_scipy +def test_temperature_model_inconsistent(location, system): + with pytest.raises(ValueError): + ModelChain(system, location, orientation_strategy='None', + aoi_model='physical', spectral_model='no_loss', + temperature_model='pvsyst') + + def test_dc_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather, mocker): m = mocker.spy(sys.modules[__name__], 'poadc') @@ -422,7 +496,7 @@ def test_invalid_dc_model_params(system, cec_dc_snl_ac_system, pvwatts_dc_pvwatts_ac_system, location): kwargs = {'dc_model': 'sapm', 'ac_model': 'snlinverter', 'aoi_model': 'no_loss', 'spectral_model': 'no_loss', - 'temp_model': 'sapm', 'losses_model': 'no_loss'} + 'temperature_model': 'sapm', 'losses_model': 'no_loss'} system.module_parameters.pop('A0') # remove a parameter with pytest.raises(ValueError): ModelChain(system, location, **kwargs) @@ -440,13 +514,13 @@ def test_invalid_dc_model_params(system, cec_dc_snl_ac_system, @pytest.mark.parametrize('model', [ - 'dc_model', 'ac_model', 'aoi_model', 'spectral_model', 'losses_model', - 'temp_model', 'losses_model' + 'dc_model', 'ac_model', 'aoi_model', 'spectral_model', + 'temperature_model', 'losses_model' ]) def test_invalid_models(model, system, location): kwargs = {'dc_model': 'pvwatts', 'ac_model': 'pvwatts', 'aoi_model': 'no_loss', 'spectral_model': 'no_loss', - 'temp_model': 'sapm', 'losses_model': 'no_loss'} + 'temperature_model': 'sapm', 'losses_model': 'no_loss'} kwargs[model] = 'invalid' with pytest.raises(ValueError): ModelChain(system, location, **kwargs) @@ -464,11 +538,47 @@ def test_deprecated_07(): # does not matter what the parameters are, just fake it until we make it module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1, 'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1} - system = PVSystem(module_parameters=module_parameters) + temp_model_params = {'a': -3.5, 'b': -0.05, 'deltaT': 3} + system = PVSystem(module_parameters=module_parameters, + temperature_model_parameters=temp_model_params) with pytest.warns(pvlibDeprecationWarning): ModelChain(system, location, dc_model='singlediode', # this should fail after 0.7 aoi_model='no_loss', spectral_model='no_loss', + temperature_model='sapm', + ac_model='snlinverter') + + +@fail_on_pvlib_version('0.8') +def test_deprecated_08(): + # explicit system creation call because fail_on_pvlib_version + # does not support decorators. + # does not matter what the parameters are, just fake it until we make it + module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1, + 'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1} + # do not assign PVSystem.temperature_model_parameters + system = PVSystem(module_parameters=module_parameters) + with pytest.warns(pvlibDeprecationWarning): + ModelChain(system, location, + dc_model='desoto', + aoi_model='no_loss', spectral_model='no_loss', + temp_model='sapm', + ac_model='snlinverter') + system = PVSystem(module_parameters=module_parameters) + with pytest.warns(pvlibDeprecationWarning): + ModelChain(system, location, + dc_model='desoto', + aoi_model='no_loss', spectral_model='no_loss', + temperature_model='sapm', + temp_model='sapm', + ac_model='snlinverter') + system = PVSystem(module_parameters=module_parameters) + with pytest.raises(ValueError): + ModelChain(system, location, + dc_model='desoto', + aoi_model='no_loss', spectral_model='no_loss', + temperature_model='pvsyst', + temp_model='sapm', ac_model='snlinverter') @@ -477,7 +587,9 @@ def test_deprecated_07(): def test_deprecated_clearsky_07(): # explicit system creation call because fail_on_pvlib_version # does not support decorators. - system = PVSystem(module_parameters={'pdc0': 1, 'gamma_pdc': -0.003}) + system = PVSystem(module_parameters={'pdc0': 1, 'gamma_pdc': -0.003}, + temperature_model_parameters={'a': -3.5, 'b': -0.05, + 'deltaT': 3}) location = Location(32.2, -110.9) mc = ModelChain(system, location, dc_model='pvwatts', ac_model='pvwatts', aoi_model='no_loss', spectral_model='no_loss') @@ -488,7 +600,8 @@ def test_deprecated_clearsky_07(): @requires_scipy -def test_basic_chain_required(sam_data, cec_inverter_parameters): +def test_basic_chain_required(sam_data, cec_inverter_parameters, + sapm_temperature_cs5p_220m): times = pd.date_range(start='20160101 1200-0700', end='20160101 1800-0700', freq='6H') latitude = 32 @@ -496,15 +609,17 @@ def test_basic_chain_required(sam_data, cec_inverter_parameters): altitude = 700 modules = sam_data['sandiamod'] module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] + temp_model_params = sapm_temperature_cs5p_220m.copy() with pytest.raises(ValueError): dc, ac = modelchain.basic_chain( - times, latitude, longitude, module_parameters, + times, latitude, longitude, module_parameters, temp_model_params, cec_inverter_parameters, altitude=altitude ) @requires_scipy -def test_basic_chain_alt_az(sam_data, cec_inverter_parameters): +def test_basic_chain_alt_az(sam_data, cec_inverter_parameters, + sapm_temperature_cs5p_220m): times = pd.date_range(start='20160101 1200-0700', end='20160101 1800-0700', freq='6H') latitude = 32.2 @@ -513,9 +628,10 @@ def test_basic_chain_alt_az(sam_data, cec_inverter_parameters): surface_azimuth = 0 modules = sam_data['sandiamod'] module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] - + temp_model_params = sapm_temperature_cs5p_220m.copy() dc, ac = modelchain.basic_chain(times, latitude, longitude, - module_parameters, cec_inverter_parameters, + module_parameters, temp_model_params, + cec_inverter_parameters, surface_tilt=surface_tilt, surface_azimuth=surface_azimuth) @@ -525,7 +641,8 @@ def test_basic_chain_alt_az(sam_data, cec_inverter_parameters): @requires_scipy -def test_basic_chain_strategy(sam_data, cec_inverter_parameters): +def test_basic_chain_strategy(sam_data, cec_inverter_parameters, + sapm_temperature_cs5p_220m): times = pd.date_range(start='20160101 1200-0700', end='20160101 1800-0700', freq='6H') latitude = 32.2 @@ -533,10 +650,11 @@ def test_basic_chain_strategy(sam_data, cec_inverter_parameters): altitude = 700 modules = sam_data['sandiamod'] module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] - + temp_model_params = sapm_temperature_cs5p_220m.copy() dc, ac = modelchain.basic_chain( - times, latitude, longitude, module_parameters, cec_inverter_parameters, - orientation_strategy='south_at_latitude_tilt', altitude=altitude) + times, latitude, longitude, module_parameters, temp_model_params, + cec_inverter_parameters, orientation_strategy='south_at_latitude_tilt', + altitude=altitude) expected = pd.Series(np.array([ 183.522449305, -2.00000000e-02]), index=times) @@ -544,7 +662,8 @@ def test_basic_chain_strategy(sam_data, cec_inverter_parameters): @requires_scipy -def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters): +def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters, + sapm_temperature_cs5p_220m): times = pd.date_range(start='20160101 1200-0700', end='20160101 1800-0700', freq='6H') latitude = 32.2 @@ -554,9 +673,10 @@ def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters): surface_azimuth = 0 modules = sam_data['sandiamod'] module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] - + temp_model_params = sapm_temperature_cs5p_220m.copy() dc, ac = modelchain.basic_chain(times, latitude, longitude, - module_parameters, cec_inverter_parameters, + module_parameters, temp_model_params, + cec_inverter_parameters, surface_tilt=surface_tilt, surface_azimuth=surface_azimuth, pressure=93194) @@ -566,7 +686,8 @@ def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters): assert_series_equal(ac, expected, check_less_precise=1) dc, ac = modelchain.basic_chain(times, latitude, longitude, - module_parameters, cec_inverter_parameters, + module_parameters, temp_model_params, + cec_inverter_parameters, surface_tilt=surface_tilt, surface_azimuth=surface_azimuth, altitude=altitude) @@ -596,7 +717,7 @@ def test_ModelChain___repr__(system, location, strategy, strategy_str): ' ac_model: snlinverter', ' aoi_model: sapm_aoi_loss', ' spectral_model: sapm_spectral_loss', - ' temp_model: sapm_temp', + ' temperature_model: sapm_temp', ' losses_model: no_extra_losses' ]) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index ce854bed5d..41589c23b6 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -17,8 +17,10 @@ from pvlib import atmosphere from pvlib import solarposition from pvlib.location import Location +from pvlib import temperature +from pvlib._deprecation import pvlibDeprecationWarning -from conftest import needs_numpy_1_10, requires_scipy +from conftest import needs_numpy_1_10, requires_scipy, fail_on_pvlib_version def test_systemdef_tmy3(): @@ -497,6 +499,94 @@ def test_PVSystem_sapm_effective_irradiance(sapm_module_params, mocker): assert_allclose(out, 1, atol=0.1) +def test_PVSystem_sapm_celltemp(mocker): + a, b, deltaT = (-3.47, -0.0594, 3) # open_rack_glass_glass + temp_model_params = {'a': a, 'b': b, 'deltaT': deltaT} + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) + mocker.spy(temperature, 'sapm_cell') + temps = 25 + irrads = 1000 + winds = 1 + out = system.sapm_celltemp(irrads, temps, winds) + temperature.sapm_cell.assert_called_once_with(irrads, temps, winds, a, b, + deltaT) + assert_allclose(out, 57, atol=1) + + +def test_PVSystem_sapm_celltemp_kwargs(mocker): + temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ + 'open_rack_glass_glass'] + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) + mocker.spy(temperature, 'sapm_cell') + temps = 25 + irrads = 1000 + winds = 1 + out = system.sapm_celltemp(irrads, temps, winds) + temperature.sapm_cell.assert_called_once_with(irrads, temps, winds, + temp_model_params['a'], + temp_model_params['b'], + temp_model_params['deltaT']) + assert_allclose(out, 57, atol=1) + + +def test_PVSystem_pvsyst_celltemp(mocker): + parameter_set = 'insulated' + temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['pvsyst'][ + parameter_set] + alpha_absorption = 0.85 + eta_m = 0.17 + module_parameters = {'alpha_absorption': alpha_absorption, 'eta_m': eta_m} + system = pvsystem.PVSystem(module_parameters=module_parameters, + temperature_model_parameters=temp_model_params) + mocker.spy(temperature, 'pvsyst_cell') + irrad = 800 + temp = 45 + wind = 0.5 + out = system.pvsyst_celltemp(irrad, temp, wind_speed=wind) + temperature.pvsyst_cell.assert_called_once_with( + irrad, temp, wind, temp_model_params['u_c'], temp_model_params['u_v'], + eta_m, alpha_absorption) + assert (out < 90) and (out > 70) + + +def test_PVSystem_pvsyst_celltemp_kwargs(mocker): + temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['pvsyst'][ + 'insulated'] + alpha_absorption = 0.85 + eta_m = 0.17 + module_parameters = {'alpha_absorption': alpha_absorption, 'eta_m': eta_m} + system = pvsystem.PVSystem(module_parameters=module_parameters, + temperature_model_parameters=temp_model_params) + mocker.spy(temperature, 'pvsyst_cell') + irrad = 800 + temp = 45 + wind = 0.5 + out = system.pvsyst_celltemp(irrad, temp, wind_speed=wind) + temperature.pvsyst_cell.assert_called_once_with( + irrad, temp, wind, temp_model_params['u_c'], temp_model_params['u_v'], + eta_m, alpha_absorption) + assert (out < 90) and (out > 70) + + +def test__infer_temperature_model_params(): + system = pvsystem.PVSystem(module_parameters={}, + racking_model='open_rack', + module_type='glass_polymer') + expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ + 'sapm']['open_rack_glass_polymer'] + assert expected == system._infer_temperature_model_params() + expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ + 'pvsyst']['freestanding'] + system = pvsystem.PVSystem(module_parameters={}, + racking_model='freestanding', + module_type='glass_polymer') + assert expected == system._infer_temperature_model_params() + system = pvsystem.PVSystem(module_parameters={}, + racking_model='not_a_rack_model', + module_type='glass_polymer') + assert {} == system._infer_temperature_model_params() + + def test_calcparams_desoto(cec_module_params): times = pd.date_range(start='2015-01-01', periods=3, freq='12H') effective_irradiance = pd.Series([0.0, 800.0, 800.0], index=times) @@ -1147,114 +1237,6 @@ def test_PVSystem_scale_voltage_current_power(mocker): m.assert_called_once_with(data, voltage=2, current=3) -def test_sapm_celltemp(): - default = pvsystem.sapm_celltemp(900, 5, 20) - assert_allclose(default['temp_cell'], 43.509, 3) - assert_allclose(default['temp_module'], 40.809, 3) - assert_frame_equal(default, pvsystem.sapm_celltemp(900, 5, 20, - [-3.47, -.0594, 3])) - - -def test_sapm_celltemp_dict_like(): - default = pvsystem.sapm_celltemp(900, 5, 20) - assert_allclose(default['temp_cell'], 43.509, 3) - assert_allclose(default['temp_module'], 40.809, 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.date_range(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 = pvsystem.sapm_celltemp(irrads, winds, temps) - - expected = pd.DataFrame({'temp_cell':[0., 23.06066166, 5.], - 'temp_module':[0., 21.56066166, 5.]}, - index=times) - - assert_frame_equal(expected, pvtemps) - - -def test_PVSystem_sapm_celltemp(mocker): - racking_model = 'roof_mount_cell_glassback' - - system = pvsystem.PVSystem(racking_model=racking_model) - mocker.spy(pvsystem, 'sapm_celltemp') - temps = 25 - irrads = 1000 - winds = 1 - out = system.sapm_celltemp(irrads, winds, temps) - pvsystem.sapm_celltemp.assert_called_once_with( - irrads, winds, temps, model=racking_model) - assert isinstance(out, pd.DataFrame) - assert out.shape == (1, 2) - - -def test_pvsyst_celltemp_default(): - default = pvsystem.pvsyst_celltemp(900, 20, 5) - assert_allclose(default, 45.137, 0.001) - - -def test_pvsyst_celltemp_non_model(): - tup_non_model = pvsystem.pvsyst_celltemp(900, 20, 5, 0.1, - model_params=(23.5, 6.25)) - assert_allclose(tup_non_model, 33.315, 0.001) - - list_non_model = pvsystem.pvsyst_celltemp(900, 20, 5, 0.1, - model_params=[26.5, 7.68]) - assert_allclose(list_non_model, 31.233, 0.001) - - -def test_pvsyst_celltemp_model_wrong_type(): - with pytest.raises(TypeError): - pvsystem.pvsyst_celltemp( - 900, 20, 5, 0.1, - model_params={"won't": 23.5, "work": 7.68}) - - -def test_pvsyst_celltemp_model_non_option(): - with pytest.raises(KeyError): - pvsystem.pvsyst_celltemp( - 900, 20, 5, 0.1, - model_params="not_an_option") - - -def test_pvsyst_celltemp_with_index(): - times = pd.date_range(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 = pvsystem.pvsyst_celltemp(irrads, temps, wind_speed=winds) - expected = pd.Series([0.0, 23.96551, 5.0], index=times) - assert_series_equal(expected, pvtemps) - - -def test_PVSystem_pvsyst_celltemp(mocker): - racking_model = 'insulated' - alpha_absorption = 0.85 - eta_m = 0.17 - module_parameters = {} - module_parameters['alpha_absorption'] = alpha_absorption - module_parameters['eta_m'] = eta_m - system = pvsystem.PVSystem(racking_model=racking_model, - module_parameters=module_parameters) - mocker.spy(pvsystem, 'pvsyst_celltemp') - irrad = 800 - temp = 45 - wind = 0.5 - out = system.pvsyst_celltemp(irrad, temp, wind_speed=wind) - pvsystem.pvsyst_celltemp.assert_called_once_with( - irrad, temp, wind, eta_m, alpha_absorption, racking_model) - assert isinstance(out, float) - assert out < 90 and out > 70 - - def test_adrinverter(sam_data): inverters = sam_data['adrinverter'] testinv = 'Ablerex_Electronics_Co___Ltd___' \ @@ -1425,7 +1407,9 @@ def test_PVSystem_localize_with_latlon(): def test_PVSystem___repr__(): system = pvsystem.PVSystem(module='blah', inverter='blarg', name='pv ftw') - expected = 'PVSystem: \n name: pv ftw\n surface_tilt: 0\n surface_azimuth: 180\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback' + expected = ('PVSystem: \n name: pv ftw\n surface_tilt: 0\n ' + 'surface_azimuth: 180\n module: blah\n inverter: blarg\n ' + 'albedo: 0.25\n racking_model: open_rack') assert system.__repr__() == expected @@ -1434,7 +1418,10 @@ def test_PVSystem_localize___repr__(): system = pvsystem.PVSystem(module='blah', inverter='blarg', name='pv ftw') localized_system = system.localize(latitude=32, longitude=-111) - expected = 'LocalizedPVSystem: \n name: None\n latitude: 32\n longitude: -111\n altitude: 0\n tz: UTC\n surface_tilt: 0\n surface_azimuth: 180\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback' + expected = ('LocalizedPVSystem: \n name: None\n latitude: 32\n ' + 'longitude: -111\n altitude: 0\n tz: UTC\n ' + 'surface_tilt: 0\n surface_azimuth: 180\n module: blah\n ' + 'inverter: blarg\n albedo: 0.25\n racking_model: open_rack') assert localized_system.__repr__() == expected @@ -1463,7 +1450,10 @@ def test_LocalizedPVSystem___repr__(): inverter='blarg', name='my name') - expected = 'LocalizedPVSystem: \n name: my name\n latitude: 32\n longitude: -111\n altitude: 0\n tz: UTC\n surface_tilt: 0\n surface_azimuth: 180\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback' + expected = ('LocalizedPVSystem: \n name: my name\n latitude: 32\n ' + 'longitude: -111\n altitude: 0\n tz: UTC\n ' + 'surface_tilt: 0\n surface_azimuth: 180\n module: blah\n ' + 'inverter: blarg\n albedo: 0.25\n racking_model: open_rack') assert localized_system.__repr__() == expected @@ -1622,3 +1612,60 @@ def test_PVSystem_pvwatts_ac_kwargs(mocker): pvsystem.pvwatts_ac.assert_called_once_with(pdc, **system.inverter_parameters) assert out < pdc + + +@fail_on_pvlib_version('0.8') +def test_deprecated_08(): + with pytest.warns(pvlibDeprecationWarning): + pvsystem.sapm_celltemp(1000, 25, 1) + with pytest.warns(pvlibDeprecationWarning): + pvsystem.pvsyst_celltemp(1000, 25) + module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1, + 'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1} + with pytest.warns(pvlibDeprecationWarning): + pvsystem.PVSystem(module_parameters=module_parameters, + racking_model='open', module_type='glass_glass') + + +@fail_on_pvlib_version('0.8') +def test__pvsyst_celltemp_translator(): + result = pvsystem._pvsyst_celltemp_translator(900, 20, 5) + assert_allclose(result, 45.137, 0.001) + result = pvsystem._pvsyst_celltemp_translator(900, 20, 5, 0.1, 0.9, + [29.0, 0.0]) + assert_allclose(result, 45.137, 0.001) + result = pvsystem._pvsyst_celltemp_translator(poa_global=900, temp_air=20, + wind_speed=5) + assert_allclose(result, 45.137, 0.001) + result = pvsystem._pvsyst_celltemp_translator(900, 20, wind_speed=5) + assert_allclose(result, 45.137, 0.001) + result = pvsystem._pvsyst_celltemp_translator(900, 20, wind_speed=5.0, + u_c=23.5, u_v=6.25, + eta_m=0.1) + assert_allclose(result, 33.315, 0.001) + result = pvsystem._pvsyst_celltemp_translator(900, 20, wind_speed=5.0, + eta_m=0.1, + model_params=[23.5, 6.25]) + assert_allclose(result, 33.315, 0.001) + result = pvsystem._pvsyst_celltemp_translator(900, 20, wind_speed=5.0, + eta_m=0.1, + model_params=(23.5, 6.25)) + assert_allclose(result, 33.315, 0.001) + + +@fail_on_pvlib_version('0.8') +def test__sapm_celltemp_translator(): + result = pvsystem._sapm_celltemp_translator(900, 5, 20, + 'open_rack_glass_glass') + assert_allclose(result, 43.509, 3) + result = pvsystem._sapm_celltemp_translator(900, 5, temp_air=20, + model='open_rack_glass_glass') + assert_allclose(result, 43.509, 3) + params = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ + 'open_rack_glass_glass'] + result = pvsystem._sapm_celltemp_translator(900, 5, 20, params) + assert_allclose(result, 43.509, 3) + result = pvsystem._sapm_celltemp_translator(900, 5, 20, + [params['a'], params['b'], + params['deltaT']]) + assert_allclose(result, 43.509, 3) diff --git a/pvlib/test/test_temperature.py b/pvlib/test/test_temperature.py new file mode 100644 index 0000000000..c3112d85a6 --- /dev/null +++ b/pvlib/test/test_temperature.py @@ -0,0 +1,99 @@ +import pandas as pd +import numpy as np + +import pytest +from pandas.util.testing import assert_series_equal +from numpy.testing import assert_allclose + +from pvlib import temperature + + +@pytest.fixture +def sapm_default(): + return temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ + 'open_rack_glass_glass'] + + +def test_sapm_cell(sapm_default): + default = temperature.sapm_cell(900, 20, 5, sapm_default['a'], + sapm_default['b'], sapm_default['deltaT']) + assert_allclose(default, 43.509, 3) + + +def test_sapm_module(sapm_default): + default = temperature.sapm_module(900, 20, 5, sapm_default['a'], + sapm_default['b']) + assert_allclose(default, 40.809, 3) + + +def test_sapm_ndarray(sapm_default): + temps = np.array([0, 10, 5]) + irrads = np.array([0, 500, 0]) + winds = np.array([10, 5, 0]) + cell_temps = temperature.sapm_cell(irrads, temps, winds, sapm_default['a'], + sapm_default['b'], + sapm_default['deltaT']) + module_temps = temperature.sapm_module(irrads, temps, winds, + sapm_default['a'], + sapm_default['b']) + expected_cell = np.array([0., 23.06066166, 5.]) + expected_module = np.array([0., 21.56066166, 5.]) + assert_allclose(expected_cell, cell_temps, 3) + assert_allclose(expected_module, module_temps, 3) + + +def test_sapm_series(sapm_default): + times = pd.date_range(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) + cell_temps = temperature.sapm_cell(irrads, temps, winds, sapm_default['a'], + sapm_default['b'], + sapm_default['deltaT']) + module_temps = temperature.sapm_module(irrads, temps, winds, + sapm_default['a'], + sapm_default['b']) + expected_cell = pd.Series([0., 23.06066166, 5.], index=times) + expected_module = pd.Series([0., 21.56066166, 5.], index=times) + assert_series_equal(expected_cell, cell_temps) + assert_series_equal(expected_module, module_temps) + + +def test_pvsyst_cell_default(): + result = temperature.pvsyst_cell(900, 20, 5) + assert_allclose(result, 45.137, 0.001) + + +def test_pvsyst_cell_kwargs(): + result = temperature.pvsyst_cell(900, 20, wind_speed=5.0, u_c=23.5, + u_v=6.25, eta_m=0.1) + assert_allclose(result, 33.315, 0.001) + + +def test_pvsyst_cell_ndarray(): + temps = np.array([0, 10, 5]) + irrads = np.array([0, 500, 0]) + winds = np.array([10, 5, 0]) + result = temperature.pvsyst_cell(irrads, temps, wind_speed=winds) + expected = np.array([0.0, 23.96551, 5.0]) + assert_allclose(expected, result, 3) + + +def test_pvsyst_cell_series(): + times = pd.date_range(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) + + result = temperature.pvsyst_cell(irrads, temps, wind_speed=winds) + expected = pd.Series([0.0, 23.96551, 5.0], index=times) + assert_series_equal(expected, result) + + +def test__temperature_model_params(): + params = temperature._temperature_model_params('sapm', + 'open_rack_glass_glass') + assert params == temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ + 'open_rack_glass_glass'] + with pytest.raises(KeyError): + temperature._temperature_model_params('sapm', 'not_a_parameter_set') diff --git a/pvlib/test/test_tracking.py b/pvlib/test/test_tracking.py index 8ab8032831..983d8ae639 100644 --- a/pvlib/test/test_tracking.py +++ b/pvlib/test/test_tracking.py @@ -440,7 +440,11 @@ def test_get_irradiance(): def test_SingleAxisTracker___repr__(): system = tracking.SingleAxisTracker(max_angle=45, gcr=.25, module='blah', inverter='blarg') - expected = 'SingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n max_angle: 45\n backtrack: True\n gcr: 0.25\n name: None\n surface_tilt: None\n surface_azimuth: None\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback' + expected = ('SingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n ' + 'max_angle: 45\n backtrack: True\n gcr: 0.25\n ' + 'name: None\n surface_tilt: None\n surface_azimuth: None\n ' + 'module: blah\n inverter: blarg\n albedo: 0.25\n ' + 'racking_model: open_rack') assert system.__repr__() == expected @@ -451,6 +455,11 @@ def test_LocalizedSingleAxisTracker___repr__(): inverter='blarg', gcr=0.25) - expected = 'LocalizedSingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n max_angle: 90\n backtrack: True\n gcr: 0.25\n name: None\n surface_tilt: None\n surface_azimuth: None\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback\n latitude: 32\n longitude: -111\n altitude: 0\n tz: UTC' + expected = ('LocalizedSingleAxisTracker: \n axis_tilt: 0\n ' + 'axis_azimuth: 0\n max_angle: 90\n backtrack: True\n ' + 'gcr: 0.25\n name: None\n surface_tilt: None\n ' + 'surface_azimuth: None\n module: blah\n inverter: blarg\n ' + 'albedo: 0.25\n racking_model: open_rack\n ' + 'latitude: 32\n longitude: -111\n altitude: 0\n tz: UTC') assert localized_system.__repr__() == expected