From 4c1b9bcb5478f477b92bb6c126a6891b2fc8afd4 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 6 Oct 2020 14:47:07 -0600 Subject: [PATCH 001/236] Initial implementation of pvsystem.Array class Provides the three basic methods that depend on tilt and azimuth and provides a container for other parameters which may vary from array to array but do not vary within a single array (racking, temperature model params, albedo, strings, modules per string, etc.). --- pvlib/pvsystem.py | 217 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index fecc93075e..30300f8a33 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -881,6 +881,223 @@ def __repr__(self): f'{attr}: {getattr(self, attr)}' for attr in attrs)) +class Array: + """ + An Array is a set of of modules at a specific orientation. + + Specifically, an array is defined by a tilt, azimuth, the + module parameters, the number of strings of modules and the + number of modules on each string. + + Parameters + ---------- + surface_tilt: float or array-like, default 0 + 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 array-like, default 180 + Azimuth angle of the module surface. + North=0, East=90, South=180, West=270. + + albedo : None or float, default None + The ground albedo. If ``None``, will attempt to use + ``surface_type`` and ``irradiance.SURFACE_ALBEDOS`` + to lookup albedo. + + surface_type : None or string, default None + The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` + for valid values. + + module : None or string, default None + The model name of the modules. + 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, default 1 + Number of modules per string in the array. + + strings: int, default 1 + Number of strings in the array. + + 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. + + """ + + def __init__(self, + surface_tilt=0, surface_azimuth=180, + albedo=None, surface_type=None, + module=None, module_type=None, + module_parameters=None, + temperature_model_parameters=None, + modules_per_string=1, strings=1, + racking_model=None): + self.surface_tilt = surface_tilt + self.surface_azimuth = surface_azimuth + + # TODO now would be a good time to address the suggestion above: + # '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 = None + + # TODO now would be a good time to address the suggestion above: + # 'could tie these together with @property' + self.module = module + if module_parameters is None: + self.module_parameters = {} + else: + self.module_parameters = module_parameters + + self.module_type = module_type + self.racking_model = racking_model + + self.strings = strings + self.modules_per_string = modules_per_string + + self.temperature_model_parameters = temperature_model_parameters + + def __repr__(self): + attrs = ['surface_tilt', 'surface_azimuth', 'module', + 'albedo', 'racking_model', 'module_type', + 'temperature_model_parameters', + 'strings', 'modules_per_string'] + return 'Array:\n ' + '\n '.join( + f'{attr}: {getattr(self, attr)}' for attr in attrs + ) + + def get_aoi(self, solar_zenith, solar_azimuth): + """ + Get the angle of incidence on the array. + + Parameters + ---------- + solar_zenith : float or Series + Solar zenith angle. + solar_azimuth : float or Series + Solar azimuth angle + + Returns + ------- + aoi : Series + Then angle of incidence. + """ + return irradiance.aoi(self.surface_tilt, self.surface_azimuth, + solar_zenith, solar_azimuth) + + def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, + dni_extra=None, airmass=None, model='haydavies', + **kwargs): + """ + Get plane of array irradiance components. + + Uses the :py:func:`irradiance.get_total_irradiance` function to + calculate the plane of array irradiance components for a surface + defined by ``self.surface_tilt`` and ``self.surface_azimuth`` with + albedo ``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 : None, float or Series, default None + Extraterrestrial direct normal irradiance + airmass : None, float or Series, default None + Airmass + model : String, default 'haydavies' + Irradiance model. + + kwargs + Extra parameters passed to :func:`irradiance.get_total_irradiance`. + + Returns + ------- + poa_irradiance : DataFrame + Column names are: ``total, beam, sky, ground``. + """ + # TODO address code duplication + # not needed for all models, but this is easier + if dni_extra is None: + dni_extra = irradiance.get_extra_radiation(solar_zenith.index) + + if airmass is None: + airmass = atmosphere.get_relative_airmass(solar_zenith) + + return irradiance.get_total_irradiance(self.surface_tilt, + self.surface_azimuth, + solar_zenith, solar_azimuth, + dni, ghi, dhi, + dni_extra=dni_extra, + airmass=airmass, + model=model, + albedo=self.albedo, + **kwargs) + + def get_iam(self, aoi, iam_model='physical'): + """ + Determine the incidence angle modifier using the method specified by + ``iam_model``. + + Parameters for the selected IAM model are expected to be in + ``PVSystem.module_parameters``. Default parameters are available for + the 'physical', 'ashrae' and 'martin_ruiz' models. + + Parameters + ---------- + aoi : numeric + The angle of incidence in degrees. + + aoi_model : string, default 'physical' + The IAM model to be used. Valid strings are 'physical', 'ashrae', + 'martin_ruiz' and 'sapm'. + + Returns + ------- + iam : numeric + The AOI modifier. + + Raises + ------ + ValueError if `iam_model` is not a valid model name. + """ + # TODO address code duplication + model = iam_model.lower() + if model in ['ashrae', 'physical', 'martin_ruiz']: + param_names = iam._IAM_MODEL_PARAMS[model] + kwargs = _build_kwargs(param_names, self.module_parameters) + func = getattr(iam, model) + return func(aoi, **kwargs) + elif model == 'sapm': + return iam.sapm(aoi, self.module_parameters) + elif model == 'interp': + raise ValueError(model + ' is not implemented as an IAM model' + 'option for PVSystem') + else: + raise ValueError(model + ' is not a valid IAM model') + + def calcparams_desoto(effective_irradiance, temp_cell, alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, EgRef=1.121, dEgdT=-0.0002677, From 532bf778a24024937998177c5adfddead750d26d Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 6 Oct 2020 15:39:30 -0600 Subject: [PATCH 002/236] Refactor PVSystem to use pvsystem.Array internally Removes array-related fields and adds _array field. All internal references to array-related attributes now refer to the the corresponding attributes of the Array class. It is likely some of the Array attributes will need to be exposed on the PVSystem instances via @properties; however, any of these instances are not covered by tests so it will be necessary to carefully work through the other places where PVsystem is used (ModelChain and child classes) to see what needs to be done. --- pvlib/pvsystem.py | 142 ++++++++++++++++++---------------------------- 1 file changed, 55 insertions(+), 87 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 30300f8a33..5a355a267f 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -174,34 +174,19 @@ def __init__(self, racking_model=None, losses_parameters=None, name=None, **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 - if module_parameters is None: - self.module_parameters = {} - 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() - else: - self.temperature_model_parameters = temperature_model_parameters - - self.modules_per_string = modules_per_string - self.strings_per_inverter = strings_per_inverter + self._array = Array( + surface_tilt, + surface_tilt, + albedo, + surface_type, + module, + module_type, + module_parameters, + temperature_model_parameters, + modules_per_string, + strings_per_inverter, + racking_model + ) self.inverter = inverter if inverter_parameters is None: @@ -223,10 +208,8 @@ def __init__(self, ) def __repr__(self): - attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module', - 'inverter', 'albedo', 'racking_model', 'module_type', - 'temperature_model_parameters'] - return ('PVSystem:\n ' + '\n '.join( + attrs = ['name', 'inverter'] + return (f'PVSystem:\n array: {str(self._array)}\n ' + '\n '.join( f'{attr}: {getattr(self, attr)}' for attr in attrs)) def get_aoi(self, solar_zenith, solar_azimuth): @@ -245,9 +228,7 @@ def get_aoi(self, solar_zenith, solar_azimuth): The angle of incidence """ - aoi = irradiance.aoi(self.surface_tilt, self.surface_azimuth, - solar_zenith, solar_azimuth) - return aoi + return self._array.get_aoi(solar_zenith, solar_azimuth) def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, model='haydavies', @@ -285,23 +266,11 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, poa_irradiance : DataFrame Column names are: ``total, beam, sky, ground``. """ - - # not needed for all models, but this is easier - if dni_extra is None: - dni_extra = irradiance.get_extra_radiation(solar_zenith.index) - - if airmass is None: - airmass = atmosphere.get_relative_airmass(solar_zenith) - - return irradiance.get_total_irradiance(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) + return self._array.get_irradiance( + solar_zenith, solar_azimuth, + dni, ghi, dhi, + dni_extra, airmass + ) def get_iam(self, aoi, iam_model='physical'): """ @@ -330,19 +299,7 @@ def get_iam(self, aoi, iam_model='physical'): ------ ValueError if `iam_model` is not a valid model name. """ - model = iam_model.lower() - if model in ['ashrae', 'physical', 'martin_ruiz']: - param_names = iam._IAM_MODEL_PARAMS[model] - kwargs = _build_kwargs(param_names, self.module_parameters) - func = getattr(iam, model) - return func(aoi, **kwargs) - elif model == 'sapm': - return iam.sapm(aoi, self.module_parameters) - elif model == 'interp': - raise ValueError(model + ' is not implemented as an IAM model' - 'option for PVSystem') - else: - raise ValueError(model + ' is not a valid IAM model') + return self._array.get_iam(aoi, iam_model) def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): """ @@ -369,7 +326,7 @@ def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): kwargs = _build_kwargs(['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', 'alpha_sc', 'EgRef', 'dEgdT', 'irrad_ref', 'temp_ref'], - self.module_parameters) + self._array.module_parameters) return calcparams_desoto(effective_irradiance, temp_cell, **kwargs) @@ -398,7 +355,7 @@ def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): kwargs = _build_kwargs(['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', 'alpha_sc', 'Adjust', 'EgRef', 'dEgdT', 'irrad_ref', 'temp_ref'], - self.module_parameters) + self._array.module_parameters) return calcparams_cec(effective_irradiance, temp_cell, **kwargs) @@ -426,7 +383,7 @@ def calcparams_pvsyst(self, effective_irradiance, temp_cell): 'R_s', 'alpha_sc', 'EgRef', 'irrad_ref', 'temp_ref', 'cells_in_series'], - self.module_parameters) + self._array.module_parameters) return calcparams_pvsyst(effective_irradiance, temp_cell, **kwargs) @@ -451,7 +408,8 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): ------- See pvsystem.sapm for details """ - return sapm(effective_irradiance, temp_cell, self.module_parameters) + return sapm(effective_irradiance, temp_cell, + self._array.module_parameters) def sapm_celltemp(self, poa_global, temp_air, wind_speed): """Uses :py:func:`temperature.sapm_cell` to calculate cell @@ -473,8 +431,9 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): numeric, values in degrees C. """ # warn user about change in default behavior in 0.9. - if (self.temperature_model_parameters == {} and self.module_type - is None and self.racking_model is None): + if (self.temperature_model_parameters == {} + and self._array.module_type is None + and self._array.racking_model is None): warnings.warn( 'temperature_model_parameters, racking_model, and module_type ' 'are not specified. Reverting to deprecated default: SAPM ' @@ -495,7 +454,7 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): def _infer_temperature_model_params(self): # try to infer temperature model parameters from from racking_model # and module_type - param_set = f'{self.racking_model}_{self.module_type}' + param_set = f'{self._array.racking_model}_{self._array.module_type}' if param_set in temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']: return temperature._temperature_model_params('sapm', param_set) elif 'freestanding' in param_set: @@ -522,7 +481,8 @@ def sapm_spectral_loss(self, airmass_absolute): F1 : numeric The SAPM spectral loss coefficient. """ - return sapm_spectral_loss(airmass_absolute, self.module_parameters) + return sapm_spectral_loss(airmass_absolute, + self._array.module_parameters) def sapm_effective_irradiance(self, poa_direct, poa_diffuse, airmass_absolute, aoi, @@ -553,7 +513,7 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, """ return sapm_effective_irradiance( poa_direct, poa_diffuse, airmass_absolute, aoi, - self.module_parameters) + self._array.module_parameters) def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): """Uses :py:func:`temperature.pvsyst_cell` to calculate cell @@ -577,7 +537,7 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): numeric, values in degrees C. """ kwargs = _build_kwargs(['eta_m', 'alpha_absorption'], - self.module_parameters) + self._array.module_parameters) kwargs.update(_build_kwargs(['u_c', 'u_v'], self.temperature_model_parameters)) return temperature.pvsyst_cell(poa_global, temp_air, wind_speed, @@ -640,9 +600,11 @@ def first_solar_spectral_loss(self, pw, airmass_absolute): """ if 'first_solar_spectral_coefficients' in \ - self.module_parameters.keys(): + self._array.module_parameters.keys(): coefficients = \ - self.module_parameters['first_solar_spectral_coefficients'] + self._array.module_parameters[ + 'first_solar_spectral_coefficients' + ] module_type = None else: module_type = self._infer_cell_type() @@ -685,12 +647,16 @@ def _infer_cell_type(self): 'GaAs': None, 'a-Si / mono-Si': 'monosi'} - if 'Technology' in self.module_parameters.keys(): + if 'Technology' in self._array.module_parameters.keys(): # CEC module parameter set - cell_type = _cell_type_dict[self.module_parameters['Technology']] - elif 'Material' in self.module_parameters.keys(): + cell_type = _cell_type_dict[ + self._array.module_parameters['Technology'] + ] + elif 'Material' in self._array.module_parameters.keys(): # Sandia module parameter set - cell_type = _cell_type_dict[self.module_parameters['Material']] + cell_type = _cell_type_dict[ + self._array.module_parameters['Material'] + ] else: cell_type = None @@ -774,9 +740,11 @@ def scale_voltage_current_power(self, data): A scaled copy of the input data. """ - return scale_voltage_current_power(data, - voltage=self.modules_per_string, - current=self.strings_per_inverter) + return scale_voltage_current_power( + data, + voltage=self._array.modules_per_string, + current=self._array.strings + ) def pvwatts_dc(self, g_poa_effective, temp_cell): """ @@ -786,11 +754,11 @@ def pvwatts_dc(self, g_poa_effective, temp_cell): See :py:func:`pvlib.pvsystem.pvwatts_dc` for details. """ - kwargs = _build_kwargs(['temp_ref'], self.module_parameters) + kwargs = _build_kwargs(['temp_ref'], self._array.module_parameters) return pvwatts_dc(g_poa_effective, temp_cell, - self.module_parameters['pdc0'], - self.module_parameters['gamma_pdc'], + self._array.module_parameters['pdc0'], + self._array.module_parameters['gamma_pdc'], **kwargs) def pvwatts_losses(self): From 4cccbd5948edb3b98d7aefe5d24578446aeef655 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 9 Oct 2020 15:34:52 -0600 Subject: [PATCH 003/236] Use self._array.temperature_parameters in PVSystem Use the temperature model parameters from the Array instance inside PVSystem. Also properly initialize the parameters. --- pvlib/pvsystem.py | 51 +++++++++++++++++++----------------- pvlib/tests/test_pvsystem.py | 18 ++++++------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 5a355a267f..7e27c79d9b 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -431,7 +431,8 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): numeric, values in degrees C. """ # warn user about change in default behavior in 0.9. - if (self.temperature_model_parameters == {} + temperature_model_params = self._array.temperature_model_parameters + if (temperature_model_params == {} and self._array.module_type is None and self._array.racking_model is None): warnings.warn( @@ -444,28 +445,13 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): pvlibDeprecationWarning) params = temperature._temperature_model_params( 'sapm', 'open_rack_glass_glass') - self.temperature_model_parameters = params + temperature_model_params = params kwargs = _build_kwargs(['a', 'b', 'deltaT'], - self.temperature_model_parameters) + temperature_model_params) 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 = f'{self._array.racking_model}_{self._array.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): """ Use the :py:func:`sapm_spectral_loss` function, the input @@ -539,7 +525,7 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): kwargs = _build_kwargs(['eta_m', 'alpha_absorption'], self._array.module_parameters) kwargs.update(_build_kwargs(['u_c', 'u_v'], - self.temperature_model_parameters)) + self._array.temperature_model_parameters)) return temperature.pvsyst_cell(poa_global, temp_air, wind_speed, **kwargs) @@ -565,7 +551,7 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): numeric, values in degrees C. """ kwargs = _build_kwargs(['u0', 'u1'], - self.temperature_model_parameters) + self._array.temperature_model_parameters) return temperature.faiman(poa_global, temp_air, wind_speed, **kwargs) @@ -937,17 +923,36 @@ def __init__(self, self.strings = strings self.modules_per_string = modules_per_string - self.temperature_model_parameters = temperature_model_parameters + if temperature_model_parameters is None: + self.temperature_model_parameters = \ + self._infer_temperature_model_params() + else: + self.temperature_model_parameters = temperature_model_parameters def __repr__(self): attrs = ['surface_tilt', 'surface_azimuth', 'module', 'albedo', 'racking_model', 'module_type', 'temperature_model_parameters', 'strings', 'modules_per_string'] - return 'Array:\n ' + '\n '.join( + return 'Array:\n ' + '\n '.join( f'{attr}: {getattr(self, attr)}' for attr in attrs ) + def _infer_temperature_model_params(self): + # try to infer temperature model parameters from from racking_model + # and module_type + param_set = f'{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 get_aoi(self, solar_zenith, solar_azimuth): """ Get the angle of incidence on the array. @@ -1005,7 +1010,6 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, poa_irradiance : DataFrame Column names are: ``total, beam, sky, ground``. """ - # TODO address code duplication # not needed for all models, but this is easier if dni_extra is None: dni_extra = irradiance.get_extra_radiation(solar_zenith.index) @@ -1050,7 +1054,6 @@ def get_iam(self, aoi, iam_model='physical'): ------ ValueError if `iam_model` is not a valid model name. """ - # TODO address code duplication model = iam_model.lower() if model in ['ashrae', 'physical', 'martin_ruiz']: param_names = iam._IAM_MODEL_PARAMS[model] diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index b9edf54fff..01f541b4da 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -360,19 +360,19 @@ def test_PVSystem_faiman_celltemp(mocker): assert_allclose(out, 56.4, atol=1) -def test__infer_temperature_model_params(): - system = pvsystem.PVSystem(module_parameters={}, - racking_model='open_rack', - module_type='glass_polymer') +def test_Array__infer_temperature_model_params(): + array = pvsystem.Array(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() - system = pvsystem.PVSystem(module_parameters={}, - racking_model='freestanding', - module_type='glass_polymer') + assert expected == array._infer_temperature_model_params() + array = pvsystem.Array(module_parameters={}, + racking_model='freestanding', + module_type='glass_polymer') expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'pvsyst']['freestanding'] - assert expected == system._infer_temperature_model_params() + assert expected == array._infer_temperature_model_params() def test_calcparams_desoto(cec_module_params): From 6e980d5efb7aeaf6501631943e8d902e26340cec Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 12 Oct 2020 08:35:43 -0600 Subject: [PATCH 004/236] Fix error passing surface_tilt in place of surface_azimuth Mistake in invocation of Array.__init__() in PVSystem.__init__() --- pvlib/pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 7e27c79d9b..e4ef339624 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -176,7 +176,7 @@ def __init__(self, self._array = Array( surface_tilt, - surface_tilt, + surface_azimuth, albedo, surface_type, module, From 527ea2cb2f50c0850a1d1eb9b3237afe4e78fb1a Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 12 Oct 2020 08:55:43 -0600 Subject: [PATCH 005/236] Expose Array attributes as attributes of PVSystem Uses `@property` to provide getters and setters for Array attributes in order to mimic the original behavior of the PVSystem class. --- pvlib/pvsystem.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index e4ef339624..b8a08ea76c 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -801,6 +801,54 @@ def localize(self, location=None, latitude=None, longitude=None, return LocalizedPVSystem(pvsystem=self, location=location) + @property + def module_parameters(self): + return self._array.module_parameters + + @property + def module(self): + return self._array.module + + @property + def module_type(self): + return self._array.module_type + + @property + def temperature_model_parameters(self): + return self._array.temperature_model_parameters + + @temperature_model_parameters.setter + def temperature_model_parameters(self, value): + self._array.temperature_model_parameters = value + + @property + def surface_tilt(self): + return self._array.surface_tilt + + @surface_tilt.setter + def surface_tilt(self, value): + self._array.surface_tilt = value + + @property + def surface_azimuth(self): + return self._array.surface_azimuth + + @surface_azimuth.setter + def surface_azimuth(self, value): + self._array.surface_azimuth = value + + @property + def albedo(self): + return self._array.albedo + + @property + def racking_model(self): + return self._array.racking_model + + @racking_model.setter + def racking_model(self, value): + self._array.racking_model = value + @deprecated('0.8', alternative='PVSystem, Location, and ModelChain', name='LocalizedPVSystem', removal='0.9') From 8b096ab292a6fa3f6c8e30539bf48c3e94a631d0 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 12 Oct 2020 08:57:26 -0600 Subject: [PATCH 006/236] Pass attributes of PVSystem._array when constructing LocalizedPVSystem --- pvlib/pvsystem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index b8a08ea76c..7fce531fdd 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -52,6 +52,7 @@ def _combine_localized_attributes(pvsystem=None, location=None, **kwargs): """ if pvsystem is not None: pv_dict = pvsystem.__dict__ + pv_dict = {**pv_dict, **pv_dict['_array'].__dict__} else: pv_dict = {} From a84eab9dd9d0b86ef50bf90251c6b124ffe2074e Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 12 Oct 2020 10:09:06 -0600 Subject: [PATCH 007/236] Fix over-indented lines --- pvlib/tests/test_pvsystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 01f541b4da..c04920a3b1 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -362,8 +362,8 @@ def test_PVSystem_faiman_celltemp(mocker): def test_Array__infer_temperature_model_params(): array = pvsystem.Array(module_parameters={}, - racking_model='open_rack', - module_type='glass_polymer') + racking_model='open_rack', + module_type='glass_polymer') expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'sapm']['open_rack_glass_polymer'] assert expected == array._infer_temperature_model_params() From 39d47eb7efc91f5e305f908955cd35fe1fa439bc Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 12 Oct 2020 12:37:52 -0600 Subject: [PATCH 008/236] Access Array attributes as attributes of the PVSystem Since the Array attributes are all exposed via @propertys on PVSystem, we can access them directly, reducing the number of changes needed to add the Array class. --- pvlib/pvsystem.py | 76 +++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index c8eedc298d..98b621c32a 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -209,8 +209,10 @@ def __init__(self, ) def __repr__(self): - attrs = ['name', 'inverter'] - return (f'PVSystem:\n array: {str(self._array)}\n ' + '\n '.join( + attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module', + 'inverter', 'albedo', 'racking_model', 'module_type', + 'temperature_model_parameters'] + return ('PVSystem:\n ' + '\n '.join( f'{attr}: {getattr(self, attr)}' for attr in attrs)) def get_aoi(self, solar_zenith, solar_azimuth): @@ -327,7 +329,7 @@ def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): kwargs = _build_kwargs(['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', 'alpha_sc', 'EgRef', 'dEgdT', 'irrad_ref', 'temp_ref'], - self._array.module_parameters) + self.module_parameters) return calcparams_desoto(effective_irradiance, temp_cell, **kwargs) @@ -356,7 +358,7 @@ def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): kwargs = _build_kwargs(['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', 'alpha_sc', 'Adjust', 'EgRef', 'dEgdT', 'irrad_ref', 'temp_ref'], - self._array.module_parameters) + self.module_parameters) return calcparams_cec(effective_irradiance, temp_cell, **kwargs) @@ -384,7 +386,7 @@ def calcparams_pvsyst(self, effective_irradiance, temp_cell): 'R_s', 'alpha_sc', 'EgRef', 'irrad_ref', 'temp_ref', 'cells_in_series'], - self._array.module_parameters) + self.module_parameters) return calcparams_pvsyst(effective_irradiance, temp_cell, **kwargs) @@ -409,8 +411,7 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): ------- See pvsystem.sapm for details """ - return sapm(effective_irradiance, temp_cell, - self._array.module_parameters) + return sapm(effective_irradiance, temp_cell, self.module_parameters) def sapm_celltemp(self, poa_global, temp_air, wind_speed): """Uses :py:func:`temperature.sapm_cell` to calculate cell @@ -432,10 +433,8 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): numeric, values in degrees C. """ # warn user about change in default behavior in 0.9. - temperature_model_params = self._array.temperature_model_parameters - if (temperature_model_params == {} - and self._array.module_type is None - and self._array.racking_model is None): + if (self.temperature_model_parameters == {} and self.module_type + is None and self.racking_model is None): warnings.warn( 'temperature_model_parameters, racking_model, and module_type ' 'are not specified. Reverting to deprecated default: SAPM ' @@ -446,10 +445,10 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): pvlibDeprecationWarning) params = temperature._temperature_model_params( 'sapm', 'open_rack_glass_glass') - temperature_model_params = params + self.temperature_model_parameters = params kwargs = _build_kwargs(['a', 'b', 'deltaT'], - temperature_model_params) + self.temperature_model_parameters) return temperature.sapm_cell(poa_global, temp_air, wind_speed, **kwargs) @@ -468,8 +467,7 @@ def sapm_spectral_loss(self, airmass_absolute): F1 : numeric The SAPM spectral loss coefficient. """ - return sapm_spectral_loss(airmass_absolute, - self._array.module_parameters) + return sapm_spectral_loss(airmass_absolute, self.module_parameters) def sapm_effective_irradiance(self, poa_direct, poa_diffuse, airmass_absolute, aoi, @@ -500,7 +498,7 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, """ return sapm_effective_irradiance( poa_direct, poa_diffuse, airmass_absolute, aoi, - self._array.module_parameters) + self.module_parameters) def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): """Uses :py:func:`temperature.pvsyst_cell` to calculate cell @@ -524,9 +522,9 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): numeric, values in degrees C. """ kwargs = _build_kwargs(['eta_m', 'alpha_absorption'], - self._array.module_parameters) + self.module_parameters) kwargs.update(_build_kwargs(['u_c', 'u_v'], - self._array.temperature_model_parameters)) + self.temperature_model_parameters)) return temperature.pvsyst_cell(poa_global, temp_air, wind_speed, **kwargs) @@ -552,7 +550,7 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): numeric, values in degrees C. """ kwargs = _build_kwargs(['u0', 'u1'], - self._array.temperature_model_parameters) + self.temperature_model_parameters) return temperature.faiman(poa_global, temp_air, wind_speed, **kwargs) @@ -627,11 +625,9 @@ def first_solar_spectral_loss(self, pw, airmass_absolute): """ if 'first_solar_spectral_coefficients' in \ - self._array.module_parameters.keys(): + self.module_parameters.keys(): coefficients = \ - self._array.module_parameters[ - 'first_solar_spectral_coefficients' - ] + self.module_parameters['first_solar_spectral_coefficients'] module_type = None else: module_type = self._infer_cell_type() @@ -674,16 +670,12 @@ def _infer_cell_type(self): 'GaAs': None, 'a-Si / mono-Si': 'monosi'} - if 'Technology' in self._array.module_parameters.keys(): + if 'Technology' in self.module_parameters.keys(): # CEC module parameter set - cell_type = _cell_type_dict[ - self._array.module_parameters['Technology'] - ] - elif 'Material' in self._array.module_parameters.keys(): + cell_type = _cell_type_dict[self.module_parameters['Technology']] + elif 'Material' in self.module_parameters.keys(): # Sandia module parameter set - cell_type = _cell_type_dict[ - self._array.module_parameters['Material'] - ] + cell_type = _cell_type_dict[self.module_parameters['Material']] else: cell_type = None @@ -767,11 +759,9 @@ def scale_voltage_current_power(self, data): A scaled copy of the input data. """ - return scale_voltage_current_power( - data, - voltage=self._array.modules_per_string, - current=self._array.strings - ) + return scale_voltage_current_power(data, + voltage=self.modules_per_string, + current=self.strings_per_inverter) def pvwatts_dc(self, g_poa_effective, temp_cell): """ @@ -781,11 +771,11 @@ def pvwatts_dc(self, g_poa_effective, temp_cell): See :py:func:`pvlib.pvsystem.pvwatts_dc` for details. """ - kwargs = _build_kwargs(['temp_ref'], self._array.module_parameters) + kwargs = _build_kwargs(['temp_ref'], self.module_parameters) return pvwatts_dc(g_poa_effective, temp_cell, - self._array.module_parameters['pdc0'], - self._array.module_parameters['gamma_pdc'], + self.module_parameters['pdc0'], + self.module_parameters['gamma_pdc'], **kwargs) def pvwatts_losses(self): @@ -890,6 +880,14 @@ def racking_model(self): def racking_model(self, value): self._array.racking_model = value + @property + def modules_per_string(self): + return self._array.modules_per_string + + @property + def strings_per_inverter(self): + return self._array.strings + @deprecated('0.8', alternative='PVSystem, Location, and ModelChain', name='LocalizedPVSystem', removal='0.9') From 2f7ccdb519886090f8fa0edcdce84981c0b7a421 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 12 Oct 2020 13:18:40 -0600 Subject: [PATCH 009/236] Initialize Array.albedo correctly --- pvlib/pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 98b621c32a..72f85ad903 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -994,7 +994,7 @@ def __init__(self, if albedo is None: self.albedo = irradiance.SURFACE_ALBEDOS.get(surface_type, 0.25) else: - self.albedo = None + self.albedo = albedo # TODO now would be a good time to address the suggestion above: # 'could tie these together with @property' From 0bc8a42da3817e5e264d11f37e28230c153c6496 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 13 Oct 2020 10:09:45 -0600 Subject: [PATCH 010/236] add ModelChainResults, plumb for airmass --- ci/requirements-py36.yml | 1 + ci/requirements-py37.yml | 1 + ci/requirements-py38.yml | 1 + pvlib/modelchain.py | 30 ++++++++++++++++++++++++++---- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml index b799da7393..acb7513ca8 100644 --- a/ci/requirements-py36.yml +++ b/ci/requirements-py36.yml @@ -5,6 +5,7 @@ channels: dependencies: - coveralls - cython + - dataclasses - ephem - netcdf4 - nose diff --git a/ci/requirements-py37.yml b/ci/requirements-py37.yml index 1542bb35d9..79dfa233ad 100644 --- a/ci/requirements-py37.yml +++ b/ci/requirements-py37.yml @@ -5,6 +5,7 @@ channels: dependencies: - coveralls - cython + - dataclasses - ephem - netcdf4 - nose diff --git a/ci/requirements-py38.yml b/ci/requirements-py38.yml index 6db508fd53..661938e8a5 100644 --- a/ci/requirements-py38.yml +++ b/ci/requirements-py38.yml @@ -6,6 +6,7 @@ dependencies: - coveralls - cython - ephem + - dataclasses - netcdf4 - nose - numba diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index e5becc4a1a..b0ae19dfd5 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -9,6 +9,7 @@ from functools import partial import warnings import pandas as pd +from dataclasses import dataclass, field from pvlib import (atmosphere, clearsky, inverter, pvsystem, solarposition, temperature, tools) @@ -260,6 +261,25 @@ def get_orientation(strategy, **kwargs): return surface_tilt, surface_azimuth +@dataclass +class ModelChainResult: + # system-level information + # weather: pd.DataFrame = field(default=None) + # solar_position: pd.DataFrame = field(default=None) + airmass: pd.DataFrame = field(default=None) + ac: pd.Series = field(default=None) + # per DC array information + total_irrad: pd.DataFrame = field(default=None) + aoi: pd.Series = field(default=None) + aoi_modifier: pd.Series = field(default=None) + spectral_modifier: pd.Series = field(default=None) + cell_temperature: pd.Series = field(default=None) + effective_irradiance: pd.Series = field(default=None) + dc: pd.Series = field(default=None) + array_ac: pd.Series = field(default=None) + diode_params: pd.DataFrame = field(default=None) + + class ModelChain: """ The ModelChain class to provides a standardized, high-level @@ -368,6 +388,8 @@ def __init__(self, system, location, self.times = None self.solar_position = None + self.results = ModelChainResult() + if kwargs: warnings.warn( 'Arbitrary ModelChain kwargs are deprecated and will be ' @@ -838,12 +860,12 @@ def infer_spectral_model(self): def first_solar_spectral_loss(self): self.spectral_modifier = self.system.first_solar_spectral_loss( self.weather['precipitable_water'], - self.airmass['airmass_absolute']) + self.results.airmass['airmass_absolute']) return self def sapm_spectral_loss(self): self.spectral_modifier = self.system.sapm_spectral_loss( - self.airmass['airmass_absolute']) + self.results.airmass['airmass_absolute']) return self def no_spectral_loss(self): @@ -1050,7 +1072,7 @@ def _prep_inputs_airmass(self): """ Assign airmass """ - self.airmass = self.location.get_airmass( + self.results.airmass = self.location.get_airmass( solar_position=self.solar_position, model=self.airmass_model) return self @@ -1171,7 +1193,7 @@ def prepare_inputs(self, weather): self.weather['dni'], self.weather['ghi'], self.weather['dhi'], - airmass=self.airmass['airmass_relative'], + airmass=self.results.airmass['airmass_relative'], model=self.transposition_model) return self From 75bd7555a10166f5d0dc5cc7f93105d85006ac5f Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 13 Oct 2020 12:27:32 -0600 Subject: [PATCH 011/236] assign rest of ModelChain outputs to ModelChain.results --- pvlib/modelchain.py | 133 ++++++++++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 61 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index b0ae19dfd5..67c1891043 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -265,7 +265,7 @@ def get_orientation(strategy, **kwargs): class ModelChainResult: # system-level information # weather: pd.DataFrame = field(default=None) - # solar_position: pd.DataFrame = field(default=None) + solar_position: pd.DataFrame = field(default=None) airmass: pd.DataFrame = field(default=None) ac: pd.Series = field(default=None) # per DC array information @@ -654,30 +654,31 @@ def infer_dc_model(self): 'set the model with the dc_model kwarg.') def sapm(self): - self.dc = self.system.sapm(self.effective_irradiance, - self.cell_temperature) + self.results.dc = self.system.sapm(self.results.effective_irradiance, + self.results.cell_temperature) - self.dc = self.system.scale_voltage_current_power(self.dc) + self.results.dc = self.system.scale_voltage_current_power( + self.results.dc) return self def _singlediode(self, calcparams_model_function): (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) = ( - calcparams_model_function(self.effective_irradiance, - self.cell_temperature)) + calcparams_model_function(self.results.effective_irradiance, + self.results.cell_temperature)) - self.diode_params = pd.DataFrame({'I_L': photocurrent, - 'I_o': saturation_current, - 'R_s': resistance_series, - 'R_sh': resistance_shunt, - 'nNsVth': nNsVth}) + self.results.diode_params = pd.DataFrame( + {'I_L': photocurrent, 'I_o': saturation_current, + 'R_s': resistance_series, 'R_sh': resistance_shunt, + 'nNsVth': nNsVth}) - self.dc = self.system.singlediode( + self.results.dc = self.system.singlediode( photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - self.dc = self.system.scale_voltage_current_power(self.dc).fillna(0) + self.results.dc = self.system.scale_voltage_current_power( + self.results.dc).fillna(0) return self @@ -691,8 +692,8 @@ def pvsyst(self): return self._singlediode(self.system.calcparams_pvsyst) def pvwatts_dc(self): - self.dc = self.system.pvwatts_dc(self.effective_irradiance, - self.cell_temperature) + self.results.dc = self.system.pvwatts_dc( + self.results.effective_irradiance, self.results.cell_temperature) return self @property @@ -743,15 +744,17 @@ def infer_ac_model(self): 'set the model with the ac_model kwarg.') def snlinverter(self): - self.ac = self.system.snlinverter(self.dc['v_mp'], self.dc['p_mp']) + self.results.ac = self.system.snlinverter(self.results.dc['v_mp'], + self.results.dc['p_mp']) return self def adrinverter(self): - self.ac = self.system.adrinverter(self.dc['v_mp'], self.dc['p_mp']) + self.results.ac = self.system.adrinverter(self.results.dc['v_mp'], + self.results.dc['p_mp']) return self def pvwatts_inverter(self): - self.ac = self.system.pvwatts_ac(self.dc).fillna(0) + self.results.ac = self.system.pvwatts_ac(self.results.dc).fillna(0) return self @property @@ -798,24 +801,27 @@ def infer_aoi_model(self): 'kwarg; or set aoi_model="no_loss".') def ashrae_aoi_loss(self): - self.aoi_modifier = self.system.get_iam(self.aoi, iam_model='ashrae') + self.results.aoi_modifier = self.system.get_iam( + self.results.aoi, iam_model='ashrae') return self def physical_aoi_loss(self): - self.aoi_modifier = self.system.get_iam(self.aoi, iam_model='physical') + self.results.aoi_modifier = self.system.get_iam(self.results.aoi, + iam_model='physical') return self def sapm_aoi_loss(self): - self.aoi_modifier = self.system.get_iam(self.aoi, iam_model='sapm') + self.results.aoi_modifier = self.system.get_iam(self.results.aoi, + iam_model='sapm') return self def martin_ruiz_aoi_loss(self): - self.aoi_modifier = self.system.get_iam(self.aoi, + self.results.aoi_modifier = self.system.get_iam(self.results.aoi, iam_model='martin_ruiz') return self def no_aoi_loss(self): - self.aoi_modifier = 1.0 + self.results.aoi_modifier = 1.0 return self @property @@ -858,18 +864,18 @@ def infer_spectral_model(self): 'spectral_model="no_loss".') def first_solar_spectral_loss(self): - self.spectral_modifier = self.system.first_solar_spectral_loss( + self.results.spectral_modifier = self.system.first_solar_spectral_loss( self.weather['precipitable_water'], self.results.airmass['airmass_absolute']) return self def sapm_spectral_loss(self): - self.spectral_modifier = self.system.sapm_spectral_loss( + self.results.spectral_modifier = self.system.sapm_spectral_loss( self.results.airmass['airmass_absolute']) return self def no_spectral_loss(self): - self.spectral_modifier = 1 + self.results.spectral_modifier = 1 return self @property @@ -923,26 +929,26 @@ def infer_temperature_model(self): .format(self.system.temperature_model_parameters)) def sapm_temp(self): - self.cell_temperature = self.system.sapm_celltemp( - self.total_irrad['poa_global'], self.weather['temp_air'], + self.results.cell_temperature = self.system.sapm_celltemp( + self.results.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.results.cell_temperature = self.system.pvsyst_celltemp( + self.results.total_irrad['poa_global'], self.weather['temp_air'], self.weather['wind_speed']) return self def faiman_temp(self): - self.cell_temperature = self.system.faiman_celltemp( - self.total_irrad['poa_global'], self.weather['temp_air'], + self.results.cell_temperature = self.system.faiman_celltemp( + self.results.total_irrad['poa_global'], self.weather['temp_air'], self.weather['wind_speed']) return self def fuentes_temp(self): - self.cell_temperature = self.system.fuentes_celltemp( - self.total_irrad['poa_global'], self.weather['temp_air'], + self.results.cell_temperature = self.system.fuentes_celltemp( + self.results.total_irrad['poa_global'], self.weather['temp_air'], self.weather['wind_speed']) return self @@ -970,7 +976,7 @@ def infer_losses_model(self): def pvwatts_losses(self): self.losses = (100 - self.system.pvwatts_losses()) / 100. - self.dc *= self.losses + self.results.dc *= self.losses return self def no_extra_losses(self): @@ -979,9 +985,10 @@ def no_extra_losses(self): def effective_irradiance_model(self): fd = self.system.module_parameters.get('FD', 1.) - self.effective_irradiance = self.spectral_modifier * ( - self.total_irrad['poa_direct']*self.aoi_modifier + - fd*self.total_irrad['poa_diffuse']) + self.results.effective_irradiance = self.results.spectral_modifier * ( + self.results.total_irrad['poa_direct'] * + self.results.aoi_modifier + + fd * self.results.total_irrad['poa_diffuse']) return self def complete_irradiance(self, weather): @@ -1029,7 +1036,7 @@ def complete_irradiance(self, weather): """ self.weather = weather - self.solar_position = self.location.get_solarposition( + self.results.solar_position = self.location.get_solarposition( self.weather.index, method=self.solar_position_method) icolumns = set(self.weather.columns) @@ -1040,22 +1047,23 @@ def complete_irradiance(self, weather): if {'ghi', 'dhi'} <= icolumns and 'dni' not in icolumns: clearsky = self.location.get_clearsky( - self.weather.index, solar_position=self.solar_position) + self.weather.index, solar_position=self.results.solar_position) self.weather.loc[:, 'dni'] = pvlib.irradiance.dni( self.weather.loc[:, 'ghi'], self.weather.loc[:, 'dhi'], - self.solar_position.zenith, + self.results.solar_position.zenith, clearsky_dni=clearsky['dni'], clearsky_tolerance=1.1) elif {'dni', 'dhi'} <= icolumns and 'ghi' not in icolumns: warnings.warn(wrn_txt, UserWarning) self.weather.loc[:, 'ghi'] = ( - self.weather.dni * tools.cosd(self.solar_position.zenith) + - self.weather.dhi) + self.weather.dhi + self.weather.dni * \ + tools.cosd(self.results.solar_position.zenith) + ) elif {'dni', 'ghi'} <= icolumns and 'dhi' not in icolumns: warnings.warn(wrn_txt, UserWarning) self.weather.loc[:, 'dhi'] = ( self.weather.ghi - self.weather.dni * - tools.cosd(self.solar_position.zenith)) + tools.cosd(self.results.solar_position.zenith)) return self @@ -1063,7 +1071,7 @@ def _prep_inputs_solar_pos(self, kwargs={}): """ Assign solar position """ - self.solar_position = self.location.get_solarposition( + self.results.solar_position = self.location.get_solarposition( self.weather.index, method=self.solar_position_method, **kwargs) return self @@ -1073,7 +1081,8 @@ def _prep_inputs_airmass(self): Assign airmass """ self.results.airmass = self.location.get_airmass( - solar_position=self.solar_position, model=self.airmass_model) + solar_position=self.results.solar_position, + model=self.airmass_model) return self def _prep_inputs_tracking(self): @@ -1081,23 +1090,24 @@ def _prep_inputs_tracking(self): Calculate tracker position and AOI """ self.tracking = self.system.singleaxis( - self.solar_position['apparent_zenith'], - self.solar_position['azimuth']) + self.results.solar_position['apparent_zenith'], + self.results.solar_position['azimuth']) self.tracking['surface_tilt'] = ( self.tracking['surface_tilt'] .fillna(self.system.axis_tilt)) self.tracking['surface_azimuth'] = ( self.tracking['surface_azimuth'] .fillna(self.system.axis_azimuth)) - self.aoi = self.tracking['aoi'] + self.results.aoi = self.tracking['aoi'] return self def _prep_inputs_fixed(self): """ Calculate AOI for fixed tilt system """ - self.aoi = self.system.get_aoi(self.solar_position['apparent_zenith'], - self.solar_position['azimuth']) + self.results.aoi = self.system.get_aoi( + self.results.solar_position['apparent_zenith'], + self.results.solar_position['azimuth']) return self def _verify_df(self, data, required): @@ -1129,7 +1139,7 @@ def _assign_weather(self, data): def _assign_total_irrad(self, data): key_list = [k for k in POA_KEYS if k in data] - self.total_irrad = data[key_list].copy() + self.results.total_irrad = data[key_list].copy() return self def prepare_inputs(self, weather): @@ -1180,16 +1190,16 @@ def prepare_inputs(self, weather): self.system.get_irradiance, self.tracking['surface_tilt'], self.tracking['surface_azimuth'], - self.solar_position['apparent_zenith'], - self.solar_position['azimuth']) + self.results.solar_position['apparent_zenith'], + self.results.solar_position['azimuth']) else: self._prep_inputs_fixed() get_irradiance = partial( self.system.get_irradiance, - self.solar_position['apparent_zenith'], - self.solar_position['azimuth']) + self.results.solar_position['apparent_zenith'], + self.results.solar_position['azimuth']) - self.total_irrad = get_irradiance( + self.results.total_irrad = get_irradiance( self.weather['dni'], self.weather['ghi'], self.weather['dhi'], @@ -1264,7 +1274,7 @@ def _prepare_temperature(self, data=None): """ if 'cell_temperature' in data: - self.cell_temperature = data['cell_temperature'] + self.results.cell_temperature = data['cell_temperature'] return self # cell_temperature is not in input. Calculate cell_temperature using @@ -1274,9 +1284,10 @@ def _prepare_temperature(self, data=None): if (('module_temperature' in data) and (self.temperature_model.__name__ == 'sapm_temp')): # use SAPM cell temperature model only - self.cell_temperature = pvlib.temperature.sapm_cell_from_module( + self.results.cell_temperature = \ + pvlib.temperature.sapm_cell_from_module( module_temperature=data['module_temperature'], - poa_global=self.total_irrad['poa_global'], + poa_global=self.results.total_irrad['poa_global'], deltaT=self.system.temperature_model_parameters['deltaT']) return self @@ -1435,7 +1446,7 @@ def run_model_from_effective_irradiance(self, data=None): self._assign_weather(data) self._assign_total_irrad(data) - self.effective_irradiance = data['effective_irradiance'] + self.results.effective_irradiance = data['effective_irradiance'] self._run_from_effective_irrad(data) return self From 22d6414014ed153c44fdf981b3f45a70f682fa8b Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 13 Oct 2020 12:40:05 -0600 Subject: [PATCH 012/236] first cut at ModelChain tests --- pvlib/tests/test_modelchain.py | 78 +++++++++++++++++----------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index d02611bbf3..3e1e9e45cf 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -235,7 +235,7 @@ def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, index=times) - ac = mc.run_model(irradiance).ac + ac = mc.run_model(irradiance).results.ac expected = pd.Series(np.array([187.80746494643176, -0.02]), index=times) @@ -255,7 +255,7 @@ def test_run_model_perez(sapm_dc_snl_ac_system, location): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, index=times) - ac = mc.run_model(irradiance).ac + ac = mc.run_model(irradiance).results.ac expected = pd.Series(np.array([187.94295642, -2.00000000e-02]), index=times) @@ -269,7 +269,7 @@ def test_run_model_gueymard_perez(sapm_dc_snl_ac_system, location): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, index=times) - ac = mc.run_model(irradiance).ac + ac = mc.run_model(irradiance).results.ac expected = pd.Series(np.array([187.94317405, -2.00000000e-02]), index=times) @@ -290,7 +290,7 @@ def test_run_model_with_weather_sapm_temp(sapm_dc_snl_ac_system, location, # assert_series_equal on call_args 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 + assert not mc.results.ac.empty def test_run_model_with_weather_pvsyst_temp(sapm_dc_snl_ac_system, location, @@ -308,7 +308,7 @@ def test_run_model_with_weather_pvsyst_temp(sapm_dc_snl_ac_system, location, 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 + assert not mc.results.ac.empty def test_run_model_with_weather_faiman_temp(sapm_dc_snl_ac_system, location, @@ -326,7 +326,7 @@ def test_run_model_with_weather_faiman_temp(sapm_dc_snl_ac_system, location, assert m_faiman.call_count == 1 assert_series_equal(m_faiman.call_args[0][1], weather['temp_air']) assert_series_equal(m_faiman.call_args[0][2], weather['wind_speed']) - assert not mc.ac.empty + assert not mc.results.ac.empty def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location, @@ -343,7 +343,7 @@ def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location, assert m_fuentes.call_count == 1 assert_series_equal(m_fuentes.call_args[0][1], weather['temp_air']) assert_series_equal(m_fuentes.call_args[0][2], weather['wind_speed']) - assert not mc.ac.empty + assert not mc.results.ac.empty def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker): @@ -359,8 +359,8 @@ def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker): assert system.singleaxis.call_count == 1 assert (mc.tracking.columns == ['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']).all() - assert mc.ac[0] > 0 - assert np.isnan(mc.ac[1]) + assert mc.results.ac[0] > 0 + assert np.isnan(mc.results.ac[1]) def test__assign_total_irrad(sapm_dc_snl_ac_system, location, weather, @@ -368,7 +368,7 @@ def test__assign_total_irrad(sapm_dc_snl_ac_system, location, weather, data = pd.concat([weather, total_irrad], axis=1) mc = ModelChain(sapm_dc_snl_ac_system, location) mc._assign_total_irrad(data) - assert_frame_equal(mc.total_irrad, total_irrad) + assert_frame_equal(mc.results.total_irrad, total_irrad) def test_prepare_inputs_from_poa(sapm_dc_snl_ac_system, location, @@ -385,7 +385,7 @@ def test_prepare_inputs_from_poa(sapm_dc_snl_ac_system, location, # weather attribute assert_frame_equal(mc.weather, weather_expected) # total_irrad attribute - assert_frame_equal(mc.total_irrad, total_irrad) + assert_frame_equal(mc.results.total_irrad, total_irrad) def test__prepare_temperature(sapm_dc_snl_ac_system, location, weather, @@ -406,13 +406,13 @@ def test__prepare_temperature(sapm_dc_snl_ac_system, location, weather, assert_series_equal(mc.cell_temperature, expected) data['cell_temperature'] = [50., 35.] mc._prepare_temperature(data) - assert_series_equal(mc.cell_temperature, data['cell_temperature']) + assert_series_equal(mc.results.cell_temperature, data['cell_temperature']) def test_run_model_from_poa(sapm_dc_snl_ac_system, location, total_irrad): mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', spectral_model='no_loss') - ac = mc.run_model_from_poa(total_irrad).ac + ac = mc.run_model_from_poa(total_irrad).results.ac expected = pd.Series(np.array([149.280238, 96.678385]), index=total_irrad.index) assert_series_equal(ac, expected) @@ -428,7 +428,7 @@ def test_run_model_from_poa_tracking(sapm_dc_snl_ac_system, location, inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters) mc = ModelChain(system, location, aoi_model='no_loss', spectral_model='no_loss') - ac = mc.run_model_from_poa(total_irrad).ac + ac = mc.run_model_from_poa(total_irrad).results.ac assert (mc.tracking.columns == ['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']).all() expected = pd.Series(np.array([149.280238, 96.678385]), @@ -443,7 +443,7 @@ def test_run_model_from_effective_irradiance(sapm_dc_snl_ac_system, location, data['effective_irradiance'] = data['poa_global'] mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', spectral_model='no_loss') - ac = mc.run_model_from_effective_irradiance(data).ac + ac = mc.run_model_from_effective_irradiance(data).results.ac expected = pd.Series(np.array([149.280238, 96.678385]), index=data.index) assert_series_equal(ac, expected) @@ -491,7 +491,7 @@ def test_infer_dc_model(sapm_dc_snl_ac_system, cec_dc_snl_ac_system, temperature_model=temp_model_function[dc_model]) mc.run_model(weather) assert m.call_count == 1 - assert isinstance(mc.dc, (pd.Series, pd.DataFrame)) + assert isinstance(mc.results.dc, (pd.Series, pd.DataFrame)) @pytest.mark.parametrize('dc_model', ['sapm', 'cec', 'cec_native']) @@ -559,7 +559,7 @@ def test_dc_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather, mc.run_model(weather) assert m.call_count == 1 assert isinstance(mc.ac, (pd.Series, pd.DataFrame)) - assert not mc.ac.empty + assert not mc.results.ac.empty def acdc(mc): @@ -583,9 +583,9 @@ def test_ac_models(sapm_dc_snl_ac_system, cec_dc_adr_ac_system, m = mocker.spy(system, ac_method_name[ac_model]) mc.run_model(weather) assert m.call_count == 1 - assert isinstance(mc.ac, pd.Series) - assert not mc.ac.empty - assert mc.ac[1] < 1 + assert isinstance(mc.results.ac, pd.Series) + assert not mc.results.ac.empty + assert mc.results.ac[1] < 1 # TODO in v0.9: remove this test for a deprecation warning @@ -609,8 +609,8 @@ def test_ac_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather, aoi_model='no_loss', spectral_model='no_loss') mc.run_model(weather) assert m.call_count == 1 - assert_series_equal(mc.ac, mc.dc) - assert not mc.ac.empty + assert_series_equal(mc.results.ac, mc.results.dc) + assert not mc.results.ac.empty def test_ac_model_not_a_model(pvwatts_dc_pvwatts_ac_system, location, weather): @@ -622,7 +622,7 @@ def test_ac_model_not_a_model(pvwatts_dc_pvwatts_ac_system, location, weather): def constant_aoi_loss(mc): - mc.aoi_modifier = 0.9 + mc.results.aoi_modifier = 0.9 @pytest.mark.parametrize('aoi_model', [ @@ -635,20 +635,20 @@ def test_aoi_models(sapm_dc_snl_ac_system, location, aoi_model, m = mocker.spy(sapm_dc_snl_ac_system, 'get_iam') mc.run_model(weather=weather) assert m.call_count == 1 - assert isinstance(mc.ac, pd.Series) - assert not mc.ac.empty - assert mc.ac[0] > 150 and mc.ac[0] < 200 - assert mc.ac[1] < 1 + assert isinstance(mc.results.ac, pd.Series) + assert not mc.results.ac.empty + assert mc.results.ac[0] > 150 and mc.results.ac[0] < 200 + assert mc.results.ac[1] < 1 def test_aoi_model_no_loss(sapm_dc_snl_ac_system, location, weather): mc = ModelChain(sapm_dc_snl_ac_system, location, dc_model='sapm', aoi_model='no_loss', spectral_model='no_loss') mc.run_model(weather) - assert mc.aoi_modifier == 1.0 - assert not mc.ac.empty - assert mc.ac[0] > 150 and mc.ac[0] < 200 - assert mc.ac[1] < 1 + assert mc.results.aoi_modifier == 1.0 + assert not mc.results.ac.empty + assert mc.results.ac[0] > 150 and mc.results.ac[0] < 200 + assert mc.results.ac[1] < 1 def test_aoi_model_user_func(sapm_dc_snl_ac_system, location, weather, mocker): @@ -657,10 +657,10 @@ def test_aoi_model_user_func(sapm_dc_snl_ac_system, location, weather, mocker): aoi_model=constant_aoi_loss, spectral_model='no_loss') mc.run_model(weather) assert m.call_count == 1 - assert mc.aoi_modifier == 0.9 - assert not mc.ac.empty - assert mc.ac[0] > 140 and mc.ac[0] < 200 - assert mc.ac[1] < 1 + assert mc.results.aoi_modifier == 0.9 + assert not mc.results.ac.empty + assert mc.results.ac[0] > 140 and mc.results.ac[0] < 200 + assert mc.results.ac[1] < 1 @pytest.mark.parametrize('aoi_model', [ @@ -683,7 +683,7 @@ def test_infer_aoi_model_invalid(location, system_no_aoi): def constant_spectral_loss(mc): - mc.spectral_modifier = 0.9 + mc.results.spectral_modifier = 0.9 @pytest.mark.parametrize('spectral_model', [ @@ -695,7 +695,7 @@ def test_spectral_models(sapm_dc_snl_ac_system, location, spectral_model, weather['precipitable_water'] = [0.3, 0.5] mc = ModelChain(sapm_dc_snl_ac_system, location, dc_model='sapm', aoi_model='no_loss', spectral_model=spectral_model) - spectral_modifier = mc.run_model(weather).spectral_modifier + spectral_modifier = mc.run_model(weather).results.spectral_modifier assert isinstance(spectral_modifier, (pd.Series, float, int)) @@ -724,7 +724,7 @@ def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather, aoi_model='no_loss', spectral_model='no_loss', losses_model='no_loss') mc.run_model(weather) - assert not np.allclose(mc.dc, dc_with_loss, equal_nan=True) + assert not np.allclose(mc.results.dc, dc_with_loss, equal_nan=True) def test_losses_models_ext_def(pvwatts_dc_pvwatts_ac_system, location, weather, @@ -737,7 +737,7 @@ def test_losses_models_ext_def(pvwatts_dc_pvwatts_ac_system, location, weather, assert m.call_count == 1 assert isinstance(mc.ac, (pd.Series, pd.DataFrame)) assert mc.losses == 0.9 - assert not mc.ac.empty + assert not mc.results.ac.empty def test_losses_models_no_loss(pvwatts_dc_pvwatts_ac_system, location, weather, From 312c3f6a521281c872b7e271b28bfedf081f56f6 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 13 Oct 2020 12:47:59 -0600 Subject: [PATCH 013/236] formatting --- pvlib/modelchain.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 67c1891043..ff9bde183d 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -655,7 +655,7 @@ def infer_dc_model(self): def sapm(self): self.results.dc = self.system.sapm(self.results.effective_irradiance, - self.results.cell_temperature) + self.results.cell_temperature) self.results.dc = self.system.scale_voltage_current_power( self.results.dc) @@ -986,7 +986,7 @@ def no_extra_losses(self): def effective_irradiance_model(self): fd = self.system.module_parameters.get('FD', 1.) self.results.effective_irradiance = self.results.spectral_modifier * ( - self.results.total_irrad['poa_direct'] * + self.results.total_irrad['poa_direct'] * self.results.aoi_modifier + fd * self.results.total_irrad['poa_diffuse']) return self @@ -1056,9 +1056,9 @@ def complete_irradiance(self, weather): elif {'dni', 'dhi'} <= icolumns and 'ghi' not in icolumns: warnings.warn(wrn_txt, UserWarning) self.weather.loc[:, 'ghi'] = ( - self.weather.dhi + self.weather.dni * \ - tools.cosd(self.results.solar_position.zenith) - ) + self.weather.dhi + self.weather.dni * + tools.cosd(self.results.solar_position.zenith) + ) elif {'dni', 'ghi'} <= icolumns and 'dhi' not in icolumns: warnings.warn(wrn_txt, UserWarning) self.weather.loc[:, 'dhi'] = ( From b9029d6fc26d6d62385580b242fba4bc1cca870e Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 13 Oct 2020 14:24:35 -0600 Subject: [PATCH 014/236] more test development --- pvlib/modelchain.py | 5 +++-- pvlib/tests/test_modelchain.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index ff9bde183d..6df7014bda 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -276,6 +276,7 @@ class ModelChainResult: cell_temperature: pd.Series = field(default=None) effective_irradiance: pd.Series = field(default=None) dc: pd.Series = field(default=None) + # losses: dont_know_tye_type = field(default=None) array_ac: pd.Series = field(default=None) diode_params: pd.DataFrame = field(default=None) @@ -1056,8 +1057,8 @@ def complete_irradiance(self, weather): elif {'dni', 'dhi'} <= icolumns and 'ghi' not in icolumns: warnings.warn(wrn_txt, UserWarning) self.weather.loc[:, 'ghi'] = ( - self.weather.dhi + self.weather.dni * - tools.cosd(self.results.solar_position.zenith) + self.weather.dhi + self.weather.dni * + tools.cosd(self.results.solar_position.zenith) ) elif {'dni', 'ghi'} <= icolumns and 'dhi' not in icolumns: warnings.warn(wrn_txt, UserWarning) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 3e1e9e45cf..86c237d306 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -403,7 +403,7 @@ def test__prepare_temperature(sapm_dc_snl_ac_system, location, weather, data['module_temperature'] = [40., 30.] mc._prepare_temperature(data) expected = pd.Series([42.4, 31.5], index=data.index) - assert_series_equal(mc.cell_temperature, expected) + assert_series_equal(mc.results.cell_temperature, expected) data['cell_temperature'] = [50., 35.] mc._prepare_temperature(data) assert_series_equal(mc.results.cell_temperature, data['cell_temperature']) @@ -450,8 +450,8 @@ def test_run_model_from_effective_irradiance(sapm_dc_snl_ac_system, location, def poadc(mc): - mc.dc = mc.total_irrad['poa_global'] * 0.2 - mc.dc.name = None # assert_series_equal will fail without this + mc.results.dc = mc.results.total_irrad['poa_global'] * 0.2 + mc.results.dc.name = None # assert_series_equal will fail without this @pytest.mark.parametrize('dc_model', [ @@ -558,12 +558,12 @@ def test_dc_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather, aoi_model='no_loss', spectral_model='no_loss') mc.run_model(weather) assert m.call_count == 1 - assert isinstance(mc.ac, (pd.Series, pd.DataFrame)) + assert isinstance(mc.results.ac, (pd.Series, pd.DataFrame)) assert not mc.results.ac.empty def acdc(mc): - mc.ac = mc.dc + mc.results.ac = mc.results.dc @pytest.mark.parametrize('ac_model', ['sandia', 'adr', 'pvwatts']) @@ -701,7 +701,7 @@ def test_spectral_models(sapm_dc_snl_ac_system, location, spectral_model, def constant_losses(mc): mc.losses = 0.9 - mc.dc *= mc.losses + mc.results.dc *= mc.losses def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather, @@ -715,11 +715,11 @@ def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather, mc.run_model(weather) assert m.call_count == 1 m.assert_called_with(age=age) - assert isinstance(mc.ac, (pd.Series, pd.DataFrame)) - assert not mc.ac.empty + assert isinstance(mc.results.ac, (pd.Series, pd.DataFrame)) + assert not mc.results.ac.empty # check that we're applying correction to dc # GH 696 - dc_with_loss = mc.dc + dc_with_loss = mc.results.dc mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model='pvwatts', aoi_model='no_loss', spectral_model='no_loss', losses_model='no_loss') From bc2c2c181aea31f44734823f8176f48679a78553 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 13 Oct 2020 14:33:30 -0600 Subject: [PATCH 015/236] few more additions --- pvlib/tests/test_modelchain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 86c237d306..c83d798b57 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -399,7 +399,7 @@ def test__prepare_temperature(sapm_dc_snl_ac_system, location, weather, mc._assign_total_irrad(data) mc._prepare_temperature(data) expected = pd.Series([48.928025, 38.080016], index=data.index) - assert_series_equal(mc.cell_temperature, expected) + assert_series_equal(mc.results.cell_temperature, expected) data['module_temperature'] = [40., 30.] mc._prepare_temperature(data) expected = pd.Series([42.4, 31.5], index=data.index) @@ -735,7 +735,7 @@ def test_losses_models_ext_def(pvwatts_dc_pvwatts_ac_system, location, weather, losses_model=constant_losses) mc.run_model(weather) assert m.call_count == 1 - assert isinstance(mc.ac, (pd.Series, pd.DataFrame)) + assert isinstance(mc.results.ac, (pd.Series, pd.DataFrame)) assert mc.losses == 0.9 assert not mc.results.ac.empty From 8ebcedc45187d94902a954671a4fc41f1613680c Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 13 Oct 2020 15:06:21 -0600 Subject: [PATCH 016/236] put dataclasses in pip section for py36 --- ci/requirements-py36-min.yml | 1 + ci/requirements-py36.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ci/requirements-py36-min.yml b/ci/requirements-py36-min.yml index 31e64f1ba0..29f63c1be1 100644 --- a/ci/requirements-py36-min.yml +++ b/ci/requirements-py36-min.yml @@ -13,6 +13,7 @@ dependencies: - pytz - requests - pip: + - dataclasses - numpy==1.12.0 - pandas==0.22.0 - scipy==1.2.0 diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml index acb7513ca8..fb37fe8404 100644 --- a/ci/requirements-py36.yml +++ b/ci/requirements-py36.yml @@ -5,7 +5,6 @@ channels: dependencies: - coveralls - cython - - dataclasses - ephem - netcdf4 - nose @@ -28,5 +27,6 @@ dependencies: - siphon # conda-forge - statsmodels - pip: + - dataclasses - nrel-pysam>=2.0 - pvfactors==1.4.1 From 05091f062fe639b9fcfa897a71546dfc84d13f2b Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 12 Oct 2020 14:37:36 -0600 Subject: [PATCH 017/236] Store a list of Arrays in PVSystem Adds the @singleton_as_scalar decorator to make a PVSystem with a single Array appear unchanged from the original PVSystem implementation. --- pvlib/pvsystem.py | 82 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 72f85ad903..b1797d8f29 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -52,7 +52,7 @@ def _combine_localized_attributes(pvsystem=None, location=None, **kwargs): """ if pvsystem is not None: pv_dict = pvsystem.__dict__ - pv_dict = {**pv_dict, **pv_dict['_array'].__dict__} + pv_dict = {**pv_dict, **pv_dict['_arrays'][0].__dict__} else: pv_dict = {} @@ -67,6 +67,22 @@ def _combine_localized_attributes(pvsystem=None, location=None, **kwargs): return new_kwargs +def singleton_as_scalar(func): + """Return a scalar if `func` returns a singleton list. + + Parameters + ---------- + func : funciton + A function that returns a list-like object. + """ + def f(*args, **kwargs): + x = func(*args, **kwargs) + if len(x) == 1: + return x[0] + return x + return f + + # 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. @@ -175,7 +191,7 @@ def __init__(self, racking_model=None, losses_parameters=None, name=None, **kwargs): - self._array = Array( + self._arrays = [Array( surface_tilt, surface_azimuth, albedo, @@ -187,7 +203,7 @@ def __init__(self, modules_per_string, strings_per_inverter, racking_model - ) + )] self.inverter = inverter if inverter_parameters is None: @@ -215,6 +231,7 @@ def __repr__(self): return ('PVSystem:\n ' + '\n '.join( f'{attr}: {getattr(self, attr)}' for attr in attrs)) + @singleton_as_scalar def get_aoi(self, solar_zenith, solar_azimuth): """Get the angle of incidence on the system. @@ -231,8 +248,10 @@ def get_aoi(self, solar_zenith, solar_azimuth): The angle of incidence """ - return self._array.get_aoi(solar_zenith, solar_azimuth) + return [array.get_aoi(solar_zenith, solar_azimuth) + for array in self._arrays] + @singleton_as_scalar def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, model='haydavies', **kwargs): @@ -269,12 +288,12 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, poa_irradiance : DataFrame Column names are: ``total, beam, sky, ground``. """ - return self._array.get_irradiance( - solar_zenith, solar_azimuth, - dni, ghi, dhi, - dni_extra, airmass - ) + return [array.get_irradiance(solar_zenith, solar_azimuth, + dni, ghi, dhi, + dni_extra, airmass) + for array in self._arrays] + @singleton_as_scalar def get_iam(self, aoi, iam_model='physical'): """ Determine the incidence angle modifier using the method specified by @@ -292,7 +311,6 @@ def get_iam(self, aoi, iam_model='physical'): aoi_model : string, default 'physical' The IAM model to be used. Valid strings are 'physical', 'ashrae', 'martin_ruiz' and 'sapm'. - Returns ------- iam : numeric @@ -302,7 +320,7 @@ def get_iam(self, aoi, iam_model='physical'): ------ ValueError if `iam_model` is not a valid model name. """ - return self._array.get_iam(aoi, iam_model) + return [array.get_iam(aoi, iam_model) for array in self._arrays] def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): """ @@ -833,60 +851,74 @@ def localize(self, location=None, latitude=None, longitude=None, return LocalizedPVSystem(pvsystem=self, location=location) @property + @singleton_as_scalar def module_parameters(self): - return self._array.module_parameters + return [array.module_parameters for array in self._arrays] @property + @singleton_as_scalar def module(self): - return self._array.module + return [array.module for array in self._arrays] @property + @singleton_as_scalar def module_type(self): - return self._array.module_type + return [array.module_type for array in self._arrays] @property + @singleton_as_scalar def temperature_model_parameters(self): - return self._array.temperature_model_parameters + return [array.temperature_model_parameters for array in self._arrays] @temperature_model_parameters.setter def temperature_model_parameters(self, value): - self._array.temperature_model_parameters = value + for array in self._arrays: + array.temperature_model_parameters = value @property + @singleton_as_scalar def surface_tilt(self): - return self._array.surface_tilt + return [array.surface_tilt for array in self._arrays] @surface_tilt.setter def surface_tilt(self, value): - self._array.surface_tilt = value + for array in self._arrays: + array.surface_tilt = value @property + @singleton_as_scalar def surface_azimuth(self): - return self._array.surface_azimuth + return [array.surface_azimuth for array in self._arrays] @surface_azimuth.setter def surface_azimuth(self, value): - self._array.surface_azimuth = value + for array in self._arrays: + array.surface_azimuth = value @property + @singleton_as_scalar def albedo(self): - return self._array.albedo + return [array.albedo for array in self._arrays] @property + @singleton_as_scalar def racking_model(self): - return self._array.racking_model + return [array.racking_model for array in self._arrays] @racking_model.setter def racking_model(self, value): - self._array.racking_model = value + for array in self._arrays: + array.racking_model = value @property + @singleton_as_scalar def modules_per_string(self): - return self._array.modules_per_string + return [array.modules_per_string for array in self._arrays] @property + @singleton_as_scalar def strings_per_inverter(self): - return self._array.strings + return [array.strings for array in self._arrays] @deprecated('0.8', alternative='PVSystem, Location, and ModelChain', From 660240144b84fc97425ebc48da5afe019fad77ad Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 13 Oct 2020 14:04:20 -0600 Subject: [PATCH 018/236] Wrap per-array inputs in a list Adds a decorator (@list_or_scalar) used to specify which parameters should be lists of the same length as PVSystem._arrays. The decorator handles validation by transforming non-list values to singleton lists then comparing to the length of the _arrays field on the PVSystem instance passed as the first parameter. Needs tests with multiple Arrays, but must wait until we have an API for adding multiple Arrays to a PVSystem. --- pvlib/pvsystem.py | 362 ++++++++++++++++++++++++++++++---------------- 1 file changed, 236 insertions(+), 126 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index b1797d8f29..dec779da45 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -4,6 +4,8 @@ """ from collections import OrderedDict +import functools +import inspect import io import os from urllib.request import urlopen @@ -75,14 +77,35 @@ def singleton_as_scalar(func): func : funciton A function that returns a list-like object. """ + @functools.wraps(func) def f(*args, **kwargs): + return_list = kwargs.pop('return_list', False) x = func(*args, **kwargs) - if len(x) == 1: + if len(x) == 1 and not return_list: return x[0] return x return f +def list_or_scalar(*list_params): + def list_or_scalar(func): + @functools.wraps(func) + def f(ref, *args, **kwargs): + sig = inspect.signature(func) + bindings = sig.bind(*tuple([ref, *args]), **kwargs) + for param in bindings.arguments.keys(): + if param in list_params: + if not isinstance(bindings.arguments[param], list): + bindings.arguments[param] = [bindings.arguments[param]] + if len(bindings.arguments[param]) != len(ref._arrays): + raise ValueError( + f"Length mismatch for parameter {param}" + ) + return func(*bindings.args, **bindings.kwargs) + return f + return list_or_scalar + + # 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. @@ -294,6 +317,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, for array in self._arrays] @singleton_as_scalar + @list_or_scalar('aoi') def get_iam(self, aoi, iam_model='physical'): """ Determine the incidence angle modifier using the method specified by @@ -320,8 +344,11 @@ def get_iam(self, aoi, iam_model='physical'): ------ ValueError if `iam_model` is not a valid model name. """ - return [array.get_iam(aoi, iam_model) for array in self._arrays] + return [array.get_iam(aoi, iam_model) + for array, aoi in zip(self._arrays, aoi)] + @singleton_as_scalar + @list_or_scalar('effective_irradiance', 'temp_cell') def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`calcparams_desoto` function, the input @@ -344,13 +371,24 @@ def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): See pvsystem.calcparams_desoto for details """ - kwargs = _build_kwargs(['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', - 'R_s', 'alpha_sc', 'EgRef', 'dEgdT', - 'irrad_ref', 'temp_ref'], - self.module_parameters) + build_kwargs = functools.partial( + _build_kwargs, + ['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', + 'R_s', 'alpha_sc', 'EgRef', 'dEgdT', + 'irrad_ref', 'temp_ref'] + ) - return calcparams_desoto(effective_irradiance, temp_cell, **kwargs) + return [ + calcparams_desoto( + effective_irradiance, temp_cell, + **build_kwargs(array.module_parameters) + ) + for array, effective_irradiance, temp_cell + in zip(self._arrays, effective_irradiance, temp_cell) + ] + @singleton_as_scalar + @list_or_scalar('effective_irradiance', 'temp_cell') def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`calcparams_cec` function, the input @@ -373,13 +411,24 @@ def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): See pvsystem.calcparams_cec for details """ - kwargs = _build_kwargs(['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', - 'R_s', 'alpha_sc', 'Adjust', 'EgRef', 'dEgdT', - 'irrad_ref', 'temp_ref'], - self.module_parameters) + build_kwargs = functools.partial( + _build_kwargs, + ['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', + 'R_s', 'alpha_sc', 'Adjust', 'EgRef', 'dEgdT', + 'irrad_ref', 'temp_ref'] + ) - return calcparams_cec(effective_irradiance, temp_cell, **kwargs) + return [ + calcparams_cec( + effective_irradiance, temp_cell, + **build_kwargs(array.module_parameters) + ) + for array, effective_irradiance, temp_cell + in zip(self._arrays, effective_irradiance, temp_cell) + ] + @singleton_as_scalar + @list_or_scalar('effective_irradiance', 'temp_cell') def calcparams_pvsyst(self, effective_irradiance, temp_cell): """ Use the :py:func:`calcparams_pvsyst` function, the input @@ -399,15 +448,26 @@ def calcparams_pvsyst(self, effective_irradiance, temp_cell): See pvsystem.calcparams_pvsyst for details """ - kwargs = _build_kwargs(['gamma_ref', 'mu_gamma', 'I_L_ref', 'I_o_ref', - 'R_sh_ref', 'R_sh_0', 'R_sh_exp', - 'R_s', 'alpha_sc', 'EgRef', - 'irrad_ref', 'temp_ref', - 'cells_in_series'], - self.module_parameters) + build_kwargs = functools.partial( + _build_kwargs, + ['gamma_ref', 'mu_gamma', 'I_L_ref', 'I_o_ref', + 'R_sh_ref', 'R_sh_0', 'R_sh_exp', + 'R_s', 'alpha_sc', 'EgRef', + 'irrad_ref', 'temp_ref', + 'cells_in_series'] + ) - return calcparams_pvsyst(effective_irradiance, temp_cell, **kwargs) + return [ + calcparams_pvsyst( + effective_irradiance, temp_cell, + **build_kwargs(array.module_parameters) + ) + for array, effective_irradiance, temp_cell + in zip(self._arrays, effective_irradiance, temp_cell) + ] + @singleton_as_scalar + @list_or_scalar('effective_irradiance', 'temp_cell') def sapm(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`sapm` function, the input parameters, @@ -429,8 +489,12 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): ------- See pvsystem.sapm for details """ - return sapm(effective_irradiance, temp_cell, self.module_parameters) + return [sapm(effective_irradiance, temp_cell, array.module_parameters) + for array, effective_irradiance, temp_cell + in zip(self._arrays, effective_irradiance, temp_cell)] + @singleton_as_scalar + @list_or_scalar('poa_global') def sapm_celltemp(self, poa_global, temp_air, wind_speed): """Uses :py:func:`temperature.sapm_cell` to calculate cell temperatures. @@ -450,26 +514,32 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): ------- numeric, values in degrees C. """ - # warn user about change in default behavior in 0.9. - if (self.temperature_model_parameters == {} and self.module_type - is None and self.racking_model is None): - warnings.warn( - 'temperature_model_parameters, racking_model, and module_type ' - 'are not specified. Reverting to deprecated default: SAPM ' - 'cell temperature model parameters for a glass/glass module ' - 'in open racking. In v0.9, temperature_model_parameters or a ' - 'valid combination of racking_model and module_type will be ' - 'required.', - pvlibDeprecationWarning) - params = temperature._temperature_model_params( - 'sapm', 'open_rack_glass_glass') - self.temperature_model_parameters = params - - kwargs = _build_kwargs(['a', 'b', 'deltaT'], - self.temperature_model_parameters) - return temperature.sapm_cell(poa_global, temp_air, wind_speed, - **kwargs) + for array in self._arrays: + # warn user about change in default behavior in 0.9. + if (array.temperature_model_parameters == {} and array.module_type + is None and array.racking_model is None): + warnings.warn( + 'temperature_model_parameters, racking_model, and ' + 'module_type are not specified. Reverting to deprecated ' + 'default: SAPM cell temperature model parameters for a ' + 'glass/glass module in open racking. In v0.9, ' + 'temperature_model_parameters or a valid combination of ' + 'racking_model and module_type will be required.', + pvlibDeprecationWarning) + params = temperature._temperature_model_params( + 'sapm', 'open_rack_glass_glass') + array.temperature_model_parameters = params + + build_kwargs = functools.partial(_build_kwargs, ['a', 'b', 'deltaT']) + return [ + temperature.sapm_cell( + poa_global, temp_air, wind_speed, + **build_kwargs(array.temperature_model_parameters) + ) + for array, poa_global in zip(self._arrays, poa_global) + ] + @singleton_as_scalar def sapm_spectral_loss(self, airmass_absolute): """ Use the :py:func:`sapm_spectral_loss` function, the input @@ -485,8 +555,11 @@ def sapm_spectral_loss(self, airmass_absolute): F1 : numeric The SAPM spectral loss coefficient. """ - return sapm_spectral_loss(airmass_absolute, self.module_parameters) + return [sapm_spectral_loss(airmass_absolute, array.module_parameters) + for array in self._arrays] + @singleton_as_scalar + @list_or_scalar('poa_direct', 'poa_diffuse', 'aoi') def sapm_effective_irradiance(self, poa_direct, poa_diffuse, airmass_absolute, aoi, reference_irradiance=1000): @@ -514,10 +587,16 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, effective_irradiance : numeric The SAPM effective irradiance. [W/m2] """ - return sapm_effective_irradiance( - poa_direct, poa_diffuse, airmass_absolute, aoi, - self.module_parameters) + return [ + sapm_effective_irradiance( + poa_direct, poa_diffuse, airmass_absolute, aoi, + array.module_parameters) + for array, poa_direct, poa_diffuse, aoi + in zip(self._arrays, poa_direct, poa_diffuse, aoi) + ] + @singleton_as_scalar + @list_or_scalar('poa_global') def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): """Uses :py:func:`temperature.pvsyst_cell` to calculate cell temperature. @@ -539,13 +618,20 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): ------- numeric, values in degrees C. """ - kwargs = _build_kwargs(['eta_m', 'alpha_absorption'], - self.module_parameters) - kwargs.update(_build_kwargs(['u_c', 'u_v'], - self.temperature_model_parameters)) - return temperature.pvsyst_cell(poa_global, temp_air, wind_speed, - **kwargs) + build_kwargs = lambda array: { + **_build_kwargs(['eta_m', 'alpha_absorption'], + array.module_parameters), + **_build_kwargs(['u_c', 'u_v'], + array.temperature_model_parameters) + } + return [ + temperature.pvsyst_cell(poa_global, temp_air, wind_speed, + **build_kwargs(array)) + for array, poa_global in zip(self._arrays, poa_global) + ] + @singleton_as_scalar + @list_or_scalar('poa_global') def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): """ Use :py:func:`temperature.faiman` to calculate cell temperature. @@ -567,11 +653,16 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): ------- numeric, values in degrees C. """ - kwargs = _build_kwargs(['u0', 'u1'], - self.temperature_model_parameters) - return temperature.faiman(poa_global, temp_air, wind_speed, - **kwargs) + return [ + temperature.faiman( + poa_global, temp_air, wind_speed, + **_build_kwargs( + ['u0', 'u1'], array.temperature_model_parameters)) + for array, poa_global in zip(self._arrays, poa_global) + ] + @singleton_as_scalar + @list_or_scalar('poa_global') def fuentes_celltemp(self, poa_global, temp_air, wind_speed): """ Use :py:func:`temperature.fuentes` to calculate cell temperature. @@ -603,15 +694,22 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): """ # default to using the PVSystem attribute, but allow user to # override with a custom surface_tilt value - kwargs = {'surface_tilt': self.surface_tilt} - temp_model_kwargs = _build_kwargs([ - 'noct_installed', 'module_height', 'wind_height', 'emissivity', - 'absorption', 'surface_tilt', 'module_width', 'module_length'], - self.temperature_model_parameters) - kwargs.update(temp_model_kwargs) - return temperature.fuentes(poa_global, temp_air, wind_speed, - **kwargs) + def _build_kwargs_fuentes(array): + kwargs = {'surface_tilt': array.surface_tilt} + temp_model_kwargs = _build_kwargs([ + 'noct_installed', 'module_height', 'wind_height', 'emissivity', + 'absorption', 'surface_tilt', 'module_width', 'module_length'], + array.temperature_model_parameters) + kwargs.update(temp_model_kwargs) + return kwargs + return [ + temperature.fuentes( + poa_global, temp_air, wind_speed, + **_build_kwargs_fuentes(array)) + for array, poa_global in zip(self._arrays, poa_global) + ] + @singleton_as_scalar def first_solar_spectral_loss(self, pw, airmass_absolute): """ @@ -642,62 +740,22 @@ def first_solar_spectral_loss(self, pw, airmass_absolute): electrical current. """ - if 'first_solar_spectral_coefficients' in \ - self.module_parameters.keys(): - coefficients = \ - self.module_parameters['first_solar_spectral_coefficients'] - module_type = None - else: - module_type = self._infer_cell_type() - coefficients = None - - return atmosphere.first_solar_spectral_correction(pw, - airmass_absolute, - module_type, - coefficients) - - def _infer_cell_type(self): - - """ - Examines module_parameters and maps the Technology key for the CEC - database and the Material key for the Sandia database to a common - list of strings for cell type. - - Returns - ------- - cell_type: str - - """ - - _cell_type_dict = {'Multi-c-Si': 'multisi', - 'Mono-c-Si': 'monosi', - 'Thin Film': 'cigs', - 'a-Si/nc': 'asi', - 'CIS': 'cigs', - 'CIGS': 'cigs', - '1-a-Si': 'asi', - 'CdTe': 'cdte', - 'a-Si': 'asi', - '2-a-Si': None, - '3-a-Si': None, - 'HIT-Si': 'monosi', - 'mc-Si': 'multisi', - 'c-Si': 'multisi', - 'Si-Film': 'asi', - 'EFG mc-Si': 'multisi', - 'GaAs': None, - 'a-Si / mono-Si': 'monosi'} - - if 'Technology' in self.module_parameters.keys(): - # CEC module parameter set - cell_type = _cell_type_dict[self.module_parameters['Technology']] - elif 'Material' in self.module_parameters.keys(): - # Sandia module parameter set - cell_type = _cell_type_dict[self.module_parameters['Material']] - else: - cell_type = None - - return cell_type + spectral_correction = [] + for array in self._arrays: + if 'first_solar_spectral_coefficients' in \ + array.module_parameters.keys(): + coefficients = \ + array.module_parameters['first_solar_spectral_coefficients'] + module_type = None + else: + module_type = array._infer_cell_type() + coefficients = None + + spectral_correction.append( + atmosphere.first_solar_spectral_correction( + pw, airmass_absolute, + module_type, coefficients)) + return spectral_correction def singlediode(self, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, @@ -760,6 +818,8 @@ def adrinverter(self, v_dc, p_dc): """ return inverter.adr(v_dc, p_dc, self.inverter_parameters) + @singleton_as_scalar + @list_or_scalar('data') def scale_voltage_current_power(self, data): """ Scales the voltage, current, and power of the `data` DataFrame @@ -777,10 +837,15 @@ def scale_voltage_current_power(self, data): A scaled copy of the input data. """ - return scale_voltage_current_power(data, - voltage=self.modules_per_string, - current=self.strings_per_inverter) + return [ + scale_voltage_current_power(data, + voltage=array.modules_per_string, + current=array.strings) + for array, data in zip(self._arrays, data) + ] + @singleton_as_scalar + @list_or_scalar('g_poa_effective', 'temp_cell') def pvwatts_dc(self, g_poa_effective, temp_cell): """ Calcuates DC power according to the PVWatts model using @@ -789,12 +854,14 @@ def pvwatts_dc(self, g_poa_effective, temp_cell): See :py:func:`pvlib.pvsystem.pvwatts_dc` for details. """ - kwargs = _build_kwargs(['temp_ref'], self.module_parameters) - - return pvwatts_dc(g_poa_effective, temp_cell, - self.module_parameters['pdc0'], - self.module_parameters['gamma_pdc'], - **kwargs) + return [ + pvwatts_dc(g_poa_effective, temp_cell, + array.module_parameters['pdc0'], + array.module_parameters['gamma_pdc'], + **_build_kwargs(['temp_ref'], array.module_parameters)) + for array, g_poa_effective, temp_cell + in zip(self._arrays, g_poa_effective, temp_cell) + ] def pvwatts_losses(self): """ @@ -1072,6 +1139,49 @@ def _infer_temperature_model_params(self): else: return {} + def _infer_cell_type(self): + + """ + Examines module_parameters and maps the Technology key for the CEC + database and the Material key for the Sandia database to a common + list of strings for cell type. + + Returns + ------- + cell_type: str + + """ + + _cell_type_dict = {'Multi-c-Si': 'multisi', + 'Mono-c-Si': 'monosi', + 'Thin Film': 'cigs', + 'a-Si/nc': 'asi', + 'CIS': 'cigs', + 'CIGS': 'cigs', + '1-a-Si': 'asi', + 'CdTe': 'cdte', + 'a-Si': 'asi', + '2-a-Si': None, + '3-a-Si': None, + 'HIT-Si': 'monosi', + 'mc-Si': 'multisi', + 'c-Si': 'multisi', + 'Si-Film': 'asi', + 'EFG mc-Si': 'multisi', + 'GaAs': None, + 'a-Si / mono-Si': 'monosi'} + + if 'Technology' in self.module_parameters.keys(): + # CEC module parameter set + cell_type = _cell_type_dict[self.module_parameters['Technology']] + elif 'Material' in self.module_parameters.keys(): + # Sandia module parameter set + cell_type = _cell_type_dict[self.module_parameters['Material']] + else: + cell_type = None + + return cell_type + def get_aoi(self, solar_zenith, solar_azimuth): """ Get the angle of incidence on the array. From 4f8f1dd1146b3db1cd2c22fce1982eebd486d8df Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 13 Oct 2020 14:15:00 -0600 Subject: [PATCH 019/236] Restore PVSystem._infer_cell_type() This function is used in the test suite. It may not be needed otherwise, but to move forward without needing to rewrite the tests I'm adding it back to the PVSystem class as a wrapper around Array._infer_cell_type() --- pvlib/pvsystem.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index dec779da45..79da4a5028 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -254,6 +254,20 @@ def __repr__(self): return ('PVSystem:\n ' + '\n '.join( f'{attr}: {getattr(self, attr)}' for attr in attrs)) + @singleton_as_scalar + def _infer_cell_type(self): + + """ + Examines module_parameters and maps the Technology key for the CEC + database and the Material key for the Sandia database to a common + list of strings for cell type. + + Returns + ------- + cell_type: str + """ + return [array._infer_cell_type() for array in self._arrays] + @singleton_as_scalar def get_aoi(self, solar_zenith, solar_azimuth): """Get the angle of incidence on the system. From 02d1bfc90058e33277cf2dc6ca6222e8439a7b27 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 13 Oct 2020 15:02:46 -0600 Subject: [PATCH 020/236] Rename and document validation decorator Renames `@list_or_scalar` to `@validate_against_arrays` and add a docstring describing its behavior. --- pvlib/pvsystem.py | 54 ++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 79da4a5028..c8d6cf82fa 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -87,8 +87,25 @@ def f(*args, **kwargs): return f -def list_or_scalar(*list_params): +def validate_against_arrays(*list_params): + """Decorator that validates the value passed to each parameter in + `list_params` against the number of Arrays in the PVSystem. + + If the value passed for each parameter in `list_params` is not a list, + then it is transformed into a singleton list before validation. This + means existing code that assumes a PVSystem has only one array will + continue to function with no changes. + + Implicitly applies the `@singleton_as_scalar` decorator as well. + + Parameters + ---------- + list_params : iterable + names of the parameters that should be lists with the same number + of elements as there are Arrays in the PVSystem instance. + """ def list_or_scalar(func): + @singleton_as_scalar @functools.wraps(func) def f(ref, *args, **kwargs): sig = inspect.signature(func) @@ -330,8 +347,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra, airmass) for array in self._arrays] - @singleton_as_scalar - @list_or_scalar('aoi') + @validate_against_arrays('aoi') def get_iam(self, aoi, iam_model='physical'): """ Determine the incidence angle modifier using the method specified by @@ -361,8 +377,7 @@ def get_iam(self, aoi, iam_model='physical'): return [array.get_iam(aoi, iam_model) for array, aoi in zip(self._arrays, aoi)] - @singleton_as_scalar - @list_or_scalar('effective_irradiance', 'temp_cell') + @validate_against_arrays('effective_irradiance', 'temp_cell') def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`calcparams_desoto` function, the input @@ -401,8 +416,7 @@ def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): in zip(self._arrays, effective_irradiance, temp_cell) ] - @singleton_as_scalar - @list_or_scalar('effective_irradiance', 'temp_cell') + @validate_against_arrays('effective_irradiance', 'temp_cell') def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`calcparams_cec` function, the input @@ -441,8 +455,7 @@ def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): in zip(self._arrays, effective_irradiance, temp_cell) ] - @singleton_as_scalar - @list_or_scalar('effective_irradiance', 'temp_cell') + @validate_against_arrays('effective_irradiance', 'temp_cell') def calcparams_pvsyst(self, effective_irradiance, temp_cell): """ Use the :py:func:`calcparams_pvsyst` function, the input @@ -480,8 +493,7 @@ def calcparams_pvsyst(self, effective_irradiance, temp_cell): in zip(self._arrays, effective_irradiance, temp_cell) ] - @singleton_as_scalar - @list_or_scalar('effective_irradiance', 'temp_cell') + @validate_against_arrays('effective_irradiance', 'temp_cell') def sapm(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`sapm` function, the input parameters, @@ -507,8 +519,7 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): for array, effective_irradiance, temp_cell in zip(self._arrays, effective_irradiance, temp_cell)] - @singleton_as_scalar - @list_or_scalar('poa_global') + @validate_against_arrays('poa_global') def sapm_celltemp(self, poa_global, temp_air, wind_speed): """Uses :py:func:`temperature.sapm_cell` to calculate cell temperatures. @@ -572,8 +583,7 @@ def sapm_spectral_loss(self, airmass_absolute): return [sapm_spectral_loss(airmass_absolute, array.module_parameters) for array in self._arrays] - @singleton_as_scalar - @list_or_scalar('poa_direct', 'poa_diffuse', 'aoi') + @validate_against_arrays('poa_direct', 'poa_diffuse', 'aoi') def sapm_effective_irradiance(self, poa_direct, poa_diffuse, airmass_absolute, aoi, reference_irradiance=1000): @@ -609,8 +619,7 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, in zip(self._arrays, poa_direct, poa_diffuse, aoi) ] - @singleton_as_scalar - @list_or_scalar('poa_global') + @validate_against_arrays('poa_global') def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): """Uses :py:func:`temperature.pvsyst_cell` to calculate cell temperature. @@ -644,8 +653,7 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): for array, poa_global in zip(self._arrays, poa_global) ] - @singleton_as_scalar - @list_or_scalar('poa_global') + @validate_against_arrays('poa_global') def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): """ Use :py:func:`temperature.faiman` to calculate cell temperature. @@ -675,8 +683,7 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): for array, poa_global in zip(self._arrays, poa_global) ] - @singleton_as_scalar - @list_or_scalar('poa_global') + @validate_against_arrays('poa_global') def fuentes_celltemp(self, poa_global, temp_air, wind_speed): """ Use :py:func:`temperature.fuentes` to calculate cell temperature. @@ -833,7 +840,7 @@ def adrinverter(self, v_dc, p_dc): return inverter.adr(v_dc, p_dc, self.inverter_parameters) @singleton_as_scalar - @list_or_scalar('data') + @validate_against_arrays('data') def scale_voltage_current_power(self, data): """ Scales the voltage, current, and power of the `data` DataFrame @@ -858,8 +865,7 @@ def scale_voltage_current_power(self, data): for array, data in zip(self._arrays, data) ] - @singleton_as_scalar - @list_or_scalar('g_poa_effective', 'temp_cell') + @validate_against_arrays('g_poa_effective', 'temp_cell') def pvwatts_dc(self, g_poa_effective, temp_cell): """ Calcuates DC power according to the PVWatts model using From 7071cc7fc03f7e9cdb304f57e9a9fa70297237e7 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 13 Oct 2020 15:15:04 -0600 Subject: [PATCH 021/236] Rename decorator for returning singleton list as a single value Update docstring to make behavior more clear. --- pvlib/pvsystem.py | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index c8d6cf82fa..6c2f55b9bc 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -69,13 +69,10 @@ def _combine_localized_attributes(pvsystem=None, location=None, **kwargs): return new_kwargs -def singleton_as_scalar(func): - """Return a scalar if `func` returns a singleton list. - - Parameters - ---------- - func : funciton - A function that returns a list-like object. +def return_singleton_value(func): + """Decorator that returns the value contained when `func` + returns a singleton list or the list if it has more than one + element. """ @functools.wraps(func) def f(*args, **kwargs): @@ -105,7 +102,7 @@ def validate_against_arrays(*list_params): of elements as there are Arrays in the PVSystem instance. """ def list_or_scalar(func): - @singleton_as_scalar + @return_singleton_value @functools.wraps(func) def f(ref, *args, **kwargs): sig = inspect.signature(func) @@ -271,7 +268,7 @@ def __repr__(self): return ('PVSystem:\n ' + '\n '.join( f'{attr}: {getattr(self, attr)}' for attr in attrs)) - @singleton_as_scalar + @return_singleton_value def _infer_cell_type(self): """ @@ -285,7 +282,7 @@ def _infer_cell_type(self): """ return [array._infer_cell_type() for array in self._arrays] - @singleton_as_scalar + @return_singleton_value def get_aoi(self, solar_zenith, solar_azimuth): """Get the angle of incidence on the system. @@ -305,7 +302,7 @@ def get_aoi(self, solar_zenith, solar_azimuth): return [array.get_aoi(solar_zenith, solar_azimuth) for array in self._arrays] - @singleton_as_scalar + @return_singleton_value def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, model='haydavies', **kwargs): @@ -564,7 +561,7 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): for array, poa_global in zip(self._arrays, poa_global) ] - @singleton_as_scalar + @return_singleton_value def sapm_spectral_loss(self, airmass_absolute): """ Use the :py:func:`sapm_spectral_loss` function, the input @@ -730,7 +727,7 @@ def _build_kwargs_fuentes(array): for array, poa_global in zip(self._arrays, poa_global) ] - @singleton_as_scalar + @return_singleton_value def first_solar_spectral_loss(self, pw, airmass_absolute): """ @@ -839,7 +836,7 @@ def adrinverter(self, v_dc, p_dc): """ return inverter.adr(v_dc, p_dc, self.inverter_parameters) - @singleton_as_scalar + @return_singleton_value @validate_against_arrays('data') def scale_voltage_current_power(self, data): """ @@ -938,22 +935,22 @@ def localize(self, location=None, latitude=None, longitude=None, return LocalizedPVSystem(pvsystem=self, location=location) @property - @singleton_as_scalar + @return_singleton_value def module_parameters(self): return [array.module_parameters for array in self._arrays] @property - @singleton_as_scalar + @return_singleton_value def module(self): return [array.module for array in self._arrays] @property - @singleton_as_scalar + @return_singleton_value def module_type(self): return [array.module_type for array in self._arrays] @property - @singleton_as_scalar + @return_singleton_value def temperature_model_parameters(self): return [array.temperature_model_parameters for array in self._arrays] @@ -963,7 +960,7 @@ def temperature_model_parameters(self, value): array.temperature_model_parameters = value @property - @singleton_as_scalar + @return_singleton_value def surface_tilt(self): return [array.surface_tilt for array in self._arrays] @@ -973,7 +970,7 @@ def surface_tilt(self, value): array.surface_tilt = value @property - @singleton_as_scalar + @return_singleton_value def surface_azimuth(self): return [array.surface_azimuth for array in self._arrays] @@ -983,12 +980,12 @@ def surface_azimuth(self, value): array.surface_azimuth = value @property - @singleton_as_scalar + @return_singleton_value def albedo(self): return [array.albedo for array in self._arrays] @property - @singleton_as_scalar + @return_singleton_value def racking_model(self): return [array.racking_model for array in self._arrays] @@ -998,12 +995,12 @@ def racking_model(self, value): array.racking_model = value @property - @singleton_as_scalar + @return_singleton_value def modules_per_string(self): return [array.modules_per_string for array in self._arrays] @property - @singleton_as_scalar + @return_singleton_value def strings_per_inverter(self): return [array.strings for array in self._arrays] From 8de0eaef9c0c6df5e60c023d157635c6a0304388 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 13 Oct 2020 15:31:16 -0600 Subject: [PATCH 022/236] Fix indentation and formatting errors --- pvlib/pvsystem.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 6c2f55b9bc..cf4a71f13b 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -342,7 +342,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, return [array.get_irradiance(solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra, airmass) - for array in self._arrays] + for array in self._arrays] @validate_against_arrays('aoi') def get_iam(self, aoi, iam_model='physical'): @@ -410,7 +410,7 @@ def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): **build_kwargs(array.module_parameters) ) for array, effective_irradiance, temp_cell - in zip(self._arrays, effective_irradiance, temp_cell) + in zip(self._arrays, effective_irradiance, temp_cell) ] @validate_against_arrays('effective_irradiance', 'temp_cell') @@ -449,7 +449,7 @@ def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): **build_kwargs(array.module_parameters) ) for array, effective_irradiance, temp_cell - in zip(self._arrays, effective_irradiance, temp_cell) + in zip(self._arrays, effective_irradiance, temp_cell) ] @validate_against_arrays('effective_irradiance', 'temp_cell') @@ -717,7 +717,7 @@ def _build_kwargs_fuentes(array): temp_model_kwargs = _build_kwargs([ 'noct_installed', 'module_height', 'wind_height', 'emissivity', 'absorption', 'surface_tilt', 'module_width', 'module_length'], - array.temperature_model_parameters) + array.temperature_model_parameters) kwargs.update(temp_model_kwargs) return kwargs return [ @@ -763,7 +763,9 @@ def first_solar_spectral_loss(self, pw, airmass_absolute): if 'first_solar_spectral_coefficients' in \ array.module_parameters.keys(): coefficients = \ - array.module_parameters['first_solar_spectral_coefficients'] + array.module_parameters[ + 'first_solar_spectral_coefficients' + ] module_type = None else: module_type = array._infer_cell_type() From 0c7580792e8a0b3330a79adaab0b65a980593cc8 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 13 Oct 2020 15:37:07 -0600 Subject: [PATCH 023/236] Use nested def instead of assigning a lambda Instead of assigning a lambda expression use an internal def to define function for building the pvsyst_celltemp kwargs. --- pvlib/pvsystem.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index cf4a71f13b..fdac0376ce 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -638,15 +638,14 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): ------- numeric, values in degrees C. """ - build_kwargs = lambda array: { - **_build_kwargs(['eta_m', 'alpha_absorption'], - array.module_parameters), - **_build_kwargs(['u_c', 'u_v'], - array.temperature_model_parameters) - } + def build_celltemp_kwargs(array): + return {**_build_kwargs(['eta_m', 'alpha_absorption'], + array.module_parameters), + **_build_kwargs(['u_c', 'u_v'], + array.temperature_model_parameters)} return [ temperature.pvsyst_cell(poa_global, temp_air, wind_speed, - **build_kwargs(array)) + **build_celltemp_kwargs(array)) for array, poa_global in zip(self._arrays, poa_global) ] From cdb346310b30af7272edd7532d78cc64dd9cd87c Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 13 Oct 2020 15:42:47 -0600 Subject: [PATCH 024/236] Fix indentation in ModelChain._prepare_temperatures --- pvlib/modelchain.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 6df7014bda..d5b6864672 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1287,9 +1287,9 @@ def _prepare_temperature(self, data=None): # use SAPM cell temperature model only self.results.cell_temperature = \ pvlib.temperature.sapm_cell_from_module( - module_temperature=data['module_temperature'], - poa_global=self.results.total_irrad['poa_global'], - deltaT=self.system.temperature_model_parameters['deltaT']) + module_temperature=data['module_temperature'], + poa_global=self.results.total_irrad['poa_global'], + deltaT=self.system.temperature_model_parameters['deltaT']) return self # Calculate cell temperature from weather data. Cell temperature models From 90dba53deddba93bc006a506d8bc1836d8c4b10c Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 10:44:00 -0600 Subject: [PATCH 025/236] Add test for Array.__repr__() --- pvlib/tests/test_pvsystem.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index fe7bb9da15..c8eaef6f8a 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1177,6 +1177,29 @@ def test_PVSystem_localize___repr__(): assert localized_system.__repr__() == expected +def test_Array___repr__(): + array = pvsystem.Array( + surface_tilt=10, surface_azimuth=100, + albedo=0.15, module_type='glass_glass', + temperature_model_parameters={'a': -3.56}, + racking_model='close_mount', + module_parameters={'foo': 'bar'}, + modules_per_string=100, + strings=10, module='baz' + ) + expected = """Array: + surface_tilt: 10 + surface_azimuth: 100 + module: baz + albedo: 0.15 + racking_model: close_mount + module_type: glass_glass + temperature_model_parameters: {'a': -3.56} + strings: 10 + modules_per_string: 100""" + assert array.__repr__() == expected + + # we could retest each of the models tested above # when they are attached to LocalizedPVSystem, but # that's probably not necessary at this point. From 202fc2262c41d957783065a303634a095a8b4d26 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 11:21:41 -0600 Subject: [PATCH 026/236] Pass list of Arrays to PVSystem.__init__ To construct a PVSystem instance with multiple arrays users can pass a list of Array instances. --- pvlib/pvsystem.py | 38 ++++++++++++++++++++++++------------ pvlib/tests/test_pvsystem.py | 11 +++++++++++ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index fdac0376ce..474fb0cb64 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -151,6 +151,11 @@ class PVSystem: Parameters ---------- + arrays : list of Array, optional + List of arrays that are part of the system. If not specified + a single array is created from the other parameters (`surface_tilt`, + `surface_azimuth` etc.) + surface_tilt: float or array-like, default 0 Surface tilt angles in decimal degrees. The tilt angle is defined as degrees from horizontal @@ -218,6 +223,7 @@ class PVSystem: """ def __init__(self, + arrays=None, surface_tilt=0, surface_azimuth=180, albedo=None, surface_type=None, module=None, module_type=None, @@ -228,19 +234,25 @@ def __init__(self, racking_model=None, losses_parameters=None, name=None, **kwargs): - self._arrays = [Array( - surface_tilt, - surface_azimuth, - albedo, - surface_type, - module, - module_type, - module_parameters, - temperature_model_parameters, - modules_per_string, - strings_per_inverter, - racking_model - )] + if arrays is None: + self._arrays = [Array( + surface_tilt, + surface_azimuth, + albedo, + surface_type, + module, + module_type, + module_parameters, + temperature_model_parameters, + modules_per_string, + strings_per_inverter, + racking_model + )] + elif not isinstance(arrays, list): + raise ValueError("expected `arrays` to be a list, got " + f"{type(arrays).__name__}") + else: + self._arrays = arrays self.inverter = inverter if inverter_parameters is None: diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index c8eaef6f8a..898f6dee91 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1075,6 +1075,17 @@ def test_PVSystem_creation(): pv_system.inverter_parameters['Paco'] = 1 +def test_PVSystem_multiple_array_creation(): + array_one = pvsystem.Array(surface_tilt=32) + array_two = pvsystem.Array(surface_tilt=15, module_parameters={'pdc0': 1}) + pv_system = pvsystem.PVSystem(arrays=[array_one, array_two]) + assert pv_system.surface_tilt == [32, 15] + assert pv_system.surface_azimuth == [180, 180] + assert pv_system.module_parameters == [{}, {'pdc0': 1}] + with pytest.raises(ValueError): + pvsystem.PVSystem(arrays=array_one) + + def test_PVSystem_get_aoi(): system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) aoi = system.get_aoi(30, 225) From 40ee652820784fc518910e4a41da6113ccbcb47e Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 13:00:03 -0600 Subject: [PATCH 027/236] Return tuple instead of list for multi-array PVSystem Change the representation of PVSystem._array to a tuple as well. --- pvlib/pvsystem.py | 113 ++++++++++++++++++----------------- pvlib/tests/test_pvsystem.py | 8 +-- 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 474fb0cb64..c4899eec88 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -235,7 +235,7 @@ def __init__(self, **kwargs): if arrays is None: - self._arrays = [Array( + self._arrays = (Array( surface_tilt, surface_azimuth, albedo, @@ -247,12 +247,9 @@ def __init__(self, modules_per_string, strings_per_inverter, racking_model - )] - elif not isinstance(arrays, list): - raise ValueError("expected `arrays` to be a list, got " - f"{type(arrays).__name__}") + ),) else: - self._arrays = arrays + self._arrays = tuple(arrays) self.inverter = inverter if inverter_parameters is None: @@ -292,7 +289,7 @@ def _infer_cell_type(self): ------- cell_type: str """ - return [array._infer_cell_type() for array in self._arrays] + return tuple(array._infer_cell_type() for array in self._arrays) @return_singleton_value def get_aoi(self, solar_zenith, solar_azimuth): @@ -311,8 +308,8 @@ def get_aoi(self, solar_zenith, solar_azimuth): The angle of incidence """ - return [array.get_aoi(solar_zenith, solar_azimuth) - for array in self._arrays] + return tuple(array.get_aoi(solar_zenith, solar_azimuth) + for array in self._arrays) @return_singleton_value def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, @@ -351,10 +348,10 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, poa_irradiance : DataFrame Column names are: ``total, beam, sky, ground``. """ - return [array.get_irradiance(solar_zenith, solar_azimuth, - dni, ghi, dhi, - dni_extra, airmass) - for array in self._arrays] + return tuple(array.get_irradiance(solar_zenith, solar_azimuth, + dni, ghi, dhi, + dni_extra, airmass) + for array in self._arrays) @validate_against_arrays('aoi') def get_iam(self, aoi, iam_model='physical'): @@ -383,8 +380,8 @@ def get_iam(self, aoi, iam_model='physical'): ------ ValueError if `iam_model` is not a valid model name. """ - return [array.get_iam(aoi, iam_model) - for array, aoi in zip(self._arrays, aoi)] + return tuple(array.get_iam(aoi, iam_model) + for array, aoi in zip(self._arrays, aoi)) @validate_against_arrays('effective_irradiance', 'temp_cell') def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): @@ -416,14 +413,14 @@ def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): 'irrad_ref', 'temp_ref'] ) - return [ + return tuple( calcparams_desoto( effective_irradiance, temp_cell, **build_kwargs(array.module_parameters) ) for array, effective_irradiance, temp_cell in zip(self._arrays, effective_irradiance, temp_cell) - ] + ) @validate_against_arrays('effective_irradiance', 'temp_cell') def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): @@ -455,14 +452,14 @@ def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): 'irrad_ref', 'temp_ref'] ) - return [ + return tuple( calcparams_cec( effective_irradiance, temp_cell, **build_kwargs(array.module_parameters) ) for array, effective_irradiance, temp_cell in zip(self._arrays, effective_irradiance, temp_cell) - ] + ) @validate_against_arrays('effective_irradiance', 'temp_cell') def calcparams_pvsyst(self, effective_irradiance, temp_cell): @@ -493,14 +490,14 @@ def calcparams_pvsyst(self, effective_irradiance, temp_cell): 'cells_in_series'] ) - return [ + return tuple( calcparams_pvsyst( effective_irradiance, temp_cell, **build_kwargs(array.module_parameters) ) for array, effective_irradiance, temp_cell in zip(self._arrays, effective_irradiance, temp_cell) - ] + ) @validate_against_arrays('effective_irradiance', 'temp_cell') def sapm(self, effective_irradiance, temp_cell, **kwargs): @@ -524,9 +521,11 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): ------- See pvsystem.sapm for details """ - return [sapm(effective_irradiance, temp_cell, array.module_parameters) - for array, effective_irradiance, temp_cell - in zip(self._arrays, effective_irradiance, temp_cell)] + return tuple( + sapm(effective_irradiance, temp_cell, array.module_parameters) + for array, effective_irradiance, temp_cell + in zip(self._arrays, effective_irradiance, temp_cell) + ) @validate_against_arrays('poa_global') def sapm_celltemp(self, poa_global, temp_air, wind_speed): @@ -565,13 +564,13 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): array.temperature_model_parameters = params build_kwargs = functools.partial(_build_kwargs, ['a', 'b', 'deltaT']) - return [ + return tuple( temperature.sapm_cell( poa_global, temp_air, wind_speed, **build_kwargs(array.temperature_model_parameters) ) for array, poa_global in zip(self._arrays, poa_global) - ] + ) @return_singleton_value def sapm_spectral_loss(self, airmass_absolute): @@ -589,8 +588,10 @@ def sapm_spectral_loss(self, airmass_absolute): F1 : numeric The SAPM spectral loss coefficient. """ - return [sapm_spectral_loss(airmass_absolute, array.module_parameters) - for array in self._arrays] + return tuple( + sapm_spectral_loss(airmass_absolute, array.module_parameters) + for array in self._arrays + ) @validate_against_arrays('poa_direct', 'poa_diffuse', 'aoi') def sapm_effective_irradiance(self, poa_direct, poa_diffuse, @@ -620,13 +621,13 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, effective_irradiance : numeric The SAPM effective irradiance. [W/m2] """ - return [ + return tuple( sapm_effective_irradiance( poa_direct, poa_diffuse, airmass_absolute, aoi, array.module_parameters) for array, poa_direct, poa_diffuse, aoi in zip(self._arrays, poa_direct, poa_diffuse, aoi) - ] + ) @validate_against_arrays('poa_global') def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): @@ -655,11 +656,11 @@ def build_celltemp_kwargs(array): array.module_parameters), **_build_kwargs(['u_c', 'u_v'], array.temperature_model_parameters)} - return [ + return tuple( temperature.pvsyst_cell(poa_global, temp_air, wind_speed, **build_celltemp_kwargs(array)) for array, poa_global in zip(self._arrays, poa_global) - ] + ) @validate_against_arrays('poa_global') def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): @@ -683,13 +684,13 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): ------- numeric, values in degrees C. """ - return [ + return tuple( temperature.faiman( poa_global, temp_air, wind_speed, **_build_kwargs( ['u0', 'u1'], array.temperature_model_parameters)) for array, poa_global in zip(self._arrays, poa_global) - ] + ) @validate_against_arrays('poa_global') def fuentes_celltemp(self, poa_global, temp_air, wind_speed): @@ -731,12 +732,12 @@ def _build_kwargs_fuentes(array): array.temperature_model_parameters) kwargs.update(temp_model_kwargs) return kwargs - return [ + return tuple( temperature.fuentes( poa_global, temp_air, wind_speed, **_build_kwargs_fuentes(array)) for array, poa_global in zip(self._arrays, poa_global) - ] + ) @return_singleton_value def first_solar_spectral_loss(self, pw, airmass_absolute): @@ -769,8 +770,7 @@ def first_solar_spectral_loss(self, pw, airmass_absolute): electrical current. """ - spectral_correction = [] - for array in self._arrays: + def _spectral_correction(array): if 'first_solar_spectral_coefficients' in \ array.module_parameters.keys(): coefficients = \ @@ -782,11 +782,11 @@ def first_solar_spectral_loss(self, pw, airmass_absolute): module_type = array._infer_cell_type() coefficients = None - spectral_correction.append( - atmosphere.first_solar_spectral_correction( + return atmosphere.first_solar_spectral_correction( pw, airmass_absolute, - module_type, coefficients)) - return spectral_correction + module_type, coefficients + ) + return tuple(_spectral_correction(array) for array in self._arrays) def singlediode(self, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, @@ -868,12 +868,12 @@ def scale_voltage_current_power(self, data): A scaled copy of the input data. """ - return [ + return tuple( scale_voltage_current_power(data, voltage=array.modules_per_string, current=array.strings) for array, data in zip(self._arrays, data) - ] + ) @validate_against_arrays('g_poa_effective', 'temp_cell') def pvwatts_dc(self, g_poa_effective, temp_cell): @@ -884,14 +884,14 @@ def pvwatts_dc(self, g_poa_effective, temp_cell): See :py:func:`pvlib.pvsystem.pvwatts_dc` for details. """ - return [ + return tuple( pvwatts_dc(g_poa_effective, temp_cell, array.module_parameters['pdc0'], array.module_parameters['gamma_pdc'], **_build_kwargs(['temp_ref'], array.module_parameters)) for array, g_poa_effective, temp_cell in zip(self._arrays, g_poa_effective, temp_cell) - ] + ) def pvwatts_losses(self): """ @@ -950,22 +950,23 @@ def localize(self, location=None, latitude=None, longitude=None, @property @return_singleton_value def module_parameters(self): - return [array.module_parameters for array in self._arrays] + return tuple(array.module_parameters for array in self._arrays) @property @return_singleton_value def module(self): - return [array.module for array in self._arrays] + return tuple(array.module for array in self._arrays) @property @return_singleton_value def module_type(self): - return [array.module_type for array in self._arrays] + return tuple(array.module_type for array in self._arrays) @property @return_singleton_value def temperature_model_parameters(self): - return [array.temperature_model_parameters for array in self._arrays] + return tuple(array.temperature_model_parameters + for array in self._arrays) @temperature_model_parameters.setter def temperature_model_parameters(self, value): @@ -975,7 +976,7 @@ def temperature_model_parameters(self, value): @property @return_singleton_value def surface_tilt(self): - return [array.surface_tilt for array in self._arrays] + return tuple(array.surface_tilt for array in self._arrays) @surface_tilt.setter def surface_tilt(self, value): @@ -985,7 +986,7 @@ def surface_tilt(self, value): @property @return_singleton_value def surface_azimuth(self): - return [array.surface_azimuth for array in self._arrays] + return tuple(array.surface_azimuth for array in self._arrays) @surface_azimuth.setter def surface_azimuth(self, value): @@ -995,12 +996,12 @@ def surface_azimuth(self, value): @property @return_singleton_value def albedo(self): - return [array.albedo for array in self._arrays] + return tuple(array.albedo for array in self._arrays) @property @return_singleton_value def racking_model(self): - return [array.racking_model for array in self._arrays] + return tuple(array.racking_model for array in self._arrays) @racking_model.setter def racking_model(self, value): @@ -1010,12 +1011,12 @@ def racking_model(self, value): @property @return_singleton_value def modules_per_string(self): - return [array.modules_per_string for array in self._arrays] + return tuple(array.modules_per_string for array in self._arrays) @property @return_singleton_value def strings_per_inverter(self): - return [array.strings for array in self._arrays] + return tuple(array.strings for array in self._arrays) @deprecated('0.8', alternative='PVSystem, Location, and ModelChain', diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 898f6dee91..04fc469e95 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1079,10 +1079,10 @@ def test_PVSystem_multiple_array_creation(): array_one = pvsystem.Array(surface_tilt=32) array_two = pvsystem.Array(surface_tilt=15, module_parameters={'pdc0': 1}) pv_system = pvsystem.PVSystem(arrays=[array_one, array_two]) - assert pv_system.surface_tilt == [32, 15] - assert pv_system.surface_azimuth == [180, 180] - assert pv_system.module_parameters == [{}, {'pdc0': 1}] - with pytest.raises(ValueError): + assert pv_system.surface_tilt == (32, 15) + assert pv_system.surface_azimuth == (180, 180) + assert pv_system.module_parameters == ({}, {'pdc0': 1}) + with pytest.raises(TypeError): pvsystem.PVSystem(arrays=array_one) From be48170a61e7117c47e499d5a97ea0e7e208475f Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 13:04:44 -0600 Subject: [PATCH 028/236] Fix indentation --- pvlib/pvsystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index c4899eec88..2ff340dd37 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -783,8 +783,8 @@ def _spectral_correction(array): coefficients = None return atmosphere.first_solar_spectral_correction( - pw, airmass_absolute, - module_type, coefficients + pw, airmass_absolute, + module_type, coefficients ) return tuple(_spectral_correction(array) for array in self._arrays) From c3796dbb28df71dadb460b990d31f86ff8525365 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 16 Oct 2020 13:05:53 -0600 Subject: [PATCH 029/236] deprecate old ModelChain attributes --- pvlib/modelchain.py | 32 ++++++++++++++++++++++++++++++++ pvlib/tests/test_modelchain.py | 10 ++++++++++ 2 files changed, 42 insertions(+) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index d5b6864672..48f1ec9bc7 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -397,6 +397,38 @@ def __init__(self, system, location, 'removed in v0.9', pvlibDeprecationWarning ) + def __getattr__(self, key): + # here to deprecate old attributes + deprecated_attrs = ['solar_position', 'airmass', 'total_irrad', + 'aoi', 'aoi_modifier', 'spectral_modifier', + 'cell_temperature', 'effective_irradiance', + 'dc', 'diode_params'] + if key in deprecated_attrs: + msg = f'ModelChain.{key} is deprecated and will' \ + f' be removed in v1.0. Use' \ + f' ModelChain.results.{key} instead' + warnings.warn(msg, pvlibDeprecationWarning) + return getattr(self.results, key) + else: + try: + return self.__dict__[key] + except(KeyError): + raise AttributeError + + def __setattr__(self, key, value): + # here to deprecate old attributes + deprecated_attrs = ['solar_position', 'airmass', 'total_irrad', + 'aoi', 'aoi_modifier', 'spectral_modifier', + 'cell_temperature', 'effective_irradiance', + 'dc', 'diode_params'] + if key in deprecated_attrs: + msg = f'ModelChain.{key} is deprecated from v0.9. Use' \ + f' ModelChain.results.{key} instead' + warnings.warn(msg, pvlibDeprecationWarning) + setattr(self.results, key, value) + else: + object.__setattr__(self, key, value) + @classmethod def with_pvwatts(cls, system, location, orientation_strategy=None, diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index c83d798b57..fea70b8c45 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -814,6 +814,16 @@ def test_ModelChain_kwargs_deprecated_09(sapm_dc_snl_ac_system, location): ModelChain(sapm_dc_snl_ac_system, location, arbitrary_kwarg='value') +@fail_on_pvlib_version('1.0') +def test_ModelChain_attributes_deprecated_10(sapm_dc_snl_ac_system, location): + match = 'Use ModelChain.results' + mc = ModelChain(sapm_dc_snl_ac_system, location) + with pytest.warns(pvlibDeprecationWarning, match=match): + mc.aoi + with pytest.warns(pvlibDeprecationWarning, match=match): + mc.aoi = 5 + + def test_basic_chain_required(sam_data, cec_inverter_parameters, sapm_temperature_cs5p_220m): times = pd.date_range(start='20160101 1200-0700', From 03eac94e745c43c2bec5d3621b67926b229308d3 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 16 Oct 2020 13:33:23 -0600 Subject: [PATCH 030/236] remove solar_position = None from ModelChain.__init__ --- pvlib/modelchain.py | 1 - pvlib/tests/test_modelchain.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 48f1ec9bc7..969fa10cd7 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -387,7 +387,6 @@ def __init__(self, system, location, self.weather = None self.times = None - self.solar_position = None self.results = ModelChainResult() diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index fea70b8c45..b768a64707 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -823,7 +823,7 @@ def test_ModelChain_attributes_deprecated_10(sapm_dc_snl_ac_system, location): with pytest.warns(pvlibDeprecationWarning, match=match): mc.aoi = 5 - + def test_basic_chain_required(sam_data, cec_inverter_parameters, sapm_temperature_cs5p_220m): times = pd.date_range(start='20160101 1200-0700', From 3e82f83a7411daf1c40171e4f700d3bd83455c95 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 13:48:12 -0600 Subject: [PATCH 031/236] Require tuple input in list_or_scalar decorator After change to returning tuple as lists we need to update the decortor to check for the correct type. It is tempting to test for any Iterable here; however, since some of the parameters are tuples of Series or DataFrame (which are themselves iterabel), we need to be careful. I believe the best option is to be very restrictive and expect input to be a tuple. --- pvlib/pvsystem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 2ff340dd37..0a610fdb4f 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -109,8 +109,9 @@ def f(ref, *args, **kwargs): bindings = sig.bind(*tuple([ref, *args]), **kwargs) for param in bindings.arguments.keys(): if param in list_params: - if not isinstance(bindings.arguments[param], list): - bindings.arguments[param] = [bindings.arguments[param]] + value = bindings.arguments[param] + if not isinstance(value, tuple): + bindings.arguments[param] = (value,) if len(bindings.arguments[param]) != len(ref._arrays): raise ValueError( f"Length mismatch for parameter {param}" From 4ed94242f96c6e9470f00c8cad46178e228de980 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 13:57:09 -0600 Subject: [PATCH 032/236] Document meaning of `arrays` parameter to PVSystem.__init__ --- pvlib/pvsystem.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 0a610fdb4f..6a8c51e94c 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -152,10 +152,22 @@ class PVSystem: Parameters ---------- - arrays : list of Array, optional + arrays : iterable of Array, optional List of arrays that are part of the system. If not specified - a single array is created from the other parameters (`surface_tilt`, - `surface_azimuth` etc.) + a single array is created from the other parameters (e.g. + `surface_tilt`, `surface_azimuth`). If `arrays` is specified + the following parameters are ignored: + + - `surface_tilt` + - `surface_azimuth` + - `albedo` + - `surface_type` + - `module` + - `module_type` + - `module_parameters` + - `temperature_model_parameters` + - `modules_per_string` + - `strings_per_inverter` surface_tilt: float or array-like, default 0 Surface tilt angles in decimal degrees. From 389d1b207384b6bc611927f140eabaede8a45386 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 14:22:31 -0600 Subject: [PATCH 033/236] Test multi-array PVSystem.scale_voltage_current_power() --- pvlib/tests/test_pvsystem.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 04fc469e95..363036a213 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -7,6 +7,7 @@ import pytest from conftest import assert_series_equal, assert_frame_equal from numpy.testing import assert_allclose +import unittest.mock as mock from pvlib import inverter, pvsystem from pvlib import atmosphere @@ -1055,6 +1056,26 @@ def test_PVSystem_scale_voltage_current_power(mocker): m.assert_called_once_with(data, voltage=2, current=3) +def test_PVSystem_multi_scale_voltage_current_power(mocker): + data = (1, 2) + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(modules_per_string=2, strings=3), + pvsystem.Array(modules_per_string=3, strings=5)] + ) + m = mocker.patch( + 'pvlib.pvsystem.scale_voltage_current_power', autospec=True + ) + system.scale_voltage_current_power(data) + m.assert_has_calls( + [mock.call(1, voltage=2, current=3), + mock.call(2, voltage=3, current=5)], + any_order=True + ) + with pytest.raises(ValueError, + match="Length mismatch for parameter data"): + system.scale_voltage_current_power(None) + + def test_PVSystem_snlinverter(cec_inverter_parameters): system = pvsystem.PVSystem( inverter=cec_inverter_parameters['Name'], From 87a600019c2432efba6e8c51e3ab6d3a997f2658 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 16 Oct 2020 14:28:58 -0600 Subject: [PATCH 034/236] add modelchain test on pvsystem with 2 arrays --- pvlib/tests/test_modelchain.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index b768a64707..83c381c63d 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -197,6 +197,27 @@ def total_irrad(weather): 'poa_diffuse': [300., 200.]}, index=weather.index) +@pytest.fixture(scope='function') +def sapm_dc_snl_ac_system_Array(sapm_module_params, cec_inverter_parameters, + sapm_temperature_cs5p_220m): + module = 'Canadian_Solar_CS5P_220M___2009_' + module_parameters = sapm_module_params.copy() + temp_model_params = sapm_temperature_cs5p_220m.copy() + array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=180, + albedo=0.2, module=module, + module_parameters=module_parameters, + temperature_mode_parameters = temp_model_params, + modules_per_string=1, + strings_per_inverter=1) + array_two = pvsystem.Array(surface_tilt=15, surface_azimuth=180, + albedo=0.2, module=module, + module_parameters=module_parameters, + temperature_mode_parameters = temp_model_params, + modules_per_string=1, + strings_per_inverter=1) + return PVSystem(arrays=[array_one, array_two]) + + def test_ModelChain_creation(sapm_dc_snl_ac_system, location): ModelChain(sapm_dc_snl_ac_system, location) @@ -791,6 +812,14 @@ def test_bad_get_orientation(): modelchain.get_orientation('bad value') +# tests for PVSystem with multiple Array +def test_with_sapm_pvsystem_arrasy(sapm_dc_snl_ac_system, location, weather): + mc = ModelChain.with_sapm(sapm_dc_snl_ac_system, location) + assert mc.dc_model == mc.sapm + mc.run_model(weather) + assert mc.results + + @fail_on_pvlib_version('0.9') @pytest.mark.parametrize('ac_model', ['snlinverter', 'adrinverter']) def test_deprecated_09(sapm_dc_snl_ac_system, cec_dc_adr_ac_system, From 1d21af43839b249237578603234a5f8734a07de3 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 14:30:59 -0600 Subject: [PATCH 035/236] Test multi-array PVSystem.get_aoi() --- pvlib/tests/test_pvsystem.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 363036a213..5f5a8facef 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1113,6 +1113,17 @@ def test_PVSystem_get_aoi(): assert np.round(aoi, 4) == 42.7408 +def test_PVSystem_multiple_array_get_aoi(): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(surface_tilt=15, surface_azimuth=135), + pvsystem.Array(surface_tilt=32, surface_azimuth=135)] + ) + aoi_one, aoi_two = system.get_aoi(30, 225) + assert np.round(aoi_two, 4) == 42.7408 + assert aoi_two != aoi_one + assert aoi_one > 0 + + def test_PVSystem_get_irradiance(): system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) times = pd.date_range(start='20160101 1200-0700', From 3fa6ba38ce64548b6ba0ec7281c9c9e6f1d804d4 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 14:46:39 -0600 Subject: [PATCH 036/236] Test multi-array PVSystem.get_irradiance() Just test that PVSystem.get_irradiance returns a tuple that has the irradiance for each array, test_PVSystem_get_irradiance() suffices to test the the value returned by Array.get_irradiance() is correct. --- pvlib/tests/test_pvsystem.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 5f5a8facef..46d51b0854 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1151,6 +1151,39 @@ def test_PVSystem_get_irradiance(): assert_frame_equal(irradiance, expected, check_less_precise=2) +def test_PVSystem_multi_array_get_irradiance(): + array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=135) + array_two = pvsystem.Array(surface_tilt=5, surface_azimuth=150) + system = pvsystem.PVSystem(arrays=[array_one, array_two]) + location = Location(latitude=32, longitude=-111) + times = pd.date_range(start='20160101 1200-0700', + end='20160101 1800-0700', freq='6H') + solar_position = location.get_solarposition(times) + irrads = pd.DataFrame({'dni':[900,0], 'ghi':[600,0], 'dhi':[100,0]}, + index=times) + array_one_expected = array_one.get_irradiance( + solar_position['apparent_zenith'], + solar_position['azimuth'], + irrads['dni'], irrads['ghi'], irrads['dhi'] + ) + array_two_expected = array_two.get_irradiance( + solar_position['apparent_zenith'], + solar_position['azimuth'], + irrads['dni'], irrads['ghi'], irrads['dhi'] + ) + array_one_irrad, array_two_irrad = system.get_irradiance( + solar_position['apparent_zenith'], + solar_position['azimuth'], + irrads['dni'], irrads['ghi'], irrads['dhi'] + ) + assert_frame_equal( + array_one_irrad, array_one_expected, check_less_precise=2 + ) + assert_frame_equal( + array_two_irrad, array_two_expected, check_less_precise=2 + ) + + @fail_on_pvlib_version('0.9') def test_PVSystem_localize_with_location(): system = pvsystem.PVSystem(module='blah', inverter='blarg') From c60c38246facfd02ba26f5139287ede8b7964bf0 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 15:17:39 -0600 Subject: [PATCH 037/236] Test multi-array PVSystem.pvwatts_dc() --- pvlib/tests/test_pvsystem.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 46d51b0854..dd5069d638 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1404,6 +1404,34 @@ def test_PVSystem_pvwatts_dc_kwargs(mocker): assert_allclose(expected, out, atol=10) +def test_PVSystem_multiple_array_pvwatts_dc(): + array_one_module_parameters = { + 'pdc0': 100, 'gamma_pdc': -0.003, 'temp_ref': 20 + } + array_one = pvsystem.Array( + module_parameters=array_one_module_parameters + ) + array_two_module_parameters = { + 'pdc0': 150, 'gamma_pdc': -0.002, 'temp_ref': 25 + } + array_two = pvsystem.Array( + module_parameters=array_two_module_parameters + ) + system = pvsystem.PVSystem(arrays=[array_one, array_two]) + irrad_one = 900 + irrad_two = 500 + temp_cell_one = 30 + temp_cell_two = 20 + expected_one = pvsystem.pvwatts_dc(irrad_one, temp_cell_one, + **array_one_module_parameters) + expected_two = pvsystem.pvwatts_dc(irrad_two, temp_cell_two, + **array_two_module_parameters) + dc_one, dc_two = system.pvwatts_dc((irrad_one, irrad_two), + (temp_cell_one, temp_cell_two)) + assert dc_one == expected_one + assert dc_two == expected_two + + def test_PVSystem_pvwatts_losses(mocker): mocker.spy(pvsystem, 'pvwatts_losses') system = make_pvwatts_system_defaults() From dfa07ca879bc56059e91b8de802c5e6f04221a08 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 15:29:08 -0600 Subject: [PATCH 038/236] Test input length validation on PVSystem.pvwatts_dc() --- pvlib/tests/test_pvsystem.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index dd5069d638..fb73c64c7d 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1432,6 +1432,29 @@ def test_PVSystem_multiple_array_pvwatts_dc(): assert dc_two == expected_two +def test_PVSystem_multiple_array_pvwatts_dc_value_error(): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(), pvsystem.Array(), pvsystem.Array()] + ) + poa_error_msg = 'Length mismatch for parameter g_poa_effective' + with pytest.raises(ValueError, match=poa_error_msg): + system.pvwatts_dc(10, (1, 1, 1)) + with pytest.raises(ValueError, match=poa_error_msg): + system.pvwatts_dc((10, 10), (1, 1, 1)) + with pytest.raises(ValueError, match=poa_error_msg): + system.pvwatts_dc((10, 10, 10, 10), (1, 1, 1)) + temp_cell_error_msg = 'Length mismatch for parameter temp_cell' + with pytest.raises(ValueError, match=temp_cell_error_msg): + system.pvwatts_dc((1, 1, 1), 1) + with pytest.raises(ValueError, match=temp_cell_error_msg): + system.pvwatts_dc((1, 1, 1), (1,)) + with pytest.raises(ValueError, match='Length mismatch for parameter .*'): + system.pvwatts_dc((1,), 1) + with pytest.raises(ValueError, match='Length mismatch for parameter .*'): + system.pvwatts_dc((1, 1, 1, 1), (1, 1)) + with pytest.raises(ValueError, match='Length mismatch for parameter .*'): + system.pvwatts_dc(2, 3) + def test_PVSystem_pvwatts_losses(mocker): mocker.spy(pvsystem, 'pvwatts_losses') system = make_pvwatts_system_defaults() From 26b7efb6b1fa9edc3ba5392ca683871a30602d1d Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 15:41:42 -0600 Subject: [PATCH 039/236] Cover non-tuple iterable edge case Per-Array input must be passed as a tuple, arbitrary iterable will raise a ValueError even if the length is correct. --- pvlib/tests/test_pvsystem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index fb73c64c7d..7453f0d4e7 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1454,6 +1454,9 @@ def test_PVSystem_multiple_array_pvwatts_dc_value_error(): system.pvwatts_dc((1, 1, 1, 1), (1, 1)) with pytest.raises(ValueError, match='Length mismatch for parameter .*'): system.pvwatts_dc(2, 3) + with pytest.raises(ValueError, match=temp_cell_error_msg): + # ValueError is raised for non-tuple iterable with correct length + system.pvwatts_dc((1, 1, 1), pd.Series([1, 2, 3])) def test_PVSystem_pvwatts_losses(mocker): mocker.spy(pvsystem, 'pvwatts_losses') From accef415c73310c5a6757383b583307c50212f8d Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 15:50:06 -0600 Subject: [PATCH 040/236] Test multi-array PVSystem.get_iam() --- pvlib/tests/test_pvsystem.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 7453f0d4e7..da6cb00951 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -33,6 +33,17 @@ def test_PVSystem_get_iam(mocker, iam_model, model_params): assert iam < 1. +def test_PVSystem_multi_array_get_iam(): + model_params = {'b': 0.05} + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(module_parameters=model_params), + pvsystem.Array(module_parameters=model_params)] + ) + iam = system.get_iam((1, 5), iam_model='ashrae') + assert len(iam) == 2 + assert iam[0] != iam[1] + + def test_PVSystem_get_iam_sapm(sapm_module_params, mocker): system = pvsystem.PVSystem(module_parameters=sapm_module_params) mocker.spy(_iam, 'sapm') From adf366b2381c34b01f279663d07512b53ae008f4 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 16 Oct 2020 15:51:43 -0600 Subject: [PATCH 041/236] Fix whitespace in test_pvsystem.py --- pvlib/tests/test_pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index da6cb00951..cce5b581a8 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1170,7 +1170,7 @@ def test_PVSystem_multi_array_get_irradiance(): times = pd.date_range(start='20160101 1200-0700', end='20160101 1800-0700', freq='6H') solar_position = location.get_solarposition(times) - irrads = pd.DataFrame({'dni':[900,0], 'ghi':[600,0], 'dhi':[100,0]}, + irrads = pd.DataFrame({'dni': [900,0], 'ghi': [600,0], 'dhi': [100,0]}, index=times) array_one_expected = array_one.get_irradiance( solar_position['apparent_zenith'], From be8fc9ea1b47a9e74fdeffb1cae5ecd6b1bbc993 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 08:35:52 -0600 Subject: [PATCH 042/236] Test PVSystem.get_iam() input length mismatch --- pvlib/tests/test_pvsystem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index cce5b581a8..d589bfc2d4 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -42,6 +42,8 @@ def test_PVSystem_multi_array_get_iam(): iam = system.get_iam((1, 5), iam_model='ashrae') assert len(iam) == 2 assert iam[0] != iam[1] + with pytest.raises(ValueError, match="Length mismatch .*"): + system.get_iam((1,), iam_model='ashrae') def test_PVSystem_get_iam_sapm(sapm_module_params, mocker): From cb0f6709a5371037fe265e1c7c7287825b27a38f Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 08:36:25 -0600 Subject: [PATCH 043/236] Test PVSystem.sapm with multiple arrays --- pvlib/tests/test_pvsystem.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index d589bfc2d4..3cccc448b7 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -223,6 +223,30 @@ def test_PVSystem_sapm(sapm_module_params, mocker): assert_allclose(out['p_mp'], 100, atol=100) +def test_PVSystem_multi_array_sapm(sapm_module_params): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(module_parameters=sapm_module_params), + pvsystem.Array(module_parameters=sapm_module_params)] + ) + effective_irradiance=(100, 500) + temp_cell=(15, 25) + sapm_one, sapm_two = system.sapm(effective_irradiance, temp_cell) + assert sapm_one['p_mp'] != sapm_two['p_mp'] + sapm_one_flip, sapm_two_flip = system.sapm( + (effective_irradiance[1], effective_irradiance[0]), + (temp_cell[1], temp_cell[0]) + ) + assert sapm_one_flip['p_mp'] == sapm_two['p_mp'] + assert sapm_two_flip['p_mp'] == sapm_one['p_mp'] + with pytest.raises(ValueError, + match="Length mismatch for parameter temp_cell"): + system.sapm(effective_irradiance, 10) + with pytest.raises(ValueError, + match="Length mismatch for parameter " + "effective_irradiance"): + system.sapm(500, temp_cell) + + @pytest.mark.parametrize('airmass,expected', [ (1.5, 1.00028714375), (np.array([[10, np.nan]]), np.array([[0.999535, 0]])), From 8554aed71d23194d2bc72e085255776cb30a7a7f Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 08:37:40 -0600 Subject: [PATCH 044/236] Test PVSystem.sapm_spectral_loss with multiple arrays --- pvlib/tests/test_pvsystem.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 3cccc448b7..a622487b68 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -272,6 +272,15 @@ def test_PVSystem_sapm_spectral_loss(sapm_module_params, mocker): assert_allclose(out, 1, atol=0.5) +def test_PVSystem_multi_array_sapm_spectral_loss(sapm_module_params): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(module_parameters=sapm_module_params), + pvsystem.Array(module_parameters=sapm_module_params)] + ) + loss_one, loss_two = system.sapm_spectral_loss(2) + assert loss_one == loss_two + + # this test could be improved to cover all cell types. # could remove the need for specifying spectral coefficients if we don't # care about the return value at all From 5f6a0ce19dc8733bc34de09a658bad0a2af65a95 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 08:43:46 -0600 Subject: [PATCH 045/236] Test PVSystem.sapm_effective_irradiance with multiple arrays --- pvlib/tests/test_pvsystem.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index a622487b68..86927c939d 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -344,6 +344,36 @@ def test_PVSystem_sapm_effective_irradiance(sapm_module_params, mocker): assert_allclose(out, expected, atol=0.1) +def test_PVSystem_multi_array_sapm_effective_irradiance(sapm_module_params): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(module_parameters=sapm_module_params), + pvsystem.Array(module_parameters=sapm_module_params)] + ) + poa_direct = (500, 900) + poa_diffuse = (50, 100) + aoi = (0, 10) + airmass_absolute = 1.5 + irrad_one, irrad_two = system.sapm_effective_irradiance( + poa_direct, poa_diffuse, airmass_absolute, aoi + ) + assert irrad_one != irrad_two + + +@pytest.mark.parametrize("poa_direct, poa_diffuse, aoi", + [(20, (10, 10), (20, 20)), + ((20, 20), (10,), (20, 20)), + ((20, 20), (10, 10), 20)]) +def test_PVSystem_sapm_effective_irradiance_value_error( + poa_direct, poa_diffuse, aoi): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(), pvsystem.Array()] + ) + with pytest.raises(ValueError, match="Length mismatch for parameter .*"): + system.sapm_effective_irradiance( + poa_direct, poa_diffuse, 10, aoi + ) + + 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} From 0e88f9ef8edb2f95defecc7ed3153fc36a837638 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 08:45:42 -0600 Subject: [PATCH 046/236] Test PVSystem.sapm_celltemp() with multiple arrays --- pvlib/tests/test_pvsystem.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 86927c939d..afaaaf5c6b 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -404,6 +404,31 @@ def test_PVSystem_sapm_celltemp_kwargs(mocker): assert_allclose(out, 57, atol=1) +def test_PVSystem_multi_array_sapm_celltemp(): + temp_model_one = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ + 'open_rack_glass_glass'] + temp_model_two = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ + 'close_mount_glass_glass'] + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(temperature_model_parameters=temp_model_one), + pvsystem.Array(temperature_model_parameters=temp_model_two)] + ) + temp_one, temp_two = system.sapm_celltemp( + (1000, 1000), 25, 1 + ) + assert temp_one != temp_two + + +@pytest.mark.parametrize("irrad", [10, (10,), (1, 1, 1)]) +def test_PVSystem_multi_array_sapm_celltemp_value_error(irrad): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(), pvsystem.Array()] + ) + with pytest.raises(ValueError, + match="Length mismatch for parameter poa_global"): + system.sapm_celltemp(irrad, 25, 0) + + def test_PVSystem_pvsyst_celltemp(mocker): parameter_set = 'insulated' temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['pvsyst'][ From 25d7f9497ac34d7b8cc549c0e3dfc550e88a6808 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 08:48:22 -0600 Subject: [PATCH 047/236] Clean up whitespace --- pvlib/tests/test_pvsystem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index afaaaf5c6b..2ca96ebe8c 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -228,8 +228,8 @@ def test_PVSystem_multi_array_sapm(sapm_module_params): arrays=[pvsystem.Array(module_parameters=sapm_module_params), pvsystem.Array(module_parameters=sapm_module_params)] ) - effective_irradiance=(100, 500) - temp_cell=(15, 25) + effective_irradiance = (100, 500) + temp_cell = (15, 25) sapm_one, sapm_two = system.sapm(effective_irradiance, temp_cell) assert sapm_one['p_mp'] != sapm_two['p_mp'] sapm_one_flip, sapm_two_flip = system.sapm( @@ -1260,7 +1260,7 @@ def test_PVSystem_multi_array_get_irradiance(): times = pd.date_range(start='20160101 1200-0700', end='20160101 1800-0700', freq='6H') solar_position = location.get_solarposition(times) - irrads = pd.DataFrame({'dni': [900,0], 'ghi': [600,0], 'dhi': [100,0]}, + irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]}, index=times) array_one_expected = array_one.get_irradiance( solar_position['apparent_zenith'], From 55c202c4aaa3cdca679d00ada5a2c7034afa7323 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 10:10:18 -0600 Subject: [PATCH 048/236] Test PVSystem.first_solar_spectral_loss() with multiple arrays --- pvlib/tests/test_pvsystem.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 2ca96ebe8c..a1c3307ac8 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -304,6 +304,21 @@ def test_PVSystem_first_solar_spectral_loss(module_parameters, module_type, assert_allclose(out, 1, atol=0.5) +def test_PVSystem_multi_array_first_solar_spectral_loss(): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array( + module_parameters={'Technology': 'mc-Si'}, + module_type='multisi' + ), + pvsystem.Array( + module_parameters={'Technology': 'mc-Si'}, + module_type='multisi' + )] + ) + loss_one, loss_two = system.first_solar_spectral_loss(1, 3) + assert loss_one == loss_two + + @pytest.mark.parametrize('test_input,expected', [ ([1000, 100, 5, 45], 1140.0510967821877), ([np.array([np.nan, 1000, 1000]), From fc22db003d43675023b75d41a7bd5dc12e0bc56c Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 10:29:49 -0600 Subject: [PATCH 049/236] Test PVSystem.pvsyst_celltemp() with multiple arrays Add two_array_system fixture to reduce duplicate setup code. --- pvlib/tests/test_pvsystem.py | 37 ++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index a1c3307ac8..ff2f5cac31 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -374,17 +374,22 @@ def test_PVSystem_multi_array_sapm_effective_irradiance(sapm_module_params): assert irrad_one != irrad_two +@pytest.fixture +def two_array_system(): + """Two-array PVSystem where Arrays use default parameters.""" + return pvsystem.PVSystem( + arrays=[pvsystem.Array(), pvsystem.Array()] + ) + + @pytest.mark.parametrize("poa_direct, poa_diffuse, aoi", [(20, (10, 10), (20, 20)), ((20, 20), (10,), (20, 20)), ((20, 20), (10, 10), 20)]) def test_PVSystem_sapm_effective_irradiance_value_error( - poa_direct, poa_diffuse, aoi): - system = pvsystem.PVSystem( - arrays=[pvsystem.Array(), pvsystem.Array()] - ) + poa_direct, poa_diffuse, aoi, two_array_system): with pytest.raises(ValueError, match="Length mismatch for parameter .*"): - system.sapm_effective_irradiance( + two_array_system.sapm_effective_irradiance( poa_direct, poa_diffuse, 10, aoi ) @@ -435,13 +440,11 @@ def test_PVSystem_multi_array_sapm_celltemp(): @pytest.mark.parametrize("irrad", [10, (10,), (1, 1, 1)]) -def test_PVSystem_multi_array_sapm_celltemp_value_error(irrad): - system = pvsystem.PVSystem( - arrays=[pvsystem.Array(), pvsystem.Array()] - ) +def test_PVSystem_multi_array_sapm_celltemp_value_error( + irrad, two_array_system): with pytest.raises(ValueError, match="Length mismatch for parameter poa_global"): - system.sapm_celltemp(irrad, 25, 0) + two_array_system.sapm_celltemp(irrad, 25, 0) def test_PVSystem_pvsyst_celltemp(mocker): @@ -464,6 +467,20 @@ def test_PVSystem_pvsyst_celltemp(mocker): assert (out < 90) and (out > 70) +def test_PVSystem_multi_array_pvsyst_celltemp(two_array_system): + temp_one, temp_two = two_array_system.pvsyst_celltemp( + (100, 800), 45, 1 + ) + assert temp_one != temp_two + +@pytest.mark.parametrize("irrad", [1, (1, 2, 3), (1,)]) +def test_PVSystem_multi_array_pvsyst_celltemp_value_error( + irrad, two_array_system): + with pytest.raises(ValueError, + match="Length mismatch for parameter poa_global"): + two_array_system.pvsyst_celltemp(irrad, 1, 1) + + def test_PVSystem_faiman_celltemp(mocker): u0, u1 = 25.0, 6.84 # default values temp_model_params = {'u0': u0, 'u1': u1} From 84653bbd97ed05fd004f75517783ada01d69cfd5 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 11:01:44 -0600 Subject: [PATCH 050/236] Consolidate tests for PVSystem.xxxx_celltemp() functions These all follow a similar pattern, so we can consolidate into two tests patrametrized by a list of the celltemp functions to test. --- pvlib/tests/test_pvsystem.py | 56 ++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index ff2f5cac31..160c4cd908 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -376,9 +376,16 @@ def test_PVSystem_multi_array_sapm_effective_irradiance(sapm_module_params): @pytest.fixture def two_array_system(): - """Two-array PVSystem where Arrays use default parameters.""" + """Two-array PVSystem. + + Both arrays are identical. + """ + temperature_model = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ + 'open_rack_glass_glass' + ] return pvsystem.PVSystem( - arrays=[pvsystem.Array(), pvsystem.Array()] + arrays=[pvsystem.Array(temperature_model_parameters=temperature_model), + pvsystem.Array(temperature_model_parameters=temperature_model)] ) @@ -424,7 +431,7 @@ def test_PVSystem_sapm_celltemp_kwargs(mocker): assert_allclose(out, 57, atol=1) -def test_PVSystem_multi_array_sapm_celltemp(): +def test_PVSystem_multi_array_sapm_celltemp_different_arrays(): temp_model_one = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ 'open_rack_glass_glass'] temp_model_two = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ @@ -439,14 +446,6 @@ def test_PVSystem_multi_array_sapm_celltemp(): assert temp_one != temp_two -@pytest.mark.parametrize("irrad", [10, (10,), (1, 1, 1)]) -def test_PVSystem_multi_array_sapm_celltemp_value_error( - irrad, two_array_system): - with pytest.raises(ValueError, - match="Length mismatch for parameter poa_global"): - two_array_system.sapm_celltemp(irrad, 25, 0) - - def test_PVSystem_pvsyst_celltemp(mocker): parameter_set = 'insulated' temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['pvsyst'][ @@ -467,20 +466,6 @@ def test_PVSystem_pvsyst_celltemp(mocker): assert (out < 90) and (out > 70) -def test_PVSystem_multi_array_pvsyst_celltemp(two_array_system): - temp_one, temp_two = two_array_system.pvsyst_celltemp( - (100, 800), 45, 1 - ) - assert temp_one != temp_two - -@pytest.mark.parametrize("irrad", [1, (1, 2, 3), (1,)]) -def test_PVSystem_multi_array_pvsyst_celltemp_value_error( - irrad, two_array_system): - with pytest.raises(ValueError, - match="Length mismatch for parameter poa_global"): - two_array_system.pvsyst_celltemp(irrad, 1, 1) - - def test_PVSystem_faiman_celltemp(mocker): u0, u1 = 25.0, 6.84 # default values temp_model_params = {'u0': u0, 'u1': u1} @@ -494,6 +479,27 @@ def test_PVSystem_faiman_celltemp(mocker): assert_allclose(out, 56.4, atol=1) +@pytest.mark.parametrize("celltemp", + [pvsystem.PVSystem.faiman_celltemp, + pvsystem.PVSystem.pvsyst_celltemp, + pvsystem.PVSystem.sapm_celltemp]) +def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system): + temp_one, temp_two = celltemp(two_array_system, (1000, 500), 25, 1) + assert temp_one != temp_two + + +@pytest.mark.parametrize("celltemp", + [pvsystem.PVSystem.faiman_celltemp, + pvsystem.PVSystem.pvsyst_celltemp, + pvsystem.PVSystem.fuentes_celltemp, + pvsystem.PVSystem.sapm_celltemp]) +def test_PVSystem_multi_array_celltemp_poa_length_mismatch( + celltemp, two_array_system): + with pytest.raises(ValueError, + match="Length mismatch for parameter poa_global"): + celltemp(two_array_system, 1000, 25, 1) + + def test_PVSystem_fuentes_celltemp(mocker): noct_installed = 45 temp_model_params = {'noct_installed': noct_installed} From 97975f3de224c9f6b889eb2667bcdae13e2f7cc0 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 11:05:16 -0600 Subject: [PATCH 051/236] Clean up whitespace --- pvlib/tests/test_pvsystem.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 160c4cd908..d43e5409b3 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -306,14 +306,16 @@ def test_PVSystem_first_solar_spectral_loss(module_parameters, module_type, def test_PVSystem_multi_array_first_solar_spectral_loss(): system = pvsystem.PVSystem( - arrays=[pvsystem.Array( - module_parameters={'Technology': 'mc-Si'}, - module_type='multisi' - ), - pvsystem.Array( - module_parameters={'Technology': 'mc-Si'}, - module_type='multisi' - )] + arrays=[ + pvsystem.Array( + module_parameters={'Technology': 'mc-Si'}, + module_type='multisi' + ), + pvsystem.Array( + module_parameters={'Technology': 'mc-Si'}, + module_type='multisi' + ) + ] ) loss_one, loss_two = system.first_solar_spectral_loss(1, 3) assert loss_one == loss_two From d90970d24acb6cda53c0e4fcfaba422cfd598410 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 11:06:34 -0600 Subject: [PATCH 052/236] Fix comment in PVSystem.fuentes_celltemp() Now we get the surface_tilt attribute form the Array, not PVSystem --- pvlib/pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 6a8c51e94c..638ea7f2da 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -735,7 +735,7 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): if you want to match the PVWatts behavior, you can override it by including a ``surface_tilt`` value in ``temperature_model_parameters``. """ - # default to using the PVSystem attribute, but allow user to + # default to using the Array attribute, but allow user to # override with a custom surface_tilt value def _build_kwargs_fuentes(array): kwargs = {'surface_tilt': array.surface_tilt} From 16e6fa95358973e92a18879fc2451a7dc64543ee Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 11:32:33 -0600 Subject: [PATCH 053/236] Test all PVSystem.calcparams_xxxx() functions with multiple arrays --- pvlib/tests/test_pvsystem.py | 38 +++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index d43e5409b3..c67ab9f1c5 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -377,7 +377,7 @@ def test_PVSystem_multi_array_sapm_effective_irradiance(sapm_module_params): @pytest.fixture -def two_array_system(): +def two_array_system(pvsyst_module_params, cec_module_params): """Two-array PVSystem. Both arrays are identical. @@ -385,9 +385,18 @@ def two_array_system(): temperature_model = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ 'open_rack_glass_glass' ] + module_params = {**pvsyst_module_params, **cec_module_params} return pvsystem.PVSystem( - arrays=[pvsystem.Array(temperature_model_parameters=temperature_model), - pvsystem.Array(temperature_model_parameters=temperature_model)] + arrays=[ + pvsystem.Array( + temperature_model_parameters=temperature_model, + module_parameters=module_params + ), + pvsystem.Array( + temperature_model_parameters=temperature_model, + module_parameters=module_params + ) + ] ) @@ -706,6 +715,29 @@ def test_PVSystem_calcparams_pvsyst(pvsyst_module_params, mocker): assert_allclose(nNsVth, np.array([1.6186, 1.7961]), atol=0.1) +@pytest.mark.parametrize('calcparams', [pvsystem.PVSystem.calcparams_pvsyst, + pvsystem.PVSystem.calcparams_desoto, + pvsystem.PVSystem.calcparams_cec]) +def test_PVSystem_multi_array_calcparams(calcparams, two_array_system): + params_one, params_two = calcparams( + two_array_system, (1000, 500), (30, 20) + ) + assert params_one != params_two + + +@pytest.mark.parametrize('calcparams, irrad, celltemp', + [ (f, irrad, celltemp) + for f in (pvsystem.PVSystem.calcparams_desoto, + pvsystem.PVSystem.calcparams_cec, + pvsystem.PVSystem.calcparams_pvsyst) + for irrad, celltemp in [(1, (1, 1)), ((1, 1), 1)]]) +def test_PVSystem_multi_array_calcparams_value_error( + calcparams, irrad, celltemp, two_array_system): + with pytest.raises(ValueError, + match='Length mismatch for parameter'): + calcparams(two_array_system, irrad, celltemp) + + @pytest.fixture(params=[ { # Can handle all python scalar inputs 'Rsh': 20., From c82eccbfeb49e1697029fa894fca282e5995a327 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 12:14:50 -0600 Subject: [PATCH 054/236] Add tests to cover PVSystem.albedo and PVSystem.surface_azimuth attributes --- pvlib/tests/test_pvsystem.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index c67ab9f1c5..0761cd6a93 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1357,6 +1357,21 @@ def test_PVSystem_multi_array_get_irradiance(): ) +def test_PVSystem_change_surface_azimuth(): + system = pvsystem.PVSystem(surface_azimuth=180) + assert system.surface_azimuth == 180 + system.surface_azimuth = 90 + assert system.surface_azimuth == 90 + + +def test_PVSystem_get_albedo(two_array_system): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(albedo=0.5)] + ) + assert system.albedo == 0.5 + assert two_array_system.albedo == (0.25, 0.25) + + @fail_on_pvlib_version('0.9') def test_PVSystem_localize_with_location(): system = pvsystem.PVSystem(module='blah', inverter='blarg') From f82ff5e55f2bbbd821cedd3d99596506f5136479 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 13:04:32 -0600 Subject: [PATCH 055/236] Conditionally install dataclasses for tests --- ci/azure/posix.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ci/azure/posix.yml b/ci/azure/posix.yml index 806cd27e77..fc35137293 100644 --- a/ci/azure/posix.yml +++ b/ci/azure/posix.yml @@ -20,6 +20,10 @@ jobs: inputs: versionSpec: '$(python.version)' + - script: pip install dataclasses + condition: eq(variables['python.version'], '3.6') + displayName: Install dataclasses if python 3.6 + - script: | pip install pytest pytest-cov pytest-mock pytest-timeout pytest-azurepipelines pytest-rerunfailures pytest-remotedata pip install -e . From 402e42fde3e3fd499e2ace6287ad6bd787cdbc23 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 19 Oct 2020 13:57:00 -0600 Subject: [PATCH 056/236] formatting --- pvlib/tests/test_modelchain.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 83c381c63d..bdf7409af8 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -206,13 +206,13 @@ def sapm_dc_snl_ac_system_Array(sapm_module_params, cec_inverter_parameters, array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=180, albedo=0.2, module=module, module_parameters=module_parameters, - temperature_mode_parameters = temp_model_params, + temperature_mode_parameters=temp_model_params, modules_per_string=1, strings_per_inverter=1) array_two = pvsystem.Array(surface_tilt=15, surface_azimuth=180, albedo=0.2, module=module, module_parameters=module_parameters, - temperature_mode_parameters = temp_model_params, + temperature_mode_parameters=temp_model_params, modules_per_string=1, strings_per_inverter=1) return PVSystem(arrays=[array_one, array_two]) @@ -812,7 +812,7 @@ def test_bad_get_orientation(): modelchain.get_orientation('bad value') -# tests for PVSystem with multiple Array +# tests for PVSystem with multiple Array def test_with_sapm_pvsystem_arrasy(sapm_dc_snl_ac_system, location, weather): mc = ModelChain.with_sapm(sapm_dc_snl_ac_system, location) assert mc.dc_model == mc.sapm From f220a822c5f19b2173bfcb9c7fd2efbc0eafe87a Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 14:30:47 -0600 Subject: [PATCH 057/236] Remove TODOs --- pvlib/pvsystem.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 638ea7f2da..3c1ed30e05 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1131,16 +1131,12 @@ def __init__(self, self.surface_tilt = surface_tilt self.surface_azimuth = surface_azimuth - # TODO now would be a good time to address the suggestion above: - # '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 - # TODO now would be a good time to address the suggestion above: - # 'could tie these together with @property' self.module = module if module_parameters is None: self.module_parameters = {} From 9e7960fde423362f798ceee38ad660ab5a031d31 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 14:32:29 -0600 Subject: [PATCH 058/236] Make decorators private These decorators are not intended to be used anywhere else. Also rename to avoid 'singleton' which is not being used in the standard "singleton design pattern" sense. --- pvlib/pvsystem.py | 74 ++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 3c1ed30e05..d4bfb9b03a 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -69,10 +69,12 @@ def _combine_localized_attributes(pvsystem=None, location=None, **kwargs): return new_kwargs -def return_singleton_value(func): - """Decorator that returns the value contained when `func` - returns a singleton list or the list if it has more than one - element. +def _unwrap_single_value(func): + """Decorator for functions that return iterables. + + If the length of the iterable returned by `func` is 1, then + the single member of the iterable is returned. If the length is + greater than 1, then entire iterable is returned. """ @functools.wraps(func) def f(*args, **kwargs): @@ -84,7 +86,7 @@ def f(*args, **kwargs): return f -def validate_against_arrays(*list_params): +def _validate_against_arrays(*list_params): """Decorator that validates the value passed to each parameter in `list_params` against the number of Arrays in the PVSystem. @@ -101,8 +103,8 @@ def validate_against_arrays(*list_params): names of the parameters that should be lists with the same number of elements as there are Arrays in the PVSystem instance. """ - def list_or_scalar(func): - @return_singleton_value + def validate_args(func): + @_unwrap_single_value @functools.wraps(func) def f(ref, *args, **kwargs): sig = inspect.signature(func) @@ -118,7 +120,7 @@ def f(ref, *args, **kwargs): ) return func(*bindings.args, **bindings.kwargs) return f - return list_or_scalar + return validate_args # not sure if this belongs in the pvsystem module. @@ -290,7 +292,7 @@ def __repr__(self): return ('PVSystem:\n ' + '\n '.join( f'{attr}: {getattr(self, attr)}' for attr in attrs)) - @return_singleton_value + @_unwrap_single_value def _infer_cell_type(self): """ @@ -304,7 +306,7 @@ def _infer_cell_type(self): """ return tuple(array._infer_cell_type() for array in self._arrays) - @return_singleton_value + @_unwrap_single_value def get_aoi(self, solar_zenith, solar_azimuth): """Get the angle of incidence on the system. @@ -324,7 +326,7 @@ def get_aoi(self, solar_zenith, solar_azimuth): return tuple(array.get_aoi(solar_zenith, solar_azimuth) for array in self._arrays) - @return_singleton_value + @_unwrap_single_value def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, model='haydavies', **kwargs): @@ -366,7 +368,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra, airmass) for array in self._arrays) - @validate_against_arrays('aoi') + @_validate_against_arrays('aoi') def get_iam(self, aoi, iam_model='physical'): """ Determine the incidence angle modifier using the method specified by @@ -396,7 +398,7 @@ def get_iam(self, aoi, iam_model='physical'): return tuple(array.get_iam(aoi, iam_model) for array, aoi in zip(self._arrays, aoi)) - @validate_against_arrays('effective_irradiance', 'temp_cell') + @_validate_against_arrays('effective_irradiance', 'temp_cell') def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`calcparams_desoto` function, the input @@ -435,7 +437,7 @@ def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): in zip(self._arrays, effective_irradiance, temp_cell) ) - @validate_against_arrays('effective_irradiance', 'temp_cell') + @_validate_against_arrays('effective_irradiance', 'temp_cell') def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`calcparams_cec` function, the input @@ -474,7 +476,7 @@ def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): in zip(self._arrays, effective_irradiance, temp_cell) ) - @validate_against_arrays('effective_irradiance', 'temp_cell') + @_validate_against_arrays('effective_irradiance', 'temp_cell') def calcparams_pvsyst(self, effective_irradiance, temp_cell): """ Use the :py:func:`calcparams_pvsyst` function, the input @@ -512,7 +514,7 @@ def calcparams_pvsyst(self, effective_irradiance, temp_cell): in zip(self._arrays, effective_irradiance, temp_cell) ) - @validate_against_arrays('effective_irradiance', 'temp_cell') + @_validate_against_arrays('effective_irradiance', 'temp_cell') def sapm(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`sapm` function, the input parameters, @@ -540,7 +542,7 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): in zip(self._arrays, effective_irradiance, temp_cell) ) - @validate_against_arrays('poa_global') + @_validate_against_arrays('poa_global') def sapm_celltemp(self, poa_global, temp_air, wind_speed): """Uses :py:func:`temperature.sapm_cell` to calculate cell temperatures. @@ -585,7 +587,7 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): for array, poa_global in zip(self._arrays, poa_global) ) - @return_singleton_value + @_unwrap_single_value def sapm_spectral_loss(self, airmass_absolute): """ Use the :py:func:`sapm_spectral_loss` function, the input @@ -606,7 +608,7 @@ def sapm_spectral_loss(self, airmass_absolute): for array in self._arrays ) - @validate_against_arrays('poa_direct', 'poa_diffuse', 'aoi') + @_validate_against_arrays('poa_direct', 'poa_diffuse', 'aoi') def sapm_effective_irradiance(self, poa_direct, poa_diffuse, airmass_absolute, aoi, reference_irradiance=1000): @@ -642,7 +644,7 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, in zip(self._arrays, poa_direct, poa_diffuse, aoi) ) - @validate_against_arrays('poa_global') + @_validate_against_arrays('poa_global') def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): """Uses :py:func:`temperature.pvsyst_cell` to calculate cell temperature. @@ -675,7 +677,7 @@ def build_celltemp_kwargs(array): for array, poa_global in zip(self._arrays, poa_global) ) - @validate_against_arrays('poa_global') + @_validate_against_arrays('poa_global') def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): """ Use :py:func:`temperature.faiman` to calculate cell temperature. @@ -705,7 +707,7 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): for array, poa_global in zip(self._arrays, poa_global) ) - @validate_against_arrays('poa_global') + @_validate_against_arrays('poa_global') def fuentes_celltemp(self, poa_global, temp_air, wind_speed): """ Use :py:func:`temperature.fuentes` to calculate cell temperature. @@ -752,7 +754,7 @@ def _build_kwargs_fuentes(array): for array, poa_global in zip(self._arrays, poa_global) ) - @return_singleton_value + @_unwrap_single_value def first_solar_spectral_loss(self, pw, airmass_absolute): """ @@ -862,8 +864,8 @@ def adrinverter(self, v_dc, p_dc): """ return inverter.adr(v_dc, p_dc, self.inverter_parameters) - @return_singleton_value - @validate_against_arrays('data') + @_unwrap_single_value + @_validate_against_arrays('data') def scale_voltage_current_power(self, data): """ Scales the voltage, current, and power of the `data` DataFrame @@ -888,7 +890,7 @@ def scale_voltage_current_power(self, data): for array, data in zip(self._arrays, data) ) - @validate_against_arrays('g_poa_effective', 'temp_cell') + @_validate_against_arrays('g_poa_effective', 'temp_cell') def pvwatts_dc(self, g_poa_effective, temp_cell): """ Calcuates DC power according to the PVWatts model using @@ -961,22 +963,22 @@ def localize(self, location=None, latitude=None, longitude=None, return LocalizedPVSystem(pvsystem=self, location=location) @property - @return_singleton_value + @_unwrap_single_value def module_parameters(self): return tuple(array.module_parameters for array in self._arrays) @property - @return_singleton_value + @_unwrap_single_value def module(self): return tuple(array.module for array in self._arrays) @property - @return_singleton_value + @_unwrap_single_value def module_type(self): return tuple(array.module_type for array in self._arrays) @property - @return_singleton_value + @_unwrap_single_value def temperature_model_parameters(self): return tuple(array.temperature_model_parameters for array in self._arrays) @@ -987,7 +989,7 @@ def temperature_model_parameters(self, value): array.temperature_model_parameters = value @property - @return_singleton_value + @_unwrap_single_value def surface_tilt(self): return tuple(array.surface_tilt for array in self._arrays) @@ -997,7 +999,7 @@ def surface_tilt(self, value): array.surface_tilt = value @property - @return_singleton_value + @_unwrap_single_value def surface_azimuth(self): return tuple(array.surface_azimuth for array in self._arrays) @@ -1007,12 +1009,12 @@ def surface_azimuth(self, value): array.surface_azimuth = value @property - @return_singleton_value + @_unwrap_single_value def albedo(self): return tuple(array.albedo for array in self._arrays) @property - @return_singleton_value + @_unwrap_single_value def racking_model(self): return tuple(array.racking_model for array in self._arrays) @@ -1022,12 +1024,12 @@ def racking_model(self, value): array.racking_model = value @property - @return_singleton_value + @_unwrap_single_value def modules_per_string(self): return tuple(array.modules_per_string for array in self._arrays) @property - @return_singleton_value + @_unwrap_single_value def strings_per_inverter(self): return tuple(array.strings for array in self._arrays) From 26001413ef4f61c961787a0bb9c7fbee0bcb72d2 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 15:10:40 -0600 Subject: [PATCH 059/236] Update PVSystem.__repr__() to handle multiple Arrays --- pvlib/pvsystem.py | 11 ++++---- pvlib/tests/test_pvsystem.py | 51 ++++++++++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index d4bfb9b03a..0c95f547a4 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -286,11 +286,12 @@ def __init__(self, ) def __repr__(self): - attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module', - 'inverter', 'albedo', 'racking_model', 'module_type', - 'temperature_model_parameters'] - return ('PVSystem:\n ' + '\n '.join( - f'{attr}: {getattr(self, attr)}' for attr in attrs)) + repr = f'PVSystem:\n name: {self.name}\n ' + for array in self._arrays: + repr += '\n '.join(array.__repr__().split('\n')) + repr += '\n ' + repr += f'inverter: {self.inverter}' + return repr @_unwrap_single_value def _infer_cell_type(self): diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 0761cd6a93..3aac44bf9e 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1404,17 +1404,52 @@ def test_PVSystem___repr__(): expected = """PVSystem: name: pv ftw - surface_tilt: 0 - surface_azimuth: 180 - module: blah - inverter: blarg - albedo: 0.25 - racking_model: None - module_type: None - temperature_model_parameters: {'a': -3.56}""" + Array: + surface_tilt: 0 + surface_azimuth: 180 + module: blah + albedo: 0.25 + racking_model: None + module_type: None + temperature_model_parameters: {'a': -3.56} + strings: 1 + modules_per_string: 1 + inverter: blarg""" assert system.__repr__() == expected +def test_PVSystem_multi_array___repr__(): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(surface_tilt=30, surface_azimuth=100), + pvsystem.Array(surface_tilt=20, surface_azimuth=220)], + inverter='blarg', + ) + expected = """PVSystem: + name: None + Array: + surface_tilt: 30 + surface_azimuth: 100 + module: None + albedo: 0.25 + racking_model: None + module_type: None + temperature_model_parameters: {} + strings: 1 + modules_per_string: 1 + Array: + surface_tilt: 20 + surface_azimuth: 220 + module: None + albedo: 0.25 + racking_model: None + module_type: None + temperature_model_parameters: {} + strings: 1 + modules_per_string: 1 + inverter: blarg""" + assert expected == system.__repr__() + + @fail_on_pvlib_version('0.9') def test_PVSystem_localize___repr__(): system = pvsystem.PVSystem( From b72840c44bf0d8187a5a59fe8da93a8400a7488b Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 15:21:15 -0600 Subject: [PATCH 060/236] Expect new format in tracking.SingleAxisTracker.__repr__() tests Format has changed to support multi-array PVSystems. --- pvlib/tests/test_tracking.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 897c64baa0..6c34277351 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -451,14 +451,17 @@ def test_SingleAxisTracker___repr__(): gcr: 0.25 cross_axis_tilt: 0.0 name: None - surface_tilt: None - surface_azimuth: None - module: blah - inverter: blarg - albedo: 0.25 - racking_model: None - module_type: None - temperature_model_parameters: {'a': -3.56}""" + Array: + surface_tilt: None + surface_azimuth: None + module: blah + albedo: 0.25 + racking_model: None + module_type: None + temperature_model_parameters: {'a': -3.56} + strings: 1 + modules_per_string: 1 + inverter: blarg""" assert system.__repr__() == expected @@ -476,14 +479,17 @@ def test_LocalizedSingleAxisTracker___repr__(): gcr: 0.25 cross_axis_tilt: 0.0 name: None - surface_tilt: None - surface_azimuth: None - module: blah + Array: + surface_tilt: None + surface_azimuth: None + module: blah + albedo: 0.25 + racking_model: None + module_type: None + temperature_model_parameters: {'a': -3.56} + strings: 1 + modules_per_string: 1 inverter: blarg - albedo: 0.25 - racking_model: None - module_type: None - temperature_model_parameters: {'a': -3.56} latitude: 32 longitude: -111 altitude: 0 From ea26f838464809e5ebc06e9e3e3ebabd67f446ac Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 19 Oct 2020 15:35:40 -0600 Subject: [PATCH 061/236] Update parameter names and doc for _validate_against_arrays() --- pvlib/pvsystem.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 0c95f547a4..3f48ee6e3f 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -86,20 +86,20 @@ def f(*args, **kwargs): return f -def _validate_against_arrays(*list_params): +def _validate_against_arrays(*tuple_params): """Decorator that validates the value passed to each parameter in `list_params` against the number of Arrays in the PVSystem. - If the value passed for each parameter in `list_params` is not a list, - then it is transformed into a singleton list before validation. This + If the value passed for each parameter in `list_params` is not a tuple, + then it is transformed into a length 1 before validation. This means existing code that assumes a PVSystem has only one array will continue to function with no changes. - Implicitly applies the `@singleton_as_scalar` decorator as well. + Implicitly applies the `@_unwrap_single_value` decorator as well. Parameters ---------- - list_params : iterable + tuple_params : iterable names of the parameters that should be lists with the same number of elements as there are Arrays in the PVSystem instance. """ @@ -110,7 +110,7 @@ def f(ref, *args, **kwargs): sig = inspect.signature(func) bindings = sig.bind(*tuple([ref, *args]), **kwargs) for param in bindings.arguments.keys(): - if param in list_params: + if param in tuple_params: value = bindings.arguments[param] if not isinstance(value, tuple): bindings.arguments[param] = (value,) From 482b2fdfe839f91bd5ad150a8b7e40034536082e Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 20 Oct 2020 13:24:47 -0600 Subject: [PATCH 062/236] Replace validation decorator with a private method The decorator adds too much complexity and room for error in the future. A validation method that is called once on each parameter that needs to be the same shape as the `self._arrays` tuple is much easier to read, understand, and maintain. It also has the benefit of increased flexibility since it allow validation to be triggered at any point, rather than before the method is called. --- pvlib/pvsystem.py | 92 ++++++++++++++++-------------------- pvlib/tests/test_pvsystem.py | 38 +++++++-------- 2 files changed, 61 insertions(+), 69 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 3f48ee6e3f..8d8571816e 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -86,43 +86,6 @@ def f(*args, **kwargs): return f -def _validate_against_arrays(*tuple_params): - """Decorator that validates the value passed to each parameter in - `list_params` against the number of Arrays in the PVSystem. - - If the value passed for each parameter in `list_params` is not a tuple, - then it is transformed into a length 1 before validation. This - means existing code that assumes a PVSystem has only one array will - continue to function with no changes. - - Implicitly applies the `@_unwrap_single_value` decorator as well. - - Parameters - ---------- - tuple_params : iterable - names of the parameters that should be lists with the same number - of elements as there are Arrays in the PVSystem instance. - """ - def validate_args(func): - @_unwrap_single_value - @functools.wraps(func) - def f(ref, *args, **kwargs): - sig = inspect.signature(func) - bindings = sig.bind(*tuple([ref, *args]), **kwargs) - for param in bindings.arguments.keys(): - if param in tuple_params: - value = bindings.arguments[param] - if not isinstance(value, tuple): - bindings.arguments[param] = (value,) - if len(bindings.arguments[param]) != len(ref._arrays): - raise ValueError( - f"Length mismatch for parameter {param}" - ) - return func(*bindings.args, **bindings.kwargs) - return f - return validate_args - - # 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. @@ -293,6 +256,17 @@ def __repr__(self): repr += f'inverter: {self.inverter}' return repr + def _validate_per_array(self, values): + # Check that values is a tuple of the same length as + # `self._arrays`. If it is a single vlaue it is packed in to + # a length-1 tuple before the check. If the length is not the + # same a ValueError is raised, otherwise the tuple is returned. + if not isinstance(values, tuple): + values = (values,) + if len(values) != len(self._arrays): + raise ValueError("Length mismatch for per-array parameter") + return values + @_unwrap_single_value def _infer_cell_type(self): @@ -369,7 +343,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra, airmass) for array in self._arrays) - @_validate_against_arrays('aoi') + @_unwrap_single_value def get_iam(self, aoi, iam_model='physical'): """ Determine the incidence angle modifier using the method specified by @@ -396,10 +370,11 @@ def get_iam(self, aoi, iam_model='physical'): ------ ValueError if `iam_model` is not a valid model name. """ + aoi = self._validate_per_array(aoi) return tuple(array.get_iam(aoi, iam_model) for array, aoi in zip(self._arrays, aoi)) - @_validate_against_arrays('effective_irradiance', 'temp_cell') + @_unwrap_single_value def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`calcparams_desoto` function, the input @@ -421,6 +396,8 @@ def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): ------- See pvsystem.calcparams_desoto for details """ + effective_irradiance = self._validate_per_array(effective_irradiance) + temp_cell = self._validate_per_array(temp_cell) build_kwargs = functools.partial( _build_kwargs, @@ -438,7 +415,7 @@ def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): in zip(self._arrays, effective_irradiance, temp_cell) ) - @_validate_against_arrays('effective_irradiance', 'temp_cell') + @_unwrap_single_value def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`calcparams_cec` function, the input @@ -460,6 +437,8 @@ def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): ------- See pvsystem.calcparams_cec for details """ + effective_irradiance = self._validate_per_array(effective_irradiance) + temp_cell = self._validate_per_array(temp_cell) build_kwargs = functools.partial( _build_kwargs, @@ -477,7 +456,7 @@ def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): in zip(self._arrays, effective_irradiance, temp_cell) ) - @_validate_against_arrays('effective_irradiance', 'temp_cell') + @_unwrap_single_value def calcparams_pvsyst(self, effective_irradiance, temp_cell): """ Use the :py:func:`calcparams_pvsyst` function, the input @@ -496,6 +475,8 @@ def calcparams_pvsyst(self, effective_irradiance, temp_cell): ------- See pvsystem.calcparams_pvsyst for details """ + effective_irradiance = self._validate_per_array(effective_irradiance) + temp_cell = self._validate_per_array(temp_cell) build_kwargs = functools.partial( _build_kwargs, @@ -515,7 +496,7 @@ def calcparams_pvsyst(self, effective_irradiance, temp_cell): in zip(self._arrays, effective_irradiance, temp_cell) ) - @_validate_against_arrays('effective_irradiance', 'temp_cell') + @_unwrap_single_value def sapm(self, effective_irradiance, temp_cell, **kwargs): """ Use the :py:func:`sapm` function, the input parameters, @@ -537,13 +518,16 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): ------- See pvsystem.sapm for details """ + effective_irradiance = self._validate_per_array(effective_irradiance) + temp_cell = self._validate_per_array(temp_cell) + return tuple( sapm(effective_irradiance, temp_cell, array.module_parameters) for array, effective_irradiance, temp_cell in zip(self._arrays, effective_irradiance, temp_cell) ) - @_validate_against_arrays('poa_global') + @_unwrap_single_value def sapm_celltemp(self, poa_global, temp_air, wind_speed): """Uses :py:func:`temperature.sapm_cell` to calculate cell temperatures. @@ -563,6 +547,7 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): ------- numeric, values in degrees C. """ + poa_global = self._validate_per_array(poa_global) for array in self._arrays: # warn user about change in default behavior in 0.9. if (array.temperature_model_parameters == {} and array.module_type @@ -609,7 +594,7 @@ def sapm_spectral_loss(self, airmass_absolute): for array in self._arrays ) - @_validate_against_arrays('poa_direct', 'poa_diffuse', 'aoi') + @_unwrap_single_value def sapm_effective_irradiance(self, poa_direct, poa_diffuse, airmass_absolute, aoi, reference_irradiance=1000): @@ -637,6 +622,9 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, effective_irradiance : numeric The SAPM effective irradiance. [W/m2] """ + poa_direct = self._validate_per_array(poa_direct) + poa_diffuse = self._validate_per_array(poa_diffuse) + aoi = self._validate_per_array(aoi) return tuple( sapm_effective_irradiance( poa_direct, poa_diffuse, airmass_absolute, aoi, @@ -645,7 +633,7 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, in zip(self._arrays, poa_direct, poa_diffuse, aoi) ) - @_validate_against_arrays('poa_global') + @_unwrap_single_value def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): """Uses :py:func:`temperature.pvsyst_cell` to calculate cell temperature. @@ -667,6 +655,7 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): ------- numeric, values in degrees C. """ + poa_global = self._validate_per_array(poa_global) def build_celltemp_kwargs(array): return {**_build_kwargs(['eta_m', 'alpha_absorption'], array.module_parameters), @@ -678,7 +667,7 @@ def build_celltemp_kwargs(array): for array, poa_global in zip(self._arrays, poa_global) ) - @_validate_against_arrays('poa_global') + @_unwrap_single_value def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): """ Use :py:func:`temperature.faiman` to calculate cell temperature. @@ -700,6 +689,7 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): ------- numeric, values in degrees C. """ + poa_global = self._validate_per_array(poa_global) return tuple( temperature.faiman( poa_global, temp_air, wind_speed, @@ -708,7 +698,7 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): for array, poa_global in zip(self._arrays, poa_global) ) - @_validate_against_arrays('poa_global') + @_unwrap_single_value def fuentes_celltemp(self, poa_global, temp_air, wind_speed): """ Use :py:func:`temperature.fuentes` to calculate cell temperature. @@ -740,6 +730,7 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): """ # default to using the Array attribute, but allow user to # override with a custom surface_tilt value + poa_global = self._validate_per_array(poa_global) def _build_kwargs_fuentes(array): kwargs = {'surface_tilt': array.surface_tilt} temp_model_kwargs = _build_kwargs([ @@ -866,7 +857,6 @@ def adrinverter(self, v_dc, p_dc): return inverter.adr(v_dc, p_dc, self.inverter_parameters) @_unwrap_single_value - @_validate_against_arrays('data') def scale_voltage_current_power(self, data): """ Scales the voltage, current, and power of the `data` DataFrame @@ -883,7 +873,7 @@ def scale_voltage_current_power(self, data): scaled_data: DataFrame A scaled copy of the input data. """ - + data = self._validate_per_array(data) return tuple( scale_voltage_current_power(data, voltage=array.modules_per_string, @@ -891,7 +881,7 @@ def scale_voltage_current_power(self, data): for array, data in zip(self._arrays, data) ) - @_validate_against_arrays('g_poa_effective', 'temp_cell') + @_unwrap_single_value def pvwatts_dc(self, g_poa_effective, temp_cell): """ Calcuates DC power according to the PVWatts model using @@ -900,6 +890,8 @@ def pvwatts_dc(self, g_poa_effective, temp_cell): See :py:func:`pvlib.pvsystem.pvwatts_dc` for details. """ + g_poa_effective = self._validate_per_array(g_poa_effective) + temp_cell = self._validate_per_array(temp_cell) return tuple( pvwatts_dc(g_poa_effective, temp_cell, array.module_parameters['pdc0'], diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 3aac44bf9e..aca1a61537 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -42,7 +42,8 @@ def test_PVSystem_multi_array_get_iam(): iam = system.get_iam((1, 5), iam_model='ashrae') assert len(iam) == 2 assert iam[0] != iam[1] - with pytest.raises(ValueError, match="Length mismatch .*"): + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): system.get_iam((1,), iam_model='ashrae') @@ -239,11 +240,10 @@ def test_PVSystem_multi_array_sapm(sapm_module_params): assert sapm_one_flip['p_mp'] == sapm_two['p_mp'] assert sapm_two_flip['p_mp'] == sapm_one['p_mp'] with pytest.raises(ValueError, - match="Length mismatch for parameter temp_cell"): + match="Length mismatch for per-array parameter"): system.sapm(effective_irradiance, 10) with pytest.raises(ValueError, - match="Length mismatch for parameter " - "effective_irradiance"): + match="Length mismatch for per-array parameter"): system.sapm(500, temp_cell) @@ -406,7 +406,8 @@ def two_array_system(pvsyst_module_params, cec_module_params): ((20, 20), (10, 10), 20)]) def test_PVSystem_sapm_effective_irradiance_value_error( poa_direct, poa_diffuse, aoi, two_array_system): - with pytest.raises(ValueError, match="Length mismatch for parameter .*"): + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): two_array_system.sapm_effective_irradiance( poa_direct, poa_diffuse, 10, aoi ) @@ -507,7 +508,7 @@ def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system): def test_PVSystem_multi_array_celltemp_poa_length_mismatch( celltemp, two_array_system): with pytest.raises(ValueError, - match="Length mismatch for parameter poa_global"): + match="Length mismatch for per-array parameter"): celltemp(two_array_system, 1000, 25, 1) @@ -734,7 +735,7 @@ def test_PVSystem_multi_array_calcparams(calcparams, two_array_system): def test_PVSystem_multi_array_calcparams_value_error( calcparams, irrad, celltemp, two_array_system): with pytest.raises(ValueError, - match='Length mismatch for parameter'): + match='Length mismatch for per-array parameter'): calcparams(two_array_system, irrad, celltemp) @@ -1245,7 +1246,7 @@ def test_PVSystem_multi_scale_voltage_current_power(mocker): any_order=True ) with pytest.raises(ValueError, - match="Length mismatch for parameter data"): + match="Length mismatch for per-array parameter"): system.scale_voltage_current_power(None) @@ -1659,25 +1660,24 @@ def test_PVSystem_multiple_array_pvwatts_dc_value_error(): system = pvsystem.PVSystem( arrays=[pvsystem.Array(), pvsystem.Array(), pvsystem.Array()] ) - poa_error_msg = 'Length mismatch for parameter g_poa_effective' - with pytest.raises(ValueError, match=poa_error_msg): + error_message = 'Length mismatch for per-array parameter' + with pytest.raises(ValueError, match=error_message): system.pvwatts_dc(10, (1, 1, 1)) - with pytest.raises(ValueError, match=poa_error_msg): + with pytest.raises(ValueError, match=error_message): system.pvwatts_dc((10, 10), (1, 1, 1)) - with pytest.raises(ValueError, match=poa_error_msg): + with pytest.raises(ValueError, match=error_message): system.pvwatts_dc((10, 10, 10, 10), (1, 1, 1)) - temp_cell_error_msg = 'Length mismatch for parameter temp_cell' - with pytest.raises(ValueError, match=temp_cell_error_msg): + with pytest.raises(ValueError, match=error_message): system.pvwatts_dc((1, 1, 1), 1) - with pytest.raises(ValueError, match=temp_cell_error_msg): + with pytest.raises(ValueError, match=error_message): system.pvwatts_dc((1, 1, 1), (1,)) - with pytest.raises(ValueError, match='Length mismatch for parameter .*'): + with pytest.raises(ValueError, match=error_message): system.pvwatts_dc((1,), 1) - with pytest.raises(ValueError, match='Length mismatch for parameter .*'): + with pytest.raises(ValueError, match=error_message): system.pvwatts_dc((1, 1, 1, 1), (1, 1)) - with pytest.raises(ValueError, match='Length mismatch for parameter .*'): + with pytest.raises(ValueError, match=error_message): system.pvwatts_dc(2, 3) - with pytest.raises(ValueError, match=temp_cell_error_msg): + with pytest.raises(ValueError, match=error_message): # ValueError is raised for non-tuple iterable with correct length system.pvwatts_dc((1, 1, 1), pd.Series([1, 2, 3])) From 09bf16a419807c0741c21a16ded8cfd95ee4344d Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 20 Oct 2020 13:28:55 -0600 Subject: [PATCH 063/236] Remove unused `inspect` import in `pvsystem` --- pvlib/pvsystem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 8d8571816e..31e55ba344 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -5,7 +5,6 @@ from collections import OrderedDict import functools -import inspect import io import os from urllib.request import urlopen From b23d3b5e8ad378812e7e59255e78c3f76be18edf Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 20 Oct 2020 13:29:45 -0600 Subject: [PATCH 064/236] Add blank line before nested defs --- pvlib/pvsystem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 31e55ba344..70af1c2e46 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -655,6 +655,7 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): numeric, values in degrees C. """ poa_global = self._validate_per_array(poa_global) + def build_celltemp_kwargs(array): return {**_build_kwargs(['eta_m', 'alpha_absorption'], array.module_parameters), @@ -730,6 +731,7 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): # default to using the Array attribute, but allow user to # override with a custom surface_tilt value poa_global = self._validate_per_array(poa_global) + def _build_kwargs_fuentes(array): kwargs = {'surface_tilt': array.surface_tilt} temp_model_kwargs = _build_kwargs([ From ed929dd6186d595e8823aaa212b71caa5f9b33da Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 20 Oct 2020 16:47:44 -0600 Subject: [PATCH 065/236] lets see what breaks --- pvlib/tests/test_modelchain.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index bdf7409af8..7af55d7e14 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -812,9 +812,10 @@ def test_bad_get_orientation(): modelchain.get_orientation('bad value') -# tests for PVSystem with multiple Array -def test_with_sapm_pvsystem_arrasy(sapm_dc_snl_ac_system, location, weather): - mc = ModelChain.with_sapm(sapm_dc_snl_ac_system, location) +# tests for PVSystem with multiple Arrays +def test_with_sapm_pvsystem_arrays(sapm_dc_snl_ac_system_Array, location, + weather): + mc = ModelChain.with_sapm(sapm_dc_snl_ac_system_Array, location) assert mc.dc_model == mc.sapm mc.run_model(weather) assert mc.results From 493b704275676ceadab7fce1c08df43a51e70013 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 20 Oct 2020 16:59:27 -0600 Subject: [PATCH 066/236] correct misspelled parameters --- pvlib/tests/test_modelchain.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 7af55d7e14..97d9d24523 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -206,15 +206,15 @@ def sapm_dc_snl_ac_system_Array(sapm_module_params, cec_inverter_parameters, array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=180, albedo=0.2, module=module, module_parameters=module_parameters, - temperature_mode_parameters=temp_model_params, + temperature_model_parameters=temp_model_params, modules_per_string=1, - strings_per_inverter=1) + strings=1) array_two = pvsystem.Array(surface_tilt=15, surface_azimuth=180, albedo=0.2, module=module, module_parameters=module_parameters, - temperature_mode_parameters=temp_model_params, + temperature_model_parameters=temp_model_params, modules_per_string=1, - strings_per_inverter=1) + strings=1) return PVSystem(arrays=[array_one, array_two]) From 7e1f3c17823839acf5a45b9af8c289b4c9a9b825 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 21 Oct 2020 13:58:16 -0600 Subject: [PATCH 067/236] infer methods, handle tuples in calculations --- pvlib/modelchain.py | 85 ++++++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 969fa10cd7..c08f7989c3 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -9,6 +9,7 @@ from functools import partial import warnings import pandas as pd +import numpy as np from dataclasses import dataclass, field from pvlib import (atmosphere, clearsky, inverter, pvsystem, solarposition, @@ -666,7 +667,12 @@ def dc_model(self, model): def infer_dc_model(self): """Infer DC power model from system attributes.""" - params = set(self.system.module_parameters.keys()) + params = np.unique( + [set(a.module_parameters.keys()) for a in self.system._arrays]) + if len(params) > 1: + raise ValueError('PVSystem arrays module_parameters have ' + 'different keys. All arrays should have keys for ' + 'the same DC model.') if {'A0', 'A1', 'C7'} <= params: return self.sapm, 'sapm' elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', @@ -762,7 +768,12 @@ def ac_model(self, model): def infer_ac_model(self): """Infer AC power model from system attributes.""" - inverter_params = set(self.system.inverter_parameters.keys()) + inverter_params = np.unique( + [set(a.inverter_parameters.keys()) for a in self.system._arrays]) + if len(inverter_params) > 1: + raise ValueError('PVSystem arrays inverter_parameters have ' + 'different keys. All arrays should have keys for ' + 'the same AC model.') if {'C0', 'C1', 'C2'} <= inverter_params: return self.snlinverter elif {'ADRCoefficients'} <= inverter_params: @@ -815,7 +826,12 @@ def aoi_model(self, model): self._aoi_model = partial(model, self) def infer_aoi_model(self): - params = set(self.system.module_parameters.keys()) + params = np.unique( + [set(a.module_parameters.keys()) for a in self.system._arrays]) + if len(params) > 1: + raise ValueError('PVSystem arrays module_parameters have ' + 'different keys. All arrays should have keys for ' + 'the same AOI model.') if {'K', 'L', 'n'} <= params: return self.physical_aoi_loss elif {'B5', 'B4', 'B3', 'B2', 'B1', 'B0'} <= params: @@ -879,7 +895,12 @@ def spectral_model(self, model): def infer_spectral_model(self): """Infer spectral model from system attributes.""" - params = set(self.system.module_parameters.keys()) + params = np.unique( + [set(a.module_parameters.keys()) for a in self.system._arrays]) + if len(params) > 1: + raise ValueError('PVSystem arrays module_parameters have ' + 'different keys. All arrays should have keys for ' + 'the same spectral modifier model.') if {'A4', 'A3', 'A2', 'A1', 'A0'} <= params: return self.sapm_spectral_loss elif ((('Technology' in params or @@ -934,16 +955,21 @@ def temperature_model(self, model): 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)) + f'Temperature model {self._temperature_model.__name__} is' + 'inconsistent with PVSystem temperature model parameters' + '{self.system._arrays[0].temperature_model_parameters.keys()}') # noqa: E501 else: self._temperature_model = partial(model, self) def infer_temperature_model(self): """Infer temperature model from system attributes.""" - params = set(self.system.temperature_model_parameters.keys()) + params = np.unique( + [set(a.temperature_model_parameters.keys()) for a in + self.system._arrays]) + if len(params) > 1: + raise ValueError('PVSystem arrays temperature_model_parameters ' + 'have different keys. All arrays should have ' + 'keys for the same cell temperature model.') # remove or statement in v0.9 if {'a', 'b', 'deltaT'} <= params or ( not params and self.system.racking_model is None @@ -956,32 +982,37 @@ def infer_temperature_model(self): elif {'noct_installed'} <= params: return self.fuentes_temp else: - raise ValueError('could not infer temperature model from ' - 'system.temperature_module_parameters {}.' - .format(self.system.temperature_model_parameters)) + raise ValueError(f'could not infer temperature model from ' + 'system.temperature_module_parameters {params}.') + + def _tuple_from_dfs(dfs, name): + ''' Extract a column from each df in dfs, return as tuple of Series + ''' + dfs = tuple(dfs) + return tuple(df[name] for df in dfs) def sapm_temp(self): + poa = self._tuple_from_dfs(self.results.total_irrad, 'poa_global') self.results.cell_temperature = self.system.sapm_celltemp( - self.results.total_irrad['poa_global'], self.weather['temp_air'], - self.weather['wind_speed']) + poa, self.weather['temp_air'], self.weather['wind_speed']) return self def pvsyst_temp(self): + poa = self._tuple_from_dfs(self.results.total_irrad, 'poa_global') self.results.cell_temperature = self.system.pvsyst_celltemp( - self.results.total_irrad['poa_global'], self.weather['temp_air'], - self.weather['wind_speed']) + poa, self.weather['temp_air'], self.weather['wind_speed']) return self def faiman_temp(self): + poa = self._tuple_from_dfs(self.results.total_irrad, 'poa_global') self.results.cell_temperature = self.system.faiman_celltemp( - self.results.total_irrad['poa_global'], self.weather['temp_air'], - self.weather['wind_speed']) + poa, self.weather['temp_air'], self.weather['wind_speed']) return self def fuentes_temp(self): + poa = self._tuple_from_dfs(self.results.total_irrad, 'poa_global') self.results.cell_temperature = self.system.fuentes_celltemp( - self.results.total_irrad['poa_global'], self.weather['temp_air'], - self.weather['wind_speed']) + poa, self.weather['temp_air'], self.weather['wind_speed']) return self @property @@ -1016,11 +1047,15 @@ def no_extra_losses(self): return self def effective_irradiance_model(self): - fd = self.system.module_parameters.get('FD', 1.) - self.results.effective_irradiance = self.results.spectral_modifier * ( - self.results.total_irrad['poa_direct'] * - self.results.aoi_modifier + - fd * self.results.total_irrad['poa_diffuse']) + def _eff_irrad(array, total_irrad, spect_mod, aoi_mod): + fd = array.module_parameters.get('FD', 1.) + return spect_mod * (total_irrad['poa_direct'] * aoi_mod + + fd * total_irrad['poa_diffuse']) + self.effective_irradiance = tuple( + _eff_irrad(array, total_irrad, spect_mod, aoi_mod) for + array, total_irrad, spect_mod, aoi_mod in zip( + self.system._arrays, self.total_irrad, self.spectral_modifier, + self.aoi_modifier)) return self def complete_irradiance(self, weather): From 2da65b461dd7e849c77a33491c4b0c797ddf3be1 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 21 Oct 2020 14:06:06 -0600 Subject: [PATCH 068/236] inverter isn't on array --- pvlib/modelchain.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index c08f7989c3..3eb3ee2d93 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -768,12 +768,7 @@ def ac_model(self, model): def infer_ac_model(self): """Infer AC power model from system attributes.""" - inverter_params = np.unique( - [set(a.inverter_parameters.keys()) for a in self.system._arrays]) - if len(inverter_params) > 1: - raise ValueError('PVSystem arrays inverter_parameters have ' - 'different keys. All arrays should have keys for ' - 'the same AC model.') + inverter_params = set(self.system.inverter_parameters.keys()) if {'C0', 'C1', 'C2'} <= inverter_params: return self.snlinverter elif {'ADRCoefficients'} <= inverter_params: From 8013029b515852d29bd650c5b32cc6206a925982 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 21 Oct 2020 14:21:03 -0600 Subject: [PATCH 069/236] fix iteration in ModelChain.effective_irradiance_model --- pvlib/modelchain.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 3eb3ee2d93..41674a4cf8 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -951,15 +951,15 @@ def temperature_model(self, model): if self._temperature_model.__name__ != name_from_params: raise ValueError( f'Temperature model {self._temperature_model.__name__} is' - 'inconsistent with PVSystem temperature model parameters' - '{self.system._arrays[0].temperature_model_parameters.keys()}') # noqa: E501 + f'inconsistent with PVSystem temperature model parameters' + f'{self.system._arrays[0].temperature_model_parameters.keys()}') # noqa: E501 else: self._temperature_model = partial(model, self) def infer_temperature_model(self): """Infer temperature model from system attributes.""" params = np.unique( - [set(a.temperature_model_parameters.keys()) for a in + [set(a.temperature_model_parameters.keys()) for a in self.system._arrays]) if len(params) > 1: raise ValueError('PVSystem arrays temperature_model_parameters ' @@ -978,7 +978,7 @@ def infer_temperature_model(self): return self.fuentes_temp else: raise ValueError(f'could not infer temperature model from ' - 'system.temperature_module_parameters {params}.') + f'system.temperature_module_parameters {params}.') def _tuple_from_dfs(dfs, name): ''' Extract a column from each df in dfs, return as tuple of Series @@ -1045,12 +1045,15 @@ def effective_irradiance_model(self): def _eff_irrad(array, total_irrad, spect_mod, aoi_mod): fd = array.module_parameters.get('FD', 1.) return spect_mod * (total_irrad['poa_direct'] * aoi_mod + - fd * total_irrad['poa_diffuse']) + fd * total_irrad['poa_diffuse']) + total_irrad = self.system._validate_per_array(self.results.total_irrad) + spect_mod = self.system._validate_per_array( + self.results.spectral_modifier) + aoi_mod = self.system._validate_per_array(self.results.aoi_modifier) self.effective_irradiance = tuple( - _eff_irrad(array, total_irrad, spect_mod, aoi_mod) for - array, total_irrad, spect_mod, aoi_mod in zip( - self.system._arrays, self.total_irrad, self.spectral_modifier, - self.aoi_modifier)) + _eff_irrad(array, ti, sm, am) for + array, ti, sm, am in zip( + self.system._arrays, total_irrad, spect_mod, aoi_mod)) return self def complete_irradiance(self, weather): From 7744a442c0422f5f5aa21da16ddaf62d90e6ea37 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 21 Oct 2020 14:55:27 -0600 Subject: [PATCH 070/236] _tuple_from_dfs to function --- pvlib/modelchain.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 41674a4cf8..a8c25cfa4d 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -980,32 +980,26 @@ def infer_temperature_model(self): raise ValueError(f'could not infer temperature model from ' f'system.temperature_module_parameters {params}.') - def _tuple_from_dfs(dfs, name): - ''' Extract a column from each df in dfs, return as tuple of Series - ''' - dfs = tuple(dfs) - return tuple(df[name] for df in dfs) - def sapm_temp(self): - poa = self._tuple_from_dfs(self.results.total_irrad, 'poa_global') + poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') self.results.cell_temperature = self.system.sapm_celltemp( poa, self.weather['temp_air'], self.weather['wind_speed']) return self def pvsyst_temp(self): - poa = self._tuple_from_dfs(self.results.total_irrad, 'poa_global') + poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') self.results.cell_temperature = self.system.pvsyst_celltemp( poa, self.weather['temp_air'], self.weather['wind_speed']) return self def faiman_temp(self): - poa = self._tuple_from_dfs(self.results.total_irrad, 'poa_global') + poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') self.results.cell_temperature = self.system.faiman_celltemp( poa, self.weather['temp_air'], self.weather['wind_speed']) return self def fuentes_temp(self): - poa = self._tuple_from_dfs(self.results.total_irrad, 'poa_global') + poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') self.results.cell_temperature = self.system.fuentes_celltemp( poa, self.weather['temp_air'], self.weather['wind_speed']) return self @@ -1515,3 +1509,10 @@ def run_model_from_effective_irradiance(self, data=None): self._run_from_effective_irrad(data) return self + + +def _tuple_from_dfs(dfs, name): + ''' Extract a column from each df in dfs, return as tuple of Series + ''' + dfs = tuple(dfs) + return tuple(df[name] for df in dfs) From 0606eb53ad1be2e3cb81674deb9fefc90de0fb80 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 21 Oct 2020 21:06:32 -0600 Subject: [PATCH 071/236] use isinstance for dataframe vs. tuple of df --- pvlib/modelchain.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index a8c25cfa4d..46e0b06d26 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1040,14 +1040,18 @@ def _eff_irrad(array, total_irrad, spect_mod, aoi_mod): fd = array.module_parameters.get('FD', 1.) return spect_mod * (total_irrad['poa_direct'] * aoi_mod + fd * total_irrad['poa_diffuse']) - total_irrad = self.system._validate_per_array(self.results.total_irrad) - spect_mod = self.system._validate_per_array( - self.results.spectral_modifier) - aoi_mod = self.system._validate_per_array(self.results.aoi_modifier) - self.effective_irradiance = tuple( - _eff_irrad(array, ti, sm, am) for - array, ti, sm, am in zip( - self.system._arrays, total_irrad, spect_mod, aoi_mod)) + if isinstance(self.results.total_irrad, tuple): + self.effective_irradiance = tuple( + _eff_irrad(array, ti, sm, am) for + array, ti, sm, am in zip( + self.system._arrays, self.results.total_irrad, + self.results.spectral_modifier, self.results.aoi_modifier)) + else: + fd = self.system.module_parameters.get('FD', 1.) + self.effective_irradiance = self.results.spectral_modifier * \ + (self.results.total_irrad['poa_direct'] * \ + self.results.aoi_modifier + + fd * self.results.total_irrad['poa_diffuse']) return self def complete_irradiance(self, weather): @@ -1512,7 +1516,10 @@ def run_model_from_effective_irradiance(self, data=None): def _tuple_from_dfs(dfs, name): - ''' Extract a column from each df in dfs, return as tuple of Series + ''' Extract a column from each df in dfs, return as Series or tuple of + Series ''' - dfs = tuple(dfs) - return tuple(df[name] for df in dfs) + if isinstance(dfs, tuple): + return tuple(df[name] for df in dfs) + else: + return dfs[name] \ No newline at end of file From 4231205aaf56c483043a190154139482b3489f72 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 22 Oct 2020 10:44:22 -0600 Subject: [PATCH 072/236] consolidate check for consistent parameters --- pvlib/modelchain.py | 50 ++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 46e0b06d26..665bbf9afb 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -376,6 +376,9 @@ def __init__(self, system, location, self.solar_position_method = solar_position_method self.airmass_model = airmass_model + # check that every array has parameters for the same models + self._check_consistent_params + # calls setters self.dc_model = dc_model self.ac_model = ac_model @@ -629,6 +632,22 @@ def orientation_strategy(self, strategy): self._orientation_strategy = strategy + def _check_consistent_params(self): + # check consistent module_parameters + params = np.unique( + [set(a.module_parameters.keys()) for a in self.system._arrays]) + if len(params) > 1: + raise ValueError('PVSystem arrays have module_parameters with' + 'different keys.') + # check consistent temperature_model_parameters + params = np.unique( + [set(a.temperature_model_parameters.keys()) for a in + self.system._arrays]) + if len(params) > 1: + raise ValueError('PVSystem arrays temperature_model_parameters ' + 'have different keys. All arrays should have ' + 'keys for the same cell temperature model.') + @property def dc_model(self): return self._dc_model @@ -666,13 +685,8 @@ def dc_model(self, model): self._dc_model = partial(model, self) def infer_dc_model(self): - """Infer DC power model from system attributes.""" - params = np.unique( - [set(a.module_parameters.keys()) for a in self.system._arrays]) - if len(params) > 1: - raise ValueError('PVSystem arrays module_parameters have ' - 'different keys. All arrays should have keys for ' - 'the same DC model.') + """Infer DC power model from array module parameters.""" + params = self.system._arrays[0].module_parameters if {'A0', 'A1', 'C7'} <= params: return self.sapm, 'sapm' elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', @@ -821,12 +835,7 @@ def aoi_model(self, model): self._aoi_model = partial(model, self) def infer_aoi_model(self): - params = np.unique( - [set(a.module_parameters.keys()) for a in self.system._arrays]) - if len(params) > 1: - raise ValueError('PVSystem arrays module_parameters have ' - 'different keys. All arrays should have keys for ' - 'the same AOI model.') + params = self.system._arrays[0].module_parameters if {'K', 'L', 'n'} <= params: return self.physical_aoi_loss elif {'B5', 'B4', 'B3', 'B2', 'B1', 'B0'} <= params: @@ -890,12 +899,7 @@ def spectral_model(self, model): def infer_spectral_model(self): """Infer spectral model from system attributes.""" - params = np.unique( - [set(a.module_parameters.keys()) for a in self.system._arrays]) - if len(params) > 1: - raise ValueError('PVSystem arrays module_parameters have ' - 'different keys. All arrays should have keys for ' - 'the same spectral modifier model.') + params = self.system._arrays[0].module_parameters if {'A4', 'A3', 'A2', 'A1', 'A0'} <= params: return self.sapm_spectral_loss elif ((('Technology' in params or @@ -958,13 +962,7 @@ def temperature_model(self, model): def infer_temperature_model(self): """Infer temperature model from system attributes.""" - params = np.unique( - [set(a.temperature_model_parameters.keys()) for a in - self.system._arrays]) - if len(params) > 1: - raise ValueError('PVSystem arrays temperature_model_parameters ' - 'have different keys. All arrays should have ' - 'keys for the same cell temperature model.') + params = self.system._arrays[0].temperature_model_parameters # remove or statement in v0.9 if {'a', 'b', 'deltaT'} <= params or ( not params and self.system.racking_model is None From 6611f571b81fa26954d4efc72b6cb48fecadb7f0 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 22 Oct 2020 10:49:55 -0600 Subject: [PATCH 073/236] missing set() --- pvlib/modelchain.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 665bbf9afb..95af576540 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -686,7 +686,7 @@ def dc_model(self, model): def infer_dc_model(self): """Infer DC power model from array module parameters.""" - params = self.system._arrays[0].module_parameters + params = set(self.system._arrays[0].module_parameters) if {'A0', 'A1', 'C7'} <= params: return self.sapm, 'sapm' elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', @@ -835,7 +835,7 @@ def aoi_model(self, model): self._aoi_model = partial(model, self) def infer_aoi_model(self): - params = self.system._arrays[0].module_parameters + params = set(self.system._arrays[0].module_parameters) if {'K', 'L', 'n'} <= params: return self.physical_aoi_loss elif {'B5', 'B4', 'B3', 'B2', 'B1', 'B0'} <= params: @@ -899,7 +899,7 @@ def spectral_model(self, model): def infer_spectral_model(self): """Infer spectral model from system attributes.""" - params = self.system._arrays[0].module_parameters + params = set(self.system._arrays[0].module_parameters) if {'A4', 'A3', 'A2', 'A1', 'A0'} <= params: return self.sapm_spectral_loss elif ((('Technology' in params or @@ -962,7 +962,7 @@ def temperature_model(self, model): def infer_temperature_model(self): """Infer temperature model from system attributes.""" - params = self.system._arrays[0].temperature_model_parameters + params = set(self.system._arrays[0].temperature_model_parameters) # remove or statement in v0.9 if {'a', 'b', 'deltaT'} <= params or ( not params and self.system.racking_model is None From 3e4871c260a88fe6b53363ad64b73ebd7ba62f3e Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 22 Oct 2020 11:02:35 -0600 Subject: [PATCH 074/236] missed one instance --- pvlib/modelchain.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 95af576540..9efd884c92 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -663,8 +663,9 @@ def dc_model(self, model): model = model.lower() if model in _DC_MODEL_PARAMS.keys(): # validate module parameters - missing_params = (_DC_MODEL_PARAMS[model] - - set(self.system.module_parameters.keys())) + missing_params = ( + _DC_MODEL_PARAMS[model] - + set(self.system._arrays[0].module_parameters.keys())) if missing_params: # some parameters are not in module.keys() raise ValueError(model + ' selected for the DC model but ' 'one or more required parameters are ' @@ -1042,12 +1043,12 @@ def _eff_irrad(array, total_irrad, spect_mod, aoi_mod): self.effective_irradiance = tuple( _eff_irrad(array, ti, sm, am) for array, ti, sm, am in zip( - self.system._arrays, self.results.total_irrad, + self.system._arrays, self.results.total_irrad, self.results.spectral_modifier, self.results.aoi_modifier)) else: fd = self.system.module_parameters.get('FD', 1.) self.effective_irradiance = self.results.spectral_modifier * \ - (self.results.total_irrad['poa_direct'] * \ + (self.results.total_irrad['poa_direct'] * self.results.aoi_modifier + fd * self.results.total_irrad['poa_diffuse']) return self @@ -1520,4 +1521,4 @@ def _tuple_from_dfs(dfs, name): if isinstance(dfs, tuple): return tuple(df[name] for df in dfs) else: - return dfs[name] \ No newline at end of file + return dfs[name] From 75195b6b5380606a00df77d2b37cdeceb2c32f8e Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 26 Oct 2020 15:15:15 -0600 Subject: [PATCH 075/236] Rename and document kwarg to prevent unwrapping return value --- pvlib/pvsystem.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 70af1c2e46..9aa40c0416 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -74,12 +74,15 @@ def _unwrap_single_value(func): If the length of the iterable returned by `func` is 1, then the single member of the iterable is returned. If the length is greater than 1, then entire iterable is returned. + + Adds 'unwrap' as a keyword argument that can be set to False + to force the return value to be a tuple, regardless of its length. """ @functools.wraps(func) def f(*args, **kwargs): - return_list = kwargs.pop('return_list', False) + unwrap = kwargs.pop('unwrap', True) x = func(*args, **kwargs) - if len(x) == 1 and not return_list: + if unwrap and len(x) == 1: return x[0] return x return f From 9ce66a475ead646d763664590d92628cc78c200e Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 27 Oct 2020 10:02:35 -0600 Subject: [PATCH 076/236] Test ModelChain with multi-array system and no losses Add a test that verifies the dc side of the ModelChain returns the correct data for each Array with `aoi_model='no_loss'` and `spectral_model='no_loss'`. Test fails because `ModelChain.results.spectral_modifier` and `ModelChain.results.aoi_modifier` are assigned a scalar value instead of a tuple. It is very likely we will also see a failure at the `ModelChain.losses_model()` call in `_run_from_effective_irradiance()` for the same reason. At this point we should (maybe) expect a failure at the call to `ModelChain.ac_model()` since we have not implemented an inverter model for multiple arrays yet. --- pvlib/tests/test_modelchain.py | 71 +++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 97d9d24523..a0a3c10bf0 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -250,7 +250,6 @@ def test_orientation_strategy(strategy, expected, sapm_dc_snl_ac_system, assert sapm_dc_snl_ac_system.surface_tilt == expected[0] assert sapm_dc_snl_ac_system.surface_azimuth == expected[1] - def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location) times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') @@ -262,6 +261,76 @@ def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location): index=times) assert_series_equal(ac, expected) +@pytest.fixture(scope='function') +def multi_array_pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m): + module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} + temp_model_parameters = sapm_temperature_cs5p_220m.copy() + inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} + array_one = pvsystem.Array( + surface_tilt=32.2, surface_azimuth=180, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_parameters + ) + array_two = pvsystem.Array( + surface_tilt=32.2, surface_azimuth=220, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_parameters + ) + two_array_system = PVSystem( + arrays=[array_one, array_two], + inverter_parameters=inverter_parameters + ) + array_one_system = PVSystem( + arrays=[array_one], + inverter_parameters=inverter_parameters + ) + array_two_system = PVSystem( + arrays=[array_two], + inverter_parameters=inverter_parameters + ) + return {'two_array_system': two_array_system, + 'array_one_system': array_one_system, + 'array_two_system': array_two_system} + + +def test_run_model_from_irradiance_arrays_no_loss( + multi_array_pvwatts_dc_pvwatts_ac_system, location): + mc_both = ModelChain( + multi_array_pvwatts_dc_pvwatts_ac_system['two_array_system'], + location, + aoi_model='no_loss', + spectral_model='no_loss', + losses_model='no_loss' + ) + mc_one = ModelChain( + multi_array_pvwatts_dc_pvwatts_ac_system['array_one_system'], + location, + aoi_model='no_loss', + spectral_model='no_loss', + losses_model='no_loss' + ) + mc_two = ModelChain( + multi_array_pvwatts_dc_pvwatts_ac_system['array_two_system'], + location, + aoi_model='no_loss', + spectral_model='no_loss', + losses_model='no_loss' + ) + times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') + irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, + index=times) + mc_one.run_model(irradiance) + mc_two.run_model(irradiance) + mc_both.run_model(irradiance) + assert_series_equal( + mc_both.results.dc[0], + mc_one.results.dc + ) + assert_series_equal( + mc_both.results.dc[1], + mc_two.results.dc + ) + def test_prepare_inputs_no_irradiance(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location) From e053674a46c88dd91e7877470f87eefc88fa688a Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 2 Nov 2020 11:18:40 -0700 Subject: [PATCH 077/236] Initialize losses parameters corresponding to system._arrays In the no_losses case, loss parameters are initialized to 1. This commit initializes a tuple with the same number of elements as the number of arrays in the system. In cases where losses are being modeled, these fields are initialized to the result of a PVSystem method call, meaning they have the correct type and shape. For cases with no losses we need to take care to manually initialize them correctly. After this commit test_run_model_from_irradiance_arrays_no_loss() fails at the call to PVSystem.pvwatts_ac() which is expected since we have not implemented inverter models with multiple DC inputs yet. --- pvlib/modelchain.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 9efd884c92..5b8abd493e 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -874,7 +874,11 @@ def martin_ruiz_aoi_loss(self): return self def no_aoi_loss(self): - self.results.aoi_modifier = 1.0 + num_arrays = len(self.system._arrays) + if num_arrays == 1: + self.results.aoi_modifier = 1.0 + else: + self.results.aoi_modifier = (1.0,) * num_arrays return self @property @@ -928,7 +932,11 @@ def sapm_spectral_loss(self): return self def no_spectral_loss(self): - self.results.spectral_modifier = 1 + num_arrays = len(self.system._arrays) + if num_arrays == 1: + self.results.spectral_modifier = 1 + else: + self.results.spectral_modifier = (1,) * num_arrays return self @property @@ -1027,6 +1035,7 @@ def infer_losses_model(self): def pvwatts_losses(self): self.losses = (100 - self.system.pvwatts_losses()) / 100. + # TODO handle multiple arrays self.results.dc *= self.losses return self @@ -1201,6 +1210,7 @@ def _assign_weather(self, data): def _assign_total_irrad(self, data): key_list = [k for k in POA_KEYS if k in data] + # TODO multiple arrays self.results.total_irrad = data[key_list].copy() return self @@ -1336,6 +1346,7 @@ def _prepare_temperature(self, data=None): """ if 'cell_temperature' in data: + # TODO replicate len(self.system._arrays) times ??? self.results.cell_temperature = data['cell_temperature'] return self @@ -1349,7 +1360,9 @@ def _prepare_temperature(self, data=None): self.results.cell_temperature = \ pvlib.temperature.sapm_cell_from_module( module_temperature=data['module_temperature'], + # TODO handle multiple poa irradiance (multiple arrays) poa_global=self.results.total_irrad['poa_global'], + # TODO handle multiple temperature models deltaT=self.system.temperature_model_parameters['deltaT']) return self @@ -1508,6 +1521,8 @@ def run_model_from_effective_irradiance(self, data=None): self._assign_weather(data) self._assign_total_irrad(data) + # TODO handle multiple irradiances (? replicat len(self.system._arrays) + # times? self.results.effective_irradiance = data['effective_irradiance'] self._run_from_effective_irrad(data) From e33a8604f916d680e8dfe2cd2ea78485b403425d Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 2 Nov 2020 11:21:41 -0700 Subject: [PATCH 078/236] Fix whitespace --- pvlib/tests/test_modelchain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index a0a3c10bf0..a095f76c80 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -261,6 +261,7 @@ def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location): index=times) assert_series_equal(ac, expected) + @pytest.fixture(scope='function') def multi_array_pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m): module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} From 1abc17f4bcedcc3cbc4784e7c3f0a085d60cc07a Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 2 Nov 2020 12:10:05 -0700 Subject: [PATCH 079/236] Add PVSystem.num_arrays property --- pvlib/pvsystem.py | 5 +++++ pvlib/tests/test_pvsystem.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 9aa40c0416..4238d692c1 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1030,6 +1030,11 @@ def modules_per_string(self): def strings_per_inverter(self): return tuple(array.strings for array in self._arrays) + @property + def num_arrays(self): + """The number of Arrays in the system.""" + return len(self._arrays) + @deprecated('0.8', alternative='PVSystem, Location, and ModelChain', name='LocalizedPVSystem', removal='0.9') diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index aca1a61537..70d8ca3f13 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1712,6 +1712,13 @@ def test_PVSystem_pvwatts_ac_kwargs(mocker): assert out < pdc +def test_PVSystem_num_arrays(): + system_one = pvsystem.PVSystem() + system_two = pvsystem.PVSystem(arrays=[pvsystem.Array(), pvsystem.Array()]) + assert system_one.num_arrays == 1 + assert system_two.num_arrays == 2 + + def test_combine_loss_factors(): test_index = pd.date_range(start='1990/01/01T12:00', periods=365, freq='D') loss_1 = pd.Series(.10, index=test_index) From d4a82b1db4272e42b27d40ef9cf4a0d8ad561b31 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 2 Nov 2020 12:15:37 -0700 Subject: [PATCH 080/236] Use self.system.num_arrays instead of len(self.system._arrays) --- pvlib/modelchain.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 5b8abd493e..5b4557e41c 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -874,11 +874,10 @@ def martin_ruiz_aoi_loss(self): return self def no_aoi_loss(self): - num_arrays = len(self.system._arrays) - if num_arrays == 1: + if self.system.num_arrays == 1: self.results.aoi_modifier = 1.0 else: - self.results.aoi_modifier = (1.0,) * num_arrays + self.results.aoi_modifier = (1.0,) * self.system.num_arrays return self @property @@ -932,11 +931,10 @@ def sapm_spectral_loss(self): return self def no_spectral_loss(self): - num_arrays = len(self.system._arrays) - if num_arrays == 1: + if self.system.num_arrays == 1: self.results.spectral_modifier = 1 else: - self.results.spectral_modifier = (1,) * num_arrays + self.results.spectral_modifier = (1,) * self.system.num_arrays return self @property @@ -1346,7 +1344,7 @@ def _prepare_temperature(self, data=None): """ if 'cell_temperature' in data: - # TODO replicate len(self.system._arrays) times ??? + # TODO replicate self.system.num_arrays times ??? self.results.cell_temperature = data['cell_temperature'] return self @@ -1521,8 +1519,8 @@ def run_model_from_effective_irradiance(self, data=None): self._assign_weather(data) self._assign_total_irrad(data) - # TODO handle multiple irradiances (? replicat len(self.system._arrays) - # times? + # TODO handle multiple irradiances (replicate self.system.num_arrays + # times?) self.results.effective_irradiance = data['effective_irradiance'] self._run_from_effective_irrad(data) From 070f628b149bf43866670bcfc988ae92232b4781 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 2 Nov 2020 13:44:23 -0700 Subject: [PATCH 081/236] Correct types of ModelChainResult fields Declares a PerArray type which is either a single value or a tuple of many values with the same type. The type for every field that should have the same length as the number of arrays is .Optional[PerArray[X]] --- pvlib/modelchain.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 5b4557e41c..48adbfb504 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -11,6 +11,7 @@ import pandas as pd import numpy as np from dataclasses import dataclass, field +from typing import Union, Tuple, Optional, TypeVar from pvlib import (atmosphere, clearsky, inverter, pvsystem, solarposition, temperature, tools) @@ -264,22 +265,24 @@ def get_orientation(strategy, **kwargs): @dataclass class ModelChainResult: + T = TypeVar('T') + PerArray = Union[T, Tuple[T, ...]] # system-level information # weather: pd.DataFrame = field(default=None) solar_position: pd.DataFrame = field(default=None) airmass: pd.DataFrame = field(default=None) ac: pd.Series = field(default=None) # per DC array information - total_irrad: pd.DataFrame = field(default=None) - aoi: pd.Series = field(default=None) - aoi_modifier: pd.Series = field(default=None) - spectral_modifier: pd.Series = field(default=None) - cell_temperature: pd.Series = field(default=None) - effective_irradiance: pd.Series = field(default=None) - dc: pd.Series = field(default=None) + total_irrad: Optional[PerArray[pd.DataFrame]] = field(default=None) + aoi: Optional[PerArray[pd.Series]] = field(default=None) + aoi_modifier: Optional[PerArray[pd.Series]] = field(default=None) + spectral_modifier: Optional[PerArray[pd.Series]] = field(default=None) + cell_temperature: Optional[PerArray[pd.Series]] = field(default=None) + effective_irradiance: Optional[PerArray[pd.Series]] = field(default=None) + dc: Optional[PerArray[pd.Series]] = field(default=None) # losses: dont_know_tye_type = field(default=None) - array_ac: pd.Series = field(default=None) - diode_params: pd.DataFrame = field(default=None) + array_ac: Optional[PerArray[pd.Series]] = field(default=None) + diode_params: Optional[PerArray[pd.DataFrame]] = field(default=None) class ModelChain: @@ -1358,10 +1361,10 @@ def _prepare_temperature(self, data=None): self.results.cell_temperature = \ pvlib.temperature.sapm_cell_from_module( module_temperature=data['module_temperature'], - # TODO handle multiple poa irradiance (multiple arrays) - poa_global=self.results.total_irrad['poa_global'], - # TODO handle multiple temperature models - deltaT=self.system.temperature_model_parameters['deltaT']) + poa_global=_tuple_from_dfs( + self.results.total_irrad, 'poa_global'), + deltaT=_tuple_from_dfs( + self.system.temperature_model_parameters, 'deltaT')) return self # Calculate cell temperature from weather data. Cell temperature models From a64d8b1ed2ae49f00845e31d8b525e2bcd518242 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 2 Nov 2020 14:37:13 -0700 Subject: [PATCH 082/236] Remove direct uses of ModelChain.system._arrays Preserve encapsulation. Add an function _array_keys() that is similar to _tuple_from_dfs in order to transparently handle the different types returned by PVSystem attributes. --- pvlib/modelchain.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 48adbfb504..dbeb94d397 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -638,14 +638,16 @@ def orientation_strategy(self, strategy): def _check_consistent_params(self): # check consistent module_parameters params = np.unique( - [set(a.module_parameters.keys()) for a in self.system._arrays]) + [set(module_parameters.keys()) + for module_parameters in self.system.module_parameters]) if len(params) > 1: raise ValueError('PVSystem arrays have module_parameters with' 'different keys.') # check consistent temperature_model_parameters params = np.unique( - [set(a.temperature_model_parameters.keys()) for a in - self.system._arrays]) + [set(temperature_model_parameters.keys()) + for temperature_model_parameters + in self.system.temperature_model_parameters]) if len(params) > 1: raise ValueError('PVSystem arrays temperature_model_parameters ' 'have different keys. All arrays should have ' @@ -668,7 +670,7 @@ def dc_model(self, model): # validate module parameters missing_params = ( _DC_MODEL_PARAMS[model] - - set(self.system._arrays[0].module_parameters.keys())) + _array_keys(self.system.module_parameters, 0)) if missing_params: # some parameters are not in module.keys() raise ValueError(model + ' selected for the DC model but ' 'one or more required parameters are ' @@ -690,7 +692,7 @@ def dc_model(self, model): def infer_dc_model(self): """Infer DC power model from array module parameters.""" - params = set(self.system._arrays[0].module_parameters) + params = _array_keys(self.system.module_parameters, 0) if {'A0', 'A1', 'C7'} <= params: return self.sapm, 'sapm' elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', @@ -839,7 +841,7 @@ def aoi_model(self, model): self._aoi_model = partial(model, self) def infer_aoi_model(self): - params = set(self.system._arrays[0].module_parameters) + params = _array_keys(self.system.module_parameters, 0) if {'K', 'L', 'n'} <= params: return self.physical_aoi_loss elif {'B5', 'B4', 'B3', 'B2', 'B1', 'B0'} <= params: @@ -906,7 +908,7 @@ def spectral_model(self, model): def infer_spectral_model(self): """Infer spectral model from system attributes.""" - params = set(self.system._arrays[0].module_parameters) + params = _array_keys(self.system.module_parameters, 0) if {'A4', 'A3', 'A2', 'A1', 'A0'} <= params: return self.sapm_spectral_loss elif ((('Technology' in params or @@ -966,13 +968,13 @@ def temperature_model(self, model): raise ValueError( f'Temperature model {self._temperature_model.__name__} is' f'inconsistent with PVSystem temperature model parameters' - f'{self.system._arrays[0].temperature_model_parameters.keys()}') # noqa: E501 + f'{_array_keys(self.system.temperature_model_parameters, 0)}') # noqa: E501 else: self._temperature_model = partial(model, self) def infer_temperature_model(self): """Infer temperature model from system attributes.""" - params = set(self.system._arrays[0].temperature_model_parameters) + params = _array_keys(self.system.temperature_model_parameters, 0) # remove or statement in v0.9 if {'a', 'b', 'deltaT'} <= params or ( not params and self.system.racking_model is None @@ -1045,15 +1047,15 @@ def no_extra_losses(self): return self def effective_irradiance_model(self): - def _eff_irrad(array, total_irrad, spect_mod, aoi_mod): - fd = array.module_parameters.get('FD', 1.) + def _eff_irrad(module_parameters, total_irrad, spect_mod, aoi_mod): + fd = module_parameters.get('FD', 1.) return spect_mod * (total_irrad['poa_direct'] * aoi_mod + fd * total_irrad['poa_diffuse']) if isinstance(self.results.total_irrad, tuple): self.effective_irradiance = tuple( _eff_irrad(array, ti, sm, am) for array, ti, sm, am in zip( - self.system._arrays, self.results.total_irrad, + self.system.module_parameters, self.results.total_irrad, self.results.spectral_modifier, self.results.aoi_modifier)) else: fd = self.system.module_parameters.get('FD', 1.) @@ -1530,6 +1532,14 @@ def run_model_from_effective_irradiance(self, data=None): return self +def _array_keys(dicts, array): + """Return a set of keys from element `array` of `dicts` if it is a tuple + otherwise return the set of keys in dicts.""" + if isinstance(dicts, tuple): + return set(dicts[array]) + return set(dicts) + + def _tuple_from_dfs(dfs, name): ''' Extract a column from each df in dfs, return as Series or tuple of Series From 337675353c9c7ae8e83c166c0460f12951d470a6 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 2 Nov 2020 14:58:35 -0700 Subject: [PATCH 083/236] Assign to self.results.effective_irradiance Don't assign to the deprecated self.effective_irradiance field internally. --- pvlib/modelchain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index dbeb94d397..48ecd72a33 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1052,14 +1052,14 @@ def _eff_irrad(module_parameters, total_irrad, spect_mod, aoi_mod): return spect_mod * (total_irrad['poa_direct'] * aoi_mod + fd * total_irrad['poa_diffuse']) if isinstance(self.results.total_irrad, tuple): - self.effective_irradiance = tuple( + self.results.effective_irradiance = tuple( _eff_irrad(array, ti, sm, am) for array, ti, sm, am in zip( self.system.module_parameters, self.results.total_irrad, self.results.spectral_modifier, self.results.aoi_modifier)) else: fd = self.system.module_parameters.get('FD', 1.) - self.effective_irradiance = self.results.spectral_modifier * \ + self.results.effective_irradiance = self.results.spectral_modifier * \ (self.results.total_irrad['poa_direct'] * self.results.aoi_modifier + fd * self.results.total_irrad['poa_diffuse']) From 4abaf2b84e3dc7b5138b7e051865b3d6e4d75a1f Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 2 Nov 2020 15:34:00 -0700 Subject: [PATCH 084/236] Test multi-array ModelChain with pvwatts losses --- pvlib/tests/test_modelchain.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index a095f76c80..6aeb24ea3a 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -818,6 +818,23 @@ def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather, assert not np.allclose(mc.results.dc, dc_with_loss, equal_nan=True) +def test_losses_models_pvwatts_arrays(multi_array_pvwatts_dc_pvwatts_ac_system, + location, weather): + age = 1 + system_both = multi_array_pvwatts_dc_pvwatts_ac_system['two_array_system'] + system_both.losses_parameters = dict(age=age) + mc = ModelChain(system_both, location, dc_model='pvwatts', + aoi_model='no_loss', spectral_model='no_loss', + losses_model='pvwatts') + mc.run_model(weather) + dc_with_loss = mc.results.dc + mc = ModelChain(system_both, location, dc_model='pvwatts', + aoi_model='no_loss', spectral_model='no_loss', + losses_model='pvwatts') + mc.run_model(weather) + assert not np.allclose(mc.results.dc, dc_with_loss, equal_nan=True) + + def test_losses_models_ext_def(pvwatts_dc_pvwatts_ac_system, location, weather, mocker): m = mocker.spy(sys.modules[__name__], 'constant_losses') From b4ace32d815cdb7dbe7b62ef8b6e3b6304896ccb Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 2 Nov 2020 15:34:19 -0700 Subject: [PATCH 085/236] Shorten line --- pvlib/modelchain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 48ecd72a33..19eb33ca49 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1059,7 +1059,8 @@ def _eff_irrad(module_parameters, total_irrad, spect_mod, aoi_mod): self.results.spectral_modifier, self.results.aoi_modifier)) else: fd = self.system.module_parameters.get('FD', 1.) - self.results.effective_irradiance = self.results.spectral_modifier * \ + self.results.effective_irradiance = \ + self.results.spectral_modifier * \ (self.results.total_irrad['poa_direct'] * self.results.aoi_modifier + fd * self.results.total_irrad['poa_diffuse']) From cf8fd8710d1be33abe27957f796618e97887894b Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 3 Nov 2020 13:14:55 -0700 Subject: [PATCH 086/236] Test multi-array modelchain with DC models that call _singlediode --- pvlib/tests/test_modelchain.py | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 6aeb24ea3a..7f74727e36 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -46,6 +46,33 @@ def cec_dc_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters, return system +@pytest.fixture +def cec_dc_snl_ac_arrays(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() + array_one = pvsystem.Array( + surface_tilt=32.2, surface_azimuth=180, + module=module_parameters['Name'], + module_parameters=module_parameters.copy(), + temperature_model_parameters=temp_model_params.copy() + ) + array_two = pvsystem.Array( + surface_tilt=42.2, surface_azimuth=220, + module=module_parameters['Name'], + module_parameters=module_parameters.copy(), + temperature_model_parameters=temp_model_params.copy() + ) + system = PVSystem( + arrays=[array_one, array_two], + inverter_parameters = cec_inverter_parameters + ) + return system + + @pytest.fixture def cec_dc_native_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters, sapm_temperature_cs5p_220m): @@ -74,6 +101,32 @@ def pvsyst_dc_snl_ac_system(pvsyst_module_params, cec_inverter_parameters, return system +@pytest.fixture +def pvsyst_dc_snl_ac_arrays(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() + array_one = pvsystem.Array( + surface_tilt=32.2, surface_azimuth=180, + module=module, + module_parameters=module_parameters.copy(), + temperature_model_parameters=temp_model_params.copy() + ) + array_two = pvsystem.Array( + surface_tilt=42.2, surface_azimuth=220, + module=module, + module_parameters=module_parameters.copy(), + temperature_model_parameters=temp_model_params.copy() + ) + system = PVSystem( + arrays=[array_one, array_two], + inverter_parameters=cec_inverter_parameters + ) + return system + + @pytest.fixture def cec_dc_adr_ac_system(sam_data, cec_module_cs5p_220m, sapm_temperature_cs5p_220m): @@ -585,6 +638,36 @@ def test_infer_dc_model(sapm_dc_snl_ac_system, cec_dc_snl_ac_system, assert isinstance(mc.results.dc, (pd.Series, pd.DataFrame)) +@pytest.mark.parametrize('dc_model', ['cec', 'desoto', 'pvsyst']) +def test_singlediode_dc_arrays(location, dc_model, + cec_dc_snl_ac_arrays, + pvsyst_dc_snl_ac_arrays, + weather): + systems = {'cec': cec_dc_snl_ac_arrays, + 'pvsyst': pvsyst_dc_snl_ac_arrays, + 'desoto': cec_dc_snl_ac_arrays} + temp_sapm = {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3} + temp_pvsyst = {'u_c': 29.0, 'u_v': 0} + temp_model_params = {'cec': temp_sapm, + 'desoto': temp_sapm, + 'pvsyst': temp_pvsyst} + temp_model = {'cec': 'sapm', 'desoto': 'sapm', 'pvsyst': 'pvsyst'} + system = systems[dc_model] + system.temperature_model_parameters = temp_model_params[dc_model] + if dc_model == 'desoto': + for module_parameters in system.module_parameters: + module_parameters.pop('Adjust') + mc = ModelChain(system, location, + aoi_model='no_loss', spectral_model='no_loss', + temperature_model=temp_model[dc_model]) + mc.run_model(weather) + assert isinstance(mc.results.dc, tuple) + assert len(mc.results.dc) == system.num_arrays + for dc in mc.results.dc: + assert isinstance(dc, (pd.Series, pd.DataFrame)) + + + @pytest.mark.parametrize('dc_model', ['sapm', 'cec', 'cec_native']) def test_infer_spectral_model(location, sapm_dc_snl_ac_system, cec_dc_snl_ac_system, From e666052ba49519db654f86da27c3aeee11339619 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 3 Nov 2020 14:52:37 -0700 Subject: [PATCH 087/236] Rewrite ModelChain._singlediode to support multi-array systems Slightly different logic depending on the number of arrays. This could be done a little more cleanly with the `unwrap=False` kwarg to `calcparams_model_function` and `system.scale_voltage_current_power`. That would give us a consistent type to work with, requiring only one if statement to unwrap dc power ourselves if `system.num_arrays == 1`; however, `mocker.spy` attempts to bind unwrap explicitly resulting in a test failure where there should not be one, since unwrap is an extra, non-explicit, parameter added by the `_unwrap_single_value` decorator. --- pvlib/modelchain.py | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 19eb33ca49..ba288c3d98 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -721,23 +721,35 @@ def sapm(self): return self def _singlediode(self, calcparams_model_function): - (photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth) = ( - calcparams_model_function(self.results.effective_irradiance, - self.results.cell_temperature)) - - self.results.diode_params = pd.DataFrame( - {'I_L': photocurrent, 'I_o': saturation_current, - 'R_s': resistance_series, 'R_sh': resistance_shunt, - 'nNsVth': nNsVth}) - - self.results.dc = self.system.singlediode( - photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth) - + def _make_diode_params(photocurrent, saturation_current, + resistance_series, resistance_shunt, + nNsVth): + return pd.DataFrame( + {'I_L': photocurrent, 'I_o': saturation_current, + 'R_s': resistance_series, 'R_sh': resistance_shunt, + 'nNsVth': nNsVth} + ) + params = calcparams_model_function(self.results.effective_irradiance, + self.results.cell_temperature) + if self.system.num_arrays == 1: + self.results.diode_params = _make_diode_params(*params) + self.results.dc = self.system.singlediode(*params) + else: + self.results.diode_params = tuple( + _make_diode_params(*params) for params in params + ) + self.results.dc = tuple( + self.system.singlediode(*params) + for params in params + ) self.results.dc = self.system.scale_voltage_current_power( - self.results.dc).fillna(0) - + self.results.dc + ) + if self.system.num_arrays == 1: + self.results.dc.fillna(0, inplace=True) + else: + for dc in self.results.dc: + dc.fillna(0, inplace=True) return self def desoto(self): From b187444309c9d3c88b6f3cf74e1ed81e368313a7 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 3 Nov 2020 15:14:00 -0700 Subject: [PATCH 088/236] Add `name` attribute to `pvsystem.Array` It may be useful to refer to Arrays by name in the future. Adds an optional field to the Array class to support this. --- pvlib/pvsystem.py | 6 ++++-- pvlib/tests/test_pvsystem.py | 10 ++++++++-- pvlib/tests/test_tracking.py | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 4238d692c1..a0dca71867 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1131,7 +1131,7 @@ def __init__(self, module_parameters=None, temperature_model_parameters=None, modules_per_string=1, strings=1, - racking_model=None): + racking_model=None, name=None): self.surface_tilt = surface_tilt self.surface_azimuth = surface_azimuth @@ -1159,8 +1159,10 @@ def __init__(self, else: self.temperature_model_parameters = temperature_model_parameters + self.name = name + def __repr__(self): - attrs = ['surface_tilt', 'surface_azimuth', 'module', + attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module', 'albedo', 'racking_model', 'module_type', 'temperature_model_parameters', 'strings', 'modules_per_string'] diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 70d8ca3f13..5da9c13e59 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1406,6 +1406,7 @@ def test_PVSystem___repr__(): expected = """PVSystem: name: pv ftw Array: + name: None surface_tilt: 0 surface_azimuth: 180 module: blah @@ -1422,12 +1423,14 @@ def test_PVSystem___repr__(): def test_PVSystem_multi_array___repr__(): system = pvsystem.PVSystem( arrays=[pvsystem.Array(surface_tilt=30, surface_azimuth=100), - pvsystem.Array(surface_tilt=20, surface_azimuth=220)], + pvsystem.Array(surface_tilt=20, surface_azimuth=220, + name='foo')], inverter='blarg', ) expected = """PVSystem: name: None Array: + name: None surface_tilt: 30 surface_azimuth: 100 module: None @@ -1438,6 +1441,7 @@ def test_PVSystem_multi_array___repr__(): strings: 1 modules_per_string: 1 Array: + name: foo surface_tilt: 20 surface_azimuth: 220 module: None @@ -1485,9 +1489,11 @@ def test_Array___repr__(): racking_model='close_mount', module_parameters={'foo': 'bar'}, modules_per_string=100, - strings=10, module='baz' + strings=10, module='baz', + name='biz' ) expected = """Array: + name: biz surface_tilt: 10 surface_azimuth: 100 module: baz diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 6c34277351..1a39d98cf8 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -452,6 +452,7 @@ def test_SingleAxisTracker___repr__(): cross_axis_tilt: 0.0 name: None Array: + name: None surface_tilt: None surface_azimuth: None module: blah @@ -480,6 +481,7 @@ def test_LocalizedSingleAxisTracker___repr__(): cross_axis_tilt: 0.0 name: None Array: + name: None surface_tilt: None surface_azimuth: None module: blah From b98ea007eb37ab6e753d430374b2df78ba8d6b41 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Wed, 18 Nov 2020 13:44:05 -0700 Subject: [PATCH 089/236] Allow per-array GHI/DHI/DNI input to PVSystem.get_irradiance() --- pvlib/pvsystem.py | 35 +++++++++++++++++++------- pvlib/tests/test_pvsystem.py | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index a0dca71867..ffdebc05bc 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -258,11 +258,21 @@ def __repr__(self): repr += f'inverter: {self.inverter}' return repr - def _validate_per_array(self, values): - # Check that values is a tuple of the same length as - # `self._arrays`. If it is a single vlaue it is packed in to - # a length-1 tuple before the check. If the length is not the - # same a ValueError is raised, otherwise the tuple is returned. + def _validate_per_array(self, values, system_wide=False): + """Check that `values` is a tuple of the same length as + `self._arrays`. + + If it is a single vlaue it is packed in to a length-1 tuple before + the check. If the length is not the same a ValueError is raised, + otherwise the tuple is returned. + + When `system_wide` is True, single values are accepted even if there + is more than one Array. In this case the single value is replicated + in a tuple of the same length as `self._arrays` and that tuple is + returned. + """ + if system_wide and not isinstance(values, tuple): + return (values,) * self.num_arrays if not isinstance(values, tuple): values = (values,) if len(values) != len(self._arrays): @@ -340,10 +350,17 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, poa_irradiance : DataFrame Column names are: ``total, beam, sky, ground``. """ - return tuple(array.get_irradiance(solar_zenith, solar_azimuth, - dni, ghi, dhi, - dni_extra, airmass) - for array in self._arrays) + dni = self._validate_per_array(dni, system_wide=True) + ghi = self._validate_per_array(ghi, system_wide=True) + dhi = self._validate_per_array(dhi, system_wide=True) + return tuple( + array.get_irradiance(solar_zenith, solar_azimuth, + dni, ghi, dhi, + dni_extra, airmass) + for array, dni, ghi, dhi in zip( + self._arrays, dni, ghi, dhi + ) + ) @_unwrap_single_value def get_iam(self, aoi, iam_model='physical'): diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 5da9c13e59..8282566512 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1358,6 +1358,55 @@ def test_PVSystem_multi_array_get_irradiance(): ) +def test_PVSystem_multi_array_get_irradiance_multi_irrad(): + array_one = pvsystem.Array() + array_two = pvsystem.Array() + system = pvsystem.PVSystem(arrays=[array_one, array_two]) + location = Location(latitude=32, longitude=-111) + times = pd.date_range(start='20160101 1200-0700', + end='20160101 1800-0700', freq='6H') + solar_position = location.get_solarposition(times) + irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]}, + index=times) + irrads_two = pd.DataFrame( + {'dni': [0, 900], 'ghi': [0, 600], 'dhi': [0, 100]}, + index=times + ) + array_irrad = system.get_irradiance( + solar_position['apparent_zenith'], + solar_position['azimuth'], + (irrads['dhi'], irrads['dhi']), + (irrads['ghi'], irrads['ghi']), + (irrads['dni'], irrads['dni']) + ) + assert_frame_equal(array_irrad[0], array_irrad[1]) + array_irrad = system.get_irradiance( + solar_position['apparent_zenith'], + solar_position['azimuth'], + (irrads['dhi'], irrads_two['dhi']), + (irrads['ghi'], irrads_two['ghi']), + (irrads['dni'], irrads_two['dni']) + ) + assert not array_irrad[0].equals(array_irrad[1]) + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.get_irradiance( + solar_position['apparent_zenith'], + solar_position['azimuth'], + (irrads['dhi'], irrads_two['dhi'], irrads['dhi']), + (irrads['ghi'], irrads_two['ghi']), + irrads['dni'] + ) + array_irrad = system.get_irradiance( + solar_position['apparent_zenith'], + solar_position['azimuth'], + (irrads['dhi'], irrads_two['dhi']), + irrads['ghi'], + irrads['dni'] + ) + assert not array_irrad[0].equals(array_irrad[1]) + + def test_PVSystem_change_surface_azimuth(): system = pvsystem.PVSystem(surface_azimuth=180) assert system.surface_azimuth == 180 From a49d16ef5dca0377438c85b0424b11aae2f76709 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 20 Nov 2020 08:41:14 -0700 Subject: [PATCH 090/236] Test that the correct input is passed to each array Validates the output of PVSystem.get_irradiance() by checking against the output from Array.get_irradiance() with the expected input for each Array. --- pvlib/tests/test_pvsystem.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 8282566512..416e4b84c8 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1359,6 +1359,13 @@ def test_PVSystem_multi_array_get_irradiance(): def test_PVSystem_multi_array_get_irradiance_multi_irrad(): + """Test a system with two identical arrays but different irradiance. + + Because only the irradiance is different we expect the same output + when only one GHI/DHI/DNI input is given, but different output + for each array when different GHI/DHI/DNI input is given. For the later + case we verify that the correct irradiance data is passed to each array. + """ array_one = pvsystem.Array() array_two = pvsystem.Array() system = pvsystem.PVSystem(arrays=[array_one, array_two]) @@ -1387,7 +1394,19 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad(): (irrads['ghi'], irrads_two['ghi']), (irrads['dni'], irrads_two['dni']) ) + array_one_expected = array_one.get_irradiance( + solar_position['apparent_zenith'], + solar_position['azimuth'], + irrads['dhi'], irrads['ghi'], irrads['dni'] + ) + array_two_expected = array_two.get_irradiance( + solar_position['apparent_zenith'], + solar_position['azimuth'], + irrads_two['dhi'], irrads_two['ghi'], irrads_two['dni'] + ) assert not array_irrad[0].equals(array_irrad[1]) + assert_frame_equal(array_irrad[0], array_one_expected) + assert_frame_equal(array_irrad[1], array_two_expected) with pytest.raises(ValueError, match="Length mismatch for per-array parameter"): system.get_irradiance( @@ -1404,6 +1423,7 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad(): irrads['ghi'], irrads['dni'] ) + assert_frame_equal(array_irrad[0], array_one_expected) assert not array_irrad[0].equals(array_irrad[1]) From 955f7271d41566cc671e2defbe90234e83b58b55 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 20 Nov 2020 08:48:05 -0700 Subject: [PATCH 091/236] Fix whitespace in test_modelchain.py --- pvlib/tests/test_modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 7f74727e36..8d2f283283 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -68,7 +68,7 @@ def cec_dc_snl_ac_arrays(cec_module_cs5p_220m, cec_inverter_parameters, ) system = PVSystem( arrays=[array_one, array_two], - inverter_parameters = cec_inverter_parameters + inverter_parameters=cec_inverter_parameters ) return system From 92f28f503a3cc185148ca6c4031da2e7e6ce8ee3 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 20 Nov 2020 11:32:25 -0700 Subject: [PATCH 092/236] Support multiple weather input to PVSystem.xxxx_celltemp() Support for system-wide or per-array values of air temperature and wind speed. --- pvlib/pvsystem.py | 24 +++++-- pvlib/tests/test_pvsystem.py | 121 ++++++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 7 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index ffdebc05bc..0b4d1d394b 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -567,6 +567,8 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): numeric, values in degrees C. """ poa_global = self._validate_per_array(poa_global) + temp_air = self._validate_per_array(temp_air, system_wide=True) + wind_speed = self._validate_per_array(wind_speed, system_wide=True) for array in self._arrays: # warn user about change in default behavior in 0.9. if (array.temperature_model_parameters == {} and array.module_type @@ -589,7 +591,9 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): poa_global, temp_air, wind_speed, **build_kwargs(array.temperature_model_parameters) ) - for array, poa_global in zip(self._arrays, poa_global) + for array, poa_global, temp_air, wind_speed in zip( + self._arrays, poa_global, temp_air, wind_speed + ) ) @_unwrap_single_value @@ -675,6 +679,8 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): numeric, values in degrees C. """ poa_global = self._validate_per_array(poa_global) + temp_air = self._validate_per_array(temp_air, system_wide=True) + wind_speed = self._validate_per_array(wind_speed, system_wide=True) def build_celltemp_kwargs(array): return {**_build_kwargs(['eta_m', 'alpha_absorption'], @@ -684,7 +690,9 @@ def build_celltemp_kwargs(array): return tuple( temperature.pvsyst_cell(poa_global, temp_air, wind_speed, **build_celltemp_kwargs(array)) - for array, poa_global in zip(self._arrays, poa_global) + for array, poa_global, temp_air, wind_speed in zip( + self._arrays, poa_global, temp_air, wind_speed + ) ) @_unwrap_single_value @@ -710,12 +718,16 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): numeric, values in degrees C. """ poa_global = self._validate_per_array(poa_global) + temp_air = self._validate_per_array(temp_air, system_wide=True) + wind_speed = self._validate_per_array(wind_speed, system_wide=True) return tuple( temperature.faiman( poa_global, temp_air, wind_speed, **_build_kwargs( ['u0', 'u1'], array.temperature_model_parameters)) - for array, poa_global in zip(self._arrays, poa_global) + for array, poa_global, temp_air, wind_speed in zip( + self._arrays, poa_global, temp_air, wind_speed + ) ) @_unwrap_single_value @@ -751,6 +763,8 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): # default to using the Array attribute, but allow user to # override with a custom surface_tilt value poa_global = self._validate_per_array(poa_global) + temp_air = self._validate_per_array(temp_air, system_wide=True) + wind_speed = self._validate_per_array(wind_speed, system_wide=True) def _build_kwargs_fuentes(array): kwargs = {'surface_tilt': array.surface_tilt} @@ -764,7 +778,9 @@ def _build_kwargs_fuentes(array): temperature.fuentes( poa_global, temp_air, wind_speed, **_build_kwargs_fuentes(array)) - for array, poa_global in zip(self._arrays, poa_global) + for array, poa_global, temp_air, wind_speed in zip( + self._arrays, poa_global, temp_air, wind_speed + ) ) @_unwrap_single_value diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 416e4b84c8..30e6ad5d2d 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -385,6 +385,10 @@ def two_array_system(pvsyst_module_params, cec_module_params): temperature_model = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ 'open_rack_glass_glass' ] + # Need u_v to be non-zero so wind-speed changes cell temperature + # under the pvsyst model. + temperature_model['u_v'] = 1.0 + temperature_model['noct_installed'] = 45 module_params = {**pvsyst_module_params, **cec_module_params} return pvsystem.PVSystem( arrays=[ @@ -494,10 +498,121 @@ def test_PVSystem_faiman_celltemp(mocker): @pytest.mark.parametrize("celltemp", [pvsystem.PVSystem.faiman_celltemp, pvsystem.PVSystem.pvsyst_celltemp, - pvsystem.PVSystem.sapm_celltemp]) + pvsystem.PVSystem.sapm_celltemp, + pvsystem.PVSystem.fuentes_celltemp]) def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system): - temp_one, temp_two = celltemp(two_array_system, (1000, 500), 25, 1) - assert temp_one != temp_two + times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3) + irrad_one = pd.Series(1000, index=times) + irrad_two = pd.Series(500, index=times) + temp_air = pd.Series(25, index=times) + wind_speed = pd.Series(1, index=times) + temp_one, temp_two = celltemp( + two_array_system, (irrad_one, irrad_two), temp_air, wind_speed) + assert (temp_one != temp_two).all() + + +@pytest.mark.parametrize("celltemp", + [pvsystem.PVSystem.faiman_celltemp, + pvsystem.PVSystem.pvsyst_celltemp, + pvsystem.PVSystem.sapm_celltemp, + pvsystem.PVSystem.fuentes_celltemp]) +def test_PVSystem_multi_array_celltemp_multi_temp(celltemp, two_array_system): + times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3) + irrad = pd.Series(1000, index=times) + temp_air_one = pd.Series(25, index=times) + temp_air_two = pd.Series(5, index=times) + wind_speed = pd.Series(1, index=times) + temp_one, temp_two = celltemp( + two_array_system, + (irrad, irrad), + (temp_air_one, temp_air_two), + wind_speed + ) + assert (temp_one != temp_two).all() + temp_one_swtich, temp_two_switch = celltemp( + two_array_system, + (irrad, irrad), + (temp_air_two, temp_air_one), + wind_speed + ) + assert_series_equal(temp_one, temp_two_switch) + assert_series_equal(temp_two, temp_one_swtich) + + +@pytest.mark.parametrize("celltemp", + [pvsystem.PVSystem.faiman_celltemp, + pvsystem.PVSystem.pvsyst_celltemp, + pvsystem.PVSystem.sapm_celltemp, + pvsystem.PVSystem.fuentes_celltemp]) +def test_PVSystem_multi_array_celltemp_multi_wind(celltemp, two_array_system): + times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3) + irrad = pd.Series(1000, index=times) + temp_air = pd.Series(25, index=times) + wind_speed_one = pd.Series(1, index=times) + wind_speed_two = pd.Series(5, index=times) + temp_one, temp_two = celltemp( + two_array_system, + (irrad, irrad), + temp_air, + (wind_speed_one, wind_speed_two) + ) + assert (temp_one != temp_two).all() + temp_one_swtich, temp_two_switch = celltemp( + two_array_system, + (irrad, irrad), + temp_air, + (wind_speed_two, wind_speed_one) + ) + assert_series_equal(temp_one, temp_two_switch) + assert_series_equal(temp_two, temp_one_swtich) + + +@pytest.mark.parametrize("celltemp", + [pvsystem.PVSystem.faiman_celltemp, + pvsystem.PVSystem.pvsyst_celltemp, + pvsystem.PVSystem.sapm_celltemp, + pvsystem.PVSystem.fuentes_celltemp]) +def test_PVSystem_multi_array_celltemp_temp_too_short( + celltemp, two_array_system): + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + celltemp(two_array_system, (1000, 1000), (1,), 1) + + +@pytest.mark.parametrize("celltemp", + [pvsystem.PVSystem.faiman_celltemp, + pvsystem.PVSystem.pvsyst_celltemp, + pvsystem.PVSystem.sapm_celltemp, + pvsystem.PVSystem.fuentes_celltemp]) +def test_PVSystem_multi_array_celltemp_temp_too_long( + celltemp, two_array_system): + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + celltemp(two_array_system, (1000, 1000), (1,1,1), 1) + + +@pytest.mark.parametrize("celltemp", + [pvsystem.PVSystem.faiman_celltemp, + pvsystem.PVSystem.pvsyst_celltemp, + pvsystem.PVSystem.sapm_celltemp, + pvsystem.PVSystem.fuentes_celltemp]) +def test_PVSystem_multi_array_celltemp_wind_too_short( + celltemp, two_array_system): + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + celltemp(two_array_system, (1000, 1000), 25, (1,)) + + +@pytest.mark.parametrize("celltemp", + [pvsystem.PVSystem.faiman_celltemp, + pvsystem.PVSystem.pvsyst_celltemp, + pvsystem.PVSystem.sapm_celltemp, + pvsystem.PVSystem.fuentes_celltemp]) +def test_PVSystem_multi_array_celltemp_wind_too_long( + celltemp, two_array_system): + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + celltemp(two_array_system, (1000, 1000), 25, (1,1,1)) @pytest.mark.parametrize("celltemp", From 2783a87dc215058596dc0bb068767c0f1224403b Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 20 Nov 2020 12:06:38 -0700 Subject: [PATCH 093/236] Fix whitespace in test_pvsystem.py In tests: - test_PVSystem_multi_array_celltemp_wind_too_long() - test_PVSystem_multi_array_celltemp_temp_too_long() --- pvlib/tests/test_pvsystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 30e6ad5d2d..469bd5e679 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -588,7 +588,7 @@ def test_PVSystem_multi_array_celltemp_temp_too_long( celltemp, two_array_system): with pytest.raises(ValueError, match="Length mismatch for per-array parameter"): - celltemp(two_array_system, (1000, 1000), (1,1,1), 1) + celltemp(two_array_system, (1000, 1000), (1, 1, 1), 1) @pytest.mark.parametrize("celltemp", @@ -612,7 +612,7 @@ def test_PVSystem_multi_array_celltemp_wind_too_long( celltemp, two_array_system): with pytest.raises(ValueError, match="Length mismatch for per-array parameter"): - celltemp(two_array_system, (1000, 1000), 25, (1,1,1)) + celltemp(two_array_system, (1000, 1000), 25, (1, 1, 1)) @pytest.mark.parametrize("celltemp", From 71c7f3b358319b68880d6b9ae8ba949e9ddb6406 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 23 Nov 2020 10:12:25 -0700 Subject: [PATCH 094/236] Allow unique weather input for each array Updates ModelChain.prepare_inputs() to accept either a tuple of DataFrames or a single DataFrame. If `weather` is a tuple each DataFrame it contains must have the same index and must have all of the required columns ('ghi', 'dhi', 'dni'). --- pvlib/modelchain.py | 72 +++++++++++++++++++++++++--------- pvlib/tests/test_modelchain.py | 64 +++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 20 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index ba288c3d98..12b2e6b592 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -7,6 +7,7 @@ """ from functools import partial +import itertools import warnings import pandas as pd import numpy as np @@ -1159,7 +1160,7 @@ def _prep_inputs_solar_pos(self, kwargs={}): Assign solar position """ self.results.solar_position = self.location.get_solarposition( - self.weather.index, method=self.solar_position_method, + self.times, method=self.solar_position_method, **kwargs) return self @@ -1209,19 +1210,33 @@ def _verify_df(self, data, required): ------ ValueError if any of required are not in data.columns. """ - if not set(required) <= set(data.columns): - raise ValueError( - f"Incomplete input data. Data needs to contain {required}. " - f"Detected data contains: {list(data.columns)}") - return + def _verify(data): + if not set(required) <= set(data.columns): + raise ValueError( + "Incomplete input data. Data needs to contain " + f"{required}. Detected data contains: " + f"{list(data.columns)}") + if not isinstance(data, tuple): + _verify(data) + else: + for array_data in data: + _verify(array_data) def _assign_weather(self, data): - key_list = [k for k in WEATHER_KEYS if k in data] - self.weather = data[key_list].copy() - if self.weather.get('wind_speed') is None: - self.weather['wind_speed'] = 0 - if self.weather.get('temp_air') is None: - self.weather['temp_air'] = 20 + def _build_weather(data): + key_list = [k for k in WEATHER_KEYS if k in data] + weather = data[key_list].copy() + if weather.get('wind_speed') is None: + weather['wind_speed'] = 0 + if weather.get('temp_air') is None: + weather['temp_air'] = 20 + return weather + if not isinstance(data, tuple): + self.weather = _build_weather(data) + else: + self.weather = tuple( + _build_weather(weather) for weather in data + ) return self def _assign_total_irrad(self, data): @@ -1230,6 +1245,17 @@ def _assign_total_irrad(self, data): self.results.total_irrad = data[key_list].copy() return self + def _assign_times(self): + """Assign self.times according the the index of self.weather. + + If there are multiple DataFrames in self.weather then the index + of the first one is assigned + """ + if isinstance(self.weather, tuple): + self.times = self.weather[0].index + else: + self.times = self.weather.index + def prepare_inputs(self, weather): """ Prepare the solar position, irradiance, and weather inputs to @@ -1252,15 +1278,17 @@ def prepare_inputs(self, weather): -------- ModelChain.complete_irradiance """ - + if isinstance(weather, tuple): + _validate_weather_indices(weather) self._verify_df(weather, required=['ghi', 'dni', 'ghi']) self._assign_weather(weather) - - self.times = self.weather.index + self._assign_times() # build kwargs for solar position calculation try: - press_temp = _build_kwargs(['pressure', 'temp_air'], weather) + press_temp = _build_kwargs(['pressure', 'temp_air'], + weather[0] if isinstance(weather, tuple) + else weather) press_temp['temperature'] = press_temp.pop('temp_air') except KeyError: pass @@ -1288,9 +1316,9 @@ def prepare_inputs(self, weather): self.results.solar_position['azimuth']) self.results.total_irrad = get_irradiance( - self.weather['dni'], - self.weather['ghi'], - self.weather['dhi'], + _tuple_from_dfs(self.weather, 'dni'), + _tuple_from_dfs(self.weather, 'ghi'), + _tuple_from_dfs(self.weather, 'dhi'), airmass=self.results.airmass['airmass_relative'], model=self.transposition_model) @@ -1545,6 +1573,12 @@ def run_model_from_effective_irradiance(self, data=None): return self +def _validate_weather_indices(data): + if not all(map(lambda data: data[0].index.equals(data[1].index), + itertools.combinations(data, 2))): + raise ValueError("Weather DataFrames must have same index.") + + def _array_keys(dicts, array): """Return a set of keys from element `array` of `dicts` if it is a tuple otherwise return the set of keys in dicts.""" diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 8d2f283283..6ff74622b2 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -268,7 +268,8 @@ def sapm_dc_snl_ac_system_Array(sapm_module_params, cec_inverter_parameters, temperature_model_parameters=temp_model_params, modules_per_string=1, strings=1) - return PVSystem(arrays=[array_one, array_two]) + return PVSystem(arrays=[array_one, array_two], + inverter_parameters=cec_inverter_parameters) def test_ModelChain_creation(sapm_dc_snl_ac_system, location): @@ -393,6 +394,67 @@ def test_prepare_inputs_no_irradiance(sapm_dc_snl_ac_system, location): mc.prepare_inputs(weather) +def test_prepare_inputs_arrays_one_missing_irradiance( + sapm_dc_snl_ac_system_Array, location): + """If any of the input DataFrames is missing a column then a + ValueError is raised.""" + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + weather = pd.DataFrame( + {'ghi': [1], 'dhi': [1], 'dni': [1]} + ) + weather_incomplete = pd.DataFrame( + {'ghi': [1], 'dhi': [1]} + ) + with pytest.raises(ValueError, + match=r"Incomplete input data\. .*"): + mc.prepare_inputs((weather, weather_incomplete)) + with pytest.raises(ValueError, + match=r"Incomplete input data\. .*"): + mc.prepare_inputs((weather_incomplete, weather)) + + +def test_ModelChain_times_error_arrays(sapm_dc_snl_ac_system_Array, location): + """ModelChain.times is assigned a single index given multiple weather + DataFrames. + """ + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + irradiance = {'ghi': [1, 2], 'dhi': [1, 2], 'dni': [1, 2]} + times_one = pd.date_range(start='1/1/2020', freq='6H', periods=2) + times_two = pd.date_range(start='1/1/2020 00:15', freq='6H', periods=2) + weather_one = pd.DataFrame(irradiance, index=times_one) + weather_two = pd.DataFrame(irradiance, index=times_two) + with pytest.raises(ValueError, match="Weather DataFrames must have " + r"same index\."): + mc.prepare_inputs((weather_one, weather_two)) + # test with overlapping, but differently sized indices. + times_three = pd.date_range(start='1/1/2020', freq='6H', periods=3) + irradiance_three = irradiance + irradiance_three['ghi'].append(3) + irradiance_three['dhi'].append(3) + irradiance_three['dni'].append(3) + weather_three = pd.DataFrame(irradiance_three, index=times_three) + with pytest.raises(ValueError, match="Weather DataFrames must have " + r"same index\."): + mc.prepare_inputs((weather_one, weather_three)) + + +def test_ModelChain_times_arrays(sapm_dc_snl_ac_system_Array, location): + """ModelChain.times is assigned a single index given multiple weather + DataFrames. + """ + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + irradiance_one = {'ghi': [1, 2], 'dhi': [1, 2], 'dni': [1, 2]} + irradiance_two = {'ghi': [2, 1], 'dhi': [2, 1], 'dni': [2, 1]} + times = pd.date_range(start='1/1/2020', freq='6H', periods=2) + weather_one = pd.DataFrame(irradiance_one, index=times) + weather_two = pd.DataFrame(irradiance_two, index=times) + mc.prepare_inputs((weather_one, weather_two)) + assert mc.times.equals(times) + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + mc.prepare_inputs(weather_one) + assert mc.times.equals(times) + + def test_run_model_perez(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location, transposition_model='perez') From df133600e95e2f6989b83e4a13a8ec801bc264b7 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 23 Nov 2020 14:51:37 -0700 Subject: [PATCH 095/236] Document that ModelChain.prepare_inputs() can take a tuple Expand docstring to include new parameter type and note on the exceptions that can be raised. --- pvlib/modelchain.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index efc1511fe0..a6c74afa55 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1263,12 +1263,22 @@ def prepare_inputs(self, weather): Parameters ---------- - weather : DataFrame + weather : DataFrame or tuple of DataFrame Column names must be ``'dni'``, ``'ghi'``, ``'dhi'``, ``'wind_speed'``, ``'temp_air'``. All irradiance components are required. Air temperature of 20 C and wind speed of 0 m/s will be added to the DataFrame if not provided. + If `weather` is a tuple each DataFrame it contains must have + the same index. + + Raises + ------ + ValueError + If the `weather` DataFrame(s) are missing an irradiance component + or if `weather` is a tuple and the DataFrames it contains do not + all have the same index. + Notes ----- Assigns attributes: ``weather``, ``solar_position``, ``airmass``, From 94a087c66d2e48010b9a789d1aa3082da43fa282 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 23 Nov 2020 15:05:35 -0700 Subject: [PATCH 096/236] Raise if length of weather is not same as system.num_arrays Rather than raising the exception from deep in PVSystem we test the length and raise immediately to make the error easier to understand and debug for users. --- pvlib/modelchain.py | 18 ++++++++++++++---- pvlib/tests/test_modelchain.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index a6c74afa55..113f4632b7 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1256,6 +1256,13 @@ def _assign_times(self): else: self.times = self.weather.index + def _check_weather_length(self, data): + if len(data) != self.system.num_arrays: + raise ValueError( + "Weather must be same length as number of arrays in system. " + f"Expected {self.system.num_arrays}, got {len(data)}." + ) + def prepare_inputs(self, weather): """ Prepare the solar position, irradiance, and weather inputs to @@ -1270,14 +1277,16 @@ def prepare_inputs(self, weather): of 0 m/s will be added to the DataFrame if not provided. If `weather` is a tuple each DataFrame it contains must have - the same index. + the same index and it must be the same length as the number + of Arrays in the system. Raises ------ ValueError - If the `weather` DataFrame(s) are missing an irradiance component - or if `weather` is a tuple and the DataFrames it contains do not - all have the same index. + If the `weather` DataFrame(s) are missing an irradiance component, + if `weather` is a tuple and the DataFrames it contains do not + all have the same index, or if `weather` is a tuple with a + different length than the number of Arrays in the system. Notes ----- @@ -1289,6 +1298,7 @@ def prepare_inputs(self, weather): ModelChain.complete_irradiance """ if isinstance(weather, tuple): + self._check_weather_length(weather) _validate_weather_indices(weather) self._verify_df(weather, required=['ghi', 'dni', 'dhi']) self._assign_weather(weather) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index fc701b3d08..7a29dd7dd1 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -413,6 +413,20 @@ def test_prepare_inputs_arrays_one_missing_irradiance( mc.prepare_inputs((weather_incomplete, weather)) +def test_prepare_inputs_weather_wrong_length( + sapm_dc_snl_ac_system_Array, location): + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + weather = pd.DataFrame({'ghi': [1], 'dhi': [1], 'dni': [1]}) + with pytest.raises(ValueError, + match="Weather must be same length as number of arrays " + r"in system\. Expected 2, got 1\."): + mc.prepare_inputs((weather,)) + with pytest.raises(ValueError, + match="Weather must be same length as number of arrays " + r"in system\. Expected 2, got 3\."): + mc.prepare_inputs((weather, weather, weather)) + + def test_ModelChain_times_error_arrays(sapm_dc_snl_ac_system_Array, location): """ModelChain.times is assigned a single index given multiple weather DataFrames. From 071ddf45746f61f876eae916a7187ccbcb8a2cae Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 23 Nov 2020 16:30:07 -0700 Subject: [PATCH 097/236] Add tests for ModelChain.run_model with multiple weather --- pvlib/tests/test_modelchain.py | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 7a29dd7dd1..9d304677ad 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -272,6 +272,30 @@ def sapm_dc_snl_ac_system_Array(sapm_module_params, cec_inverter_parameters, inverter_parameters=cec_inverter_parameters) +@pytest.fixture(scope='function') +def sapm_dc_snl_ac_system_same_arrays(sapm_module_params, + cec_inverter_parameters, + sapm_temperature_cs5p_220m): + """A system with two identical arrays.""" + module = 'Canadian_Solar_CS5P_220M___2009_' + module_parameters = sapm_module_params.copy() + temp_model_params = sapm_temperature_cs5p_220m.copy() + array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=180, + albedo=0.2, module=module, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + modules_per_string=1, + strings=1) + array_two = pvsystem.Array(surface_tilt=32, surface_azimuth=180, + albedo=0.2, module=module, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + modules_per_string=1, + strings=1) + return PVSystem(arrays=[array_one, array_two], + inverter_parameters=cec_inverter_parameters) + + def test_ModelChain_creation(sapm_dc_snl_ac_system, location): ModelChain(sapm_dc_snl_ac_system, location) @@ -479,6 +503,17 @@ def test_prepare_inputs_missing_irrad_component( mc.prepare_inputs(weather) +def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays, location): + mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) + times = pd.date_range('20200101 1200-0700', periods=2, freq='6H') + weather_one = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, + index=times) + weather_two = pd.DataFrame({'dni': 500, 'ghi': 300, 'dhi': 75}, + index=times) + mc.run_model((weather_one, weather_two)) + assert (mc.dc[0] != mc.dc[1]).all() + + def test_run_model_perez(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location, transposition_model='perez') From 0b3c7edf02ebd01b0d8e212359ca764f4a3200fb Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 23 Nov 2020 16:30:44 -0700 Subject: [PATCH 098/236] Add test for ModelChain._prepare_temperature with multiple weather --- pvlib/tests/test_modelchain.py | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 9d304677ad..6406af1c3f 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -674,6 +674,41 @@ def test__prepare_temperature(sapm_dc_snl_ac_system, location, weather, assert_series_equal(mc.results.cell_temperature, data['cell_temperature']) +def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays, + location, weather, + total_irrad): + data = weather.copy() + data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad + data_two = data.copy() * 0.5 + mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location, + aoi_model='no_loss', spectral_model='no_loss') + # prepare_temperature expects mc.total_irrad and mc.weather to be set + mc._assign_weather((data, data_two)) + mc._assign_total_irrad((data, data_two)) + mc._prepare_temperature((data, data_two)) + expected = pd.Series([48.928025, 38.080016], index=data.index) + assert_series_equal(mc.results.cell_temperature[0], expected) + data['module_temperature'] = [40., 30.] + mc._prepare_temperature((data, data_two)) + expected = pd.Series([42.4, 31.5], index=data.index) + assert_series_equal(mc.results.cell_temperature[0], expected) + data['cell_temperature'] = [50., 35.] + mc._prepare_temperature((data, data_two)) + assert_series_equal( + mc.results.cell_temperature[0], data['cell_temperature']) + data_two['module_temperature'] = [40., 30.] + mc._prepare_temperature((data, data_two)) + assert_series_equal(mc.results.cell_temperature[1], expected) + assert_series_equal( + mc.results.cell_temperature[0], data['cell_temperature']) + data_two['cell_temperature'] = [10.0, 20.0] + mc._prepare_temperature((data, data_two)) + assert_series_equal( + mc.results.cell_temperature[1], data_two['cell_temperature']) + assert_series_equal( + mc.results.cell_temperature[0], data['cell_temperature']) + + def test_run_model_from_poa(sapm_dc_snl_ac_system, location, total_irrad): mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', spectral_model='no_loss') From 4d0ec6c34be51e18657a87a008adad0f49ffdedf Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 24 Nov 2020 07:59:46 -0700 Subject: [PATCH 099/236] Use itertools.starmap in _validate_weather_indices Substantially more readable and concise. --- pvlib/modelchain.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 113f4632b7..5e7634cede 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1594,8 +1594,9 @@ def run_model_from_effective_irradiance(self, data=None): def _validate_weather_indices(data): - if not all(map(lambda data: data[0].index.equals(data[1].index), - itertools.combinations(data, 2))): + indexes = map(lambda df: df.index, data) + if not all(itertools.starmap(pd.Index.equals, + itertools.combinations(indexes, 2))): raise ValueError("Weather DataFrames must have same index.") From d7a64f6cc0cb0e56b4df1d6ad2124151df9f72c5 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 24 Nov 2020 11:31:04 -0700 Subject: [PATCH 100/236] Update arrays in sapm_dc_snl_ac_system_same_arrays fixture Use the sam array parameters as sapm_dc_snl_ac so we can use the same expected output in multi-array tests. --- pvlib/tests/test_modelchain.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 6406af1c3f..cf06591a92 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -280,14 +280,14 @@ def sapm_dc_snl_ac_system_same_arrays(sapm_module_params, module = 'Canadian_Solar_CS5P_220M___2009_' module_parameters = sapm_module_params.copy() temp_model_params = sapm_temperature_cs5p_220m.copy() - array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=180, - albedo=0.2, module=module, + array_one = pvsystem.Array(surface_tilt=32.2, surface_azimuth=180, + module=module, module_parameters=module_parameters, temperature_model_parameters=temp_model_params, modules_per_string=1, strings=1) - array_two = pvsystem.Array(surface_tilt=32, surface_azimuth=180, - albedo=0.2, module=module, + array_two = pvsystem.Array(surface_tilt=32.2, surface_azimuth=180, + module=module, module_parameters=module_parameters, temperature_model_parameters=temp_model_params, modules_per_string=1, From d8088206ade06b79d550734e23e0b46f964cc367 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 24 Nov 2020 12:21:00 -0700 Subject: [PATCH 101/236] Rework _prepare_temperature tests Add some assertions to verify that the cell temperature for both arrays is not the same. --- pvlib/tests/test_modelchain.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index cf06591a92..5bdf56ce1f 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -679,7 +679,7 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays, total_irrad): data = weather.copy() data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad - data_two = data.copy() * 0.5 + data_two = data.copy() mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location, aoi_model='no_loss', spectral_model='no_loss') # prepare_temperature expects mc.total_irrad and mc.weather to be set @@ -688,9 +688,11 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays, mc._prepare_temperature((data, data_two)) expected = pd.Series([48.928025, 38.080016], index=data.index) assert_series_equal(mc.results.cell_temperature[0], expected) + assert_series_equal(mc.results.cell_temperature[1], expected) data['module_temperature'] = [40., 30.] mc._prepare_temperature((data, data_two)) expected = pd.Series([42.4, 31.5], index=data.index) + assert (mc.results.cell_temperature[1] != expected).all() assert_series_equal(mc.results.cell_temperature[0], expected) data['cell_temperature'] = [50., 35.] mc._prepare_temperature((data, data_two)) From 0f4cbf764b7244b3f31363537fc93acacba969ce Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 24 Nov 2020 12:41:27 -0700 Subject: [PATCH 102/236] ModelChain._prepare_temperature accepts multiple DataFrames If multiple dataframes are passed in a tuple then _prepare_temperature will look for 'cell_temperature' or 'module_temperature' in each data frame. For data frames that do not have one of these columns the ModelChain.temperature_model() is applied to calculate the cell temperature from the weather data that has already been assigned in other ModelChain fields. --- pvlib/modelchain.py | 93 ++++++++++++++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 5e7634cede..7da153b9db 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1005,24 +1005,29 @@ def infer_temperature_model(self): def sapm_temp(self): poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') + temp_air = _tuple_from_dfs(self.weather, 'temp_air') + wind_speed = _tuple_from_dfs(self.weather, 'wind_speed') self.results.cell_temperature = self.system.sapm_celltemp( - poa, self.weather['temp_air'], self.weather['wind_speed']) + poa, temp_air, wind_speed) return self def pvsyst_temp(self): poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') + # TODO handle multiple weather self.results.cell_temperature = self.system.pvsyst_celltemp( poa, self.weather['temp_air'], self.weather['wind_speed']) return self def faiman_temp(self): poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') + # TODO handle multiple weather self.results.cell_temperature = self.system.faiman_celltemp( poa, self.weather['temp_air'], self.weather['wind_speed']) return self def fuentes_temp(self): poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') + # TODO handle multiple weather self.results.cell_temperature = self.system.fuentes_celltemp( poa, self.weather['temp_air'], self.weather['wind_speed']) return self @@ -1240,9 +1245,15 @@ def _build_weather(data): return self def _assign_total_irrad(self, data): - key_list = [k for k in POA_KEYS if k in data] - # TODO multiple arrays - self.results.total_irrad = data[key_list].copy() + def _build_irrad(data): + key_list = [k for k in POA_KEYS if k in data] + return data[key_list].copy() + if isinstance(data, tuple): + self.results.total_irrad = tuple( + _build_irrad(irrad_data) for irrad_data in data + ) + return self + self.results.total_irrad = _build_irrad(data) return self def _assign_times(self): @@ -1385,6 +1396,40 @@ def prepare_inputs_from_poa(self, data): return self + def _get_cell_temperature(self, data, + total_irrad, temperature_model_parameters): + """Extract the cell temperature data from a DataFrame. + + If 'cell_temperature' column exists then it is returned. If + 'module_temperature' column exists then it is used to calculate + the cell temperature. If neither column exists then None is + returned. + """ + if 'cell_temperature' in data: + return data['cell_temperature'] + # cell_temperature is not in input. Calculate cell_temperature using + # a temperature_model. + # If module_temperature is in input data we can use the SAPM cell + # temperature model. + if (('module_temperature' in data) and + (self.temperature_model.__name__ == 'sapm_temp')): + # use SAPM cell temperature model only + return pvlib.temperature.sapm_cell_from_module( + module_temperature=data['module_temperature'], + poa_global=total_irrad['poa_global'], + deltaT=temperature_model_parameters['deltaT']) + + def _prepare_temperature_single_array(self, data): + """Set cell_temperature using a single weather data frame.""" + self.results.cell_temperature = self._get_cell_temperature( + data, + self.results.total_irrad, + self.system.temperature_model_parameters + ) + if self.results.cell_temperature is None: + self.temperature_model() + return self + def _prepare_temperature(self, data=None): """ Sets cell_temperature using inputs in data and the specified @@ -1406,33 +1451,29 @@ def _prepare_temperature(self, data=None): ------- self - Assigns attribute ``cell_temperature``. + Assigns attribute ``results.cell_temperature``. """ - if 'cell_temperature' in data: - # TODO replicate self.system.num_arrays times ??? - self.results.cell_temperature = data['cell_temperature'] - return self - - # cell_temperature is not in input. Calculate cell_temperature using - # a temperature_model. - # If module_temperature is in input data we can use the SAPM cell - # temperature model. - if (('module_temperature' in data) and - (self.temperature_model.__name__ == 'sapm_temp')): - # use SAPM cell temperature model only - self.results.cell_temperature = \ - pvlib.temperature.sapm_cell_from_module( - module_temperature=data['module_temperature'], - poa_global=_tuple_from_dfs( - self.results.total_irrad, 'poa_global'), - deltaT=_tuple_from_dfs( - self.system.temperature_model_parameters, 'deltaT')) - return self - + if not isinstance(data, tuple) and self.system.num_arrays > 1: + data = (data,) * self.system.num_arrays + elif not isinstance(data, tuple): + return self._prepare_temperature_single_array(data) + given_cell_temperature = itertools.starmap( + self._get_cell_temperature, + zip(data, self.results.total_irrad, + self.system.temperature_model_parameters) + ) # Calculate cell temperature from weather data. Cell temperature models # expect total_irrad['poa_global']. self.temperature_model() + # replace calculated cell temperature with temperature given in `data` + # where available. + self.results.cell_temperature = tuple( + itertools.starmap( + lambda given, modeled: modeled if given is None else given, + zip(given_cell_temperature, self.results.cell_temperature) + ) + ) return self def run_model(self, weather): From 5ef7b9a25ea003edd27971f99a0d1ca8cf7655bc Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 24 Nov 2020 12:46:38 -0700 Subject: [PATCH 103/236] Fix indentation in ModelChain._get_cell_temperature() --- pvlib/modelchain.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 7da153b9db..f590215c44 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1415,9 +1415,9 @@ def _get_cell_temperature(self, data, (self.temperature_model.__name__ == 'sapm_temp')): # use SAPM cell temperature model only return pvlib.temperature.sapm_cell_from_module( - module_temperature=data['module_temperature'], - poa_global=total_irrad['poa_global'], - deltaT=temperature_model_parameters['deltaT']) + module_temperature=data['module_temperature'], + poa_global=total_irrad['poa_global'], + deltaT=temperature_model_parameters['deltaT']) def _prepare_temperature_single_array(self, data): """Set cell_temperature using a single weather data frame.""" From 8cfacfa09faa8c0d349aaf09e0aedec7f38ccdc7 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 24 Nov 2020 14:50:24 -0700 Subject: [PATCH 104/236] Support multiple weather DataFrames in all ModelChain.xxxx_temp() Adds support for per-array weather input in - ModelChain.pvsyst_temp() - ModelChain.faiman_temp() - ModelChain.fuentes_temp() Support was added in ModelChain.sapm_temp() in a previous commit. --- pvlib/modelchain.py | 15 +++++++++------ pvlib/tests/test_modelchain.py | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index f590215c44..0ec5d1722d 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1013,23 +1013,26 @@ def sapm_temp(self): def pvsyst_temp(self): poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') - # TODO handle multiple weather + temp_air = _tuple_from_dfs(self.weather, 'temp_air') + wind_speed = _tuple_from_dfs(self.weather, 'wind_speed') self.results.cell_temperature = self.system.pvsyst_celltemp( - poa, self.weather['temp_air'], self.weather['wind_speed']) + poa, temp_air, wind_speed) return self def faiman_temp(self): poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') - # TODO handle multiple weather + temp_air = _tuple_from_dfs(self.weather, 'temp_air') + wind_speed = _tuple_from_dfs(self.weather, 'wind_speed') self.results.cell_temperature = self.system.faiman_celltemp( - poa, self.weather['temp_air'], self.weather['wind_speed']) + poa, temp_air, wind_speed) return self def fuentes_temp(self): poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') - # TODO handle multiple weather + temp_air = _tuple_from_dfs(self.weather, 'temp_air') + wind_speed = _tuple_from_dfs(self.weather, 'wind_speed') self.results.cell_temperature = self.system.fuentes_celltemp( - poa, self.weather['temp_air'], self.weather['wind_speed']) + poa, temp_air, wind_speed) return self @property diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 5bdf56ce1f..809d6b9b73 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -711,6 +711,33 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays, mc.results.cell_temperature[0], data['cell_temperature']) +@pytest.mark.parametrize('temp_params,temp_model', + [({'a': -3.47, 'b': -.0594, 'deltaT': 3}, + ModelChain.sapm_temp), + ({'u_c': 29.0, 'u_v': 0}, + ModelChain.pvsyst_temp), + ({'u0': 25.0, 'u1': 6.84}, + ModelChain.faiman_temp), + ({'noct_installed': 45}, + ModelChain.fuentes_temp)]) +def test_temperature_models_arrays_multi_weather( + temp_params, temp_model, + sapm_dc_snl_ac_system_same_arrays, + location, weather, total_irrad): + sapm_dc_snl_ac_system_same_arrays.temperature_model_parameters = \ + temp_params + # set air temp so it does not default to the same value for both arrays + weather['temp_air'] = 25 + weather_one = weather + weather_two = weather.copy() * 0.5 + mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location, + aoi_model='no_loss', spectral_model='no_loss') + mc.prepare_inputs((weather_one, weather_two)) + temp_model(mc) + assert (mc.results.cell_temperature[0] + != mc.results.cell_temperature[1]).all() + + def test_run_model_from_poa(sapm_dc_snl_ac_system, location, total_irrad): mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', spectral_model='no_loss') From 2f991f3d536e0217dfc0b69d2b3c12185e8c2b66 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 24 Nov 2020 15:04:07 -0700 Subject: [PATCH 105/236] Refactor ModelChain.xxxx_temp() functions Abstract the pattern used in each function to reduce code duplication. Should reduce the burden for adding new temperature models as well as maintenance in the future. --- pvlib/modelchain.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 0ec5d1722d..24e3baa8b4 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1003,37 +1003,39 @@ def infer_temperature_model(self): raise ValueError(f'could not infer temperature model from ' f'system.temperature_module_parameters {params}.') - def sapm_temp(self): + def _set_celltemp(self, model): + """Set self.results.cell_temp using the given cell temperature model. + + Parameters + ---------- + model : function + A function that takes POA irradiance, air temperature, and + wind speed and returns cell temperature. `model` must accept + tuples or single values for each parameter where each element of + the tuple is the value for a different array in the system + (see :py:class:`pvlib.pvsystem.PVSystem` for more information). + + Returns + ------- + self + """ poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') temp_air = _tuple_from_dfs(self.weather, 'temp_air') wind_speed = _tuple_from_dfs(self.weather, 'wind_speed') - self.results.cell_temperature = self.system.sapm_celltemp( - poa, temp_air, wind_speed) + self.results.cell_temperature = model(poa, temp_air, wind_speed) return self + def sapm_temp(self): + return self._set_celltemp(self.system.sapm_celltemp) + def pvsyst_temp(self): - poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') - temp_air = _tuple_from_dfs(self.weather, 'temp_air') - wind_speed = _tuple_from_dfs(self.weather, 'wind_speed') - self.results.cell_temperature = self.system.pvsyst_celltemp( - poa, temp_air, wind_speed) - return self + return self._set_celltemp(self.system.pvsyst_celltemp) def faiman_temp(self): - poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') - temp_air = _tuple_from_dfs(self.weather, 'temp_air') - wind_speed = _tuple_from_dfs(self.weather, 'wind_speed') - self.results.cell_temperature = self.system.faiman_celltemp( - poa, temp_air, wind_speed) - return self + return self._set_celltemp(self.system.faiman_celltemp) def fuentes_temp(self): - poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') - temp_air = _tuple_from_dfs(self.weather, 'temp_air') - wind_speed = _tuple_from_dfs(self.weather, 'wind_speed') - self.results.cell_temperature = self.system.fuentes_celltemp( - poa, temp_air, wind_speed) - return self + return self._set_celltemp(self.system.fuentes_celltemp) @property def losses_model(self): From 0096181daa4754fd33dcbf5a8a8dce8705f963e9 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 30 Nov 2020 08:32:17 -0700 Subject: [PATCH 106/236] Accept multiple weather frames in ModelChain.complete_irradiance Apply the same logic as in ModelChain.prepare_inputs to validate the indices of the data frames. Iterate over them, adding missing columns where necessary. --- pvlib/modelchain.py | 33 ++++++++++++++++++++------------ pvlib/tests/test_modelchain.py | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 24e3baa8b4..ca0ca2bf2e 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1132,12 +1132,23 @@ def complete_irradiance(self, weather): >>> mc = ModelChain(my_system, my_location) # doctest: +SKIP >>> mc.run_model(my_weather) # doctest: +SKIP """ + if isinstance(weather, tuple): + _validate_weather_indices(weather) self.weather = weather - + self._assign_times() self.results.solar_position = self.location.get_solarposition( - self.weather.index, method=self.solar_position_method) + self.times, method=self.solar_position_method) - icolumns = set(self.weather.columns) + if isinstance(weather, tuple): + for w in self.weather: + self._complete_irradiance(w) + else: + self._complete_irradiance(self.weather) + + return self + + def _complete_irradiance(self, weather): + icolumns = set(weather.columns) wrn_txt = ("This function is not safe at the moment.\n" + "Results can be too high or negative.\n" + "Help to improve this function on github:\n" + @@ -1145,26 +1156,24 @@ def complete_irradiance(self, weather): if {'ghi', 'dhi'} <= icolumns and 'dni' not in icolumns: clearsky = self.location.get_clearsky( - self.weather.index, solar_position=self.results.solar_position) - self.weather.loc[:, 'dni'] = pvlib.irradiance.dni( - self.weather.loc[:, 'ghi'], self.weather.loc[:, 'dhi'], + weather.index, solar_position=self.results.solar_position) + weather.loc[:, 'dni'] = pvlib.irradiance.dni( + weather.loc[:, 'ghi'], weather.loc[:, 'dhi'], self.results.solar_position.zenith, clearsky_dni=clearsky['dni'], clearsky_tolerance=1.1) elif {'dni', 'dhi'} <= icolumns and 'ghi' not in icolumns: warnings.warn(wrn_txt, UserWarning) - self.weather.loc[:, 'ghi'] = ( - self.weather.dhi + self.weather.dni * + weather.loc[:, 'ghi'] = ( + weather.dhi + weather.dni * tools.cosd(self.results.solar_position.zenith) ) elif {'dni', 'ghi'} <= icolumns and 'dhi' not in icolumns: warnings.warn(wrn_txt, UserWarning) - self.weather.loc[:, 'dhi'] = ( - self.weather.ghi - self.weather.dni * + weather.loc[:, 'dhi'] = ( + weather.ghi - weather.dni * tools.cosd(self.results.solar_position.zenith)) - return self - def _prep_inputs_solar_pos(self, kwargs={}): """ Assign solar position diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 809d6b9b73..7153b830cd 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1376,3 +1376,38 @@ def test_complete_irradiance(sapm_dc_snl_ac_system, location): assert_series_equal(mc.weather['dni'], pd.Series([49.756966, 62.153947], index=times, name='dni')) + + +@pytest.mark.filterwarnings("ignore:This function is not safe at the moment") +def test_complete_irradiance_arrays( + sapm_dc_snl_ac_system_same_arrays, location): + """ModelChain.complete_irradiance can accept a tuple of weather + DataFrames.""" + times = pd.date_range(start='2020-01-01 0700-0700', periods=2, freq='H') + weather = pd.DataFrame({'dni': [2, 3], + 'dhi': [4, 6], + 'ghi': [9, 5]}, index=times) + mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) + with pytest.raises(ValueError, + match=r"Weather DataFrames must have same index\."): + mc.complete_irradiance((weather, weather[1:])) + mc.complete_irradiance((weather, weather)) + for mc_weather in mc.weather: + assert_series_equal(mc_weather['dni'], + pd.Series([2, 3], index=times, name='dni')) + assert_series_equal(mc_weather['dhi'], + pd.Series([4, 6], index=times, name='dhi')) + assert_series_equal(mc_weather['ghi'], + pd.Series([9, 5], index=times, name='ghi')) + mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) + mc.complete_irradiance((weather[['ghi', 'dhi']], weather[['dhi', 'dni']])) + assert 'dni' in mc.weather[0].columns + assert 'ghi' in mc.weather[1].columns + mc.complete_irradiance((weather, weather[['ghi', 'dni']])) + assert_series_equal(mc.weather[0]['dhi'], + pd.Series([4, 6], index=times, name='dhi')) + assert_series_equal(mc.weather[0]['ghi'], + pd.Series([9, 5], index=times, name='ghi')) + assert_series_equal(mc.weather[0]['dni'], + pd.Series([2, 3], index=times, name='dni')) + assert 'dhi' in mc.weather[1].columns From 663fb0c99aa28ed5e6602d4949e8595fd4faa4c6 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 30 Nov 2020 10:52:16 -0700 Subject: [PATCH 107/236] ModelChain.complete_irradiance input same length as number of arrays Document the new input types and exceptions. --- pvlib/modelchain.py | 13 ++++++++++++- pvlib/tests/test_modelchain.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index ca0ca2bf2e..5882c28dd6 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1101,16 +1101,26 @@ def complete_irradiance(self, weather): Parameters ---------- - weather : DataFrame + weather : DataFrame or tuple of DatFrame Column names must be ``'dni'``, ``'ghi'``, ``'dhi'``, ``'wind_speed'``, ``'temp_air'``. All irradiance components are required. Air temperature of 20 C and wind speed of 0 m/s will be added to the DataFrame if not provided. + If weather is a tuple it must be the same length as the number + of arrays in the system and the indices for each DataFrame must + be the same. Returns ------- self + Raises + ------ + ValueError + if the number of dataframes in `weather` is not the same as the + number of arrays in the system or if the indices of all elements + of `weather` are not the same. + Notes ----- Assigns attributes: ``weather`` @@ -1133,6 +1143,7 @@ def complete_irradiance(self, weather): >>> mc.run_model(my_weather) # doctest: +SKIP """ if isinstance(weather, tuple): + self._check_weather_length(weather) _validate_weather_indices(weather) self.weather = weather self._assign_times() diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 7153b830cd..12593c9877 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1411,3 +1411,19 @@ def test_complete_irradiance_arrays( assert_series_equal(mc.weather[0]['dni'], pd.Series([2, 3], index=times, name='dni')) assert 'dhi' in mc.weather[1].columns + + +def test_complete_irradiance_arrays_wrong_length( + sapm_dc_snl_ac_system_same_arrays, location): + mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) + times = pd.date_range(start='2020-01-01 0700-0700', periods=2, freq='H') + weather = pd.DataFrame({'dni': [2, 3], + 'dhi': [4, 6], + 'ghi': [9, 5]}, index=times) + error_str = "Weather must be same length as number " \ + r"of arrays in system\. Expected 2, got [0-9]+\." + with pytest.raises(ValueError, match=error_str): + mc.complete_irradiance((weather,)) + with pytest.raises(ValueError, match=error_str): + mc.complete_irradiance((weather, weather, weather)) + From a0fa84b78c2274997f81a8a5ddd870d0e6112dcf Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 30 Nov 2020 12:31:42 -0700 Subject: [PATCH 108/236] Validate per-array input to ModelChain.prepare_inputs_from_poa() When passing POA irradiance directly to ModelChain, it must be passed independently for each array. If the wrong number of POA dataframes are provided by the user we raise a value error. --- pvlib/modelchain.py | 25 +++++++++++++++++++++++-- pvlib/tests/test_modelchain.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 5882c28dd6..86fd5aca8b 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1380,6 +1380,16 @@ def prepare_inputs(self, weather): return self + def _check_poa_length(self, data): + """Check that the number of elements in `data` is the same as + the number of arrays in `self.system`.""" + if self.system.num_arrays == 1 and not isinstance(data, tuple): + return + if not isinstance(data, tuple) or len(data) != self.system.num_arrays: + raise ValueError("POA must be provided independently for " + "each array. Input must be a tuple of length " + f"{self.system.num_arrays}.") + def prepare_inputs_from_poa(self, data): """ Prepare the solar position, irradiance and weather inputs to @@ -1387,7 +1397,7 @@ def prepare_inputs_from_poa(self, data): Parameters ---------- - data : DataFrame + data : DataFrame or tuple of DataFrame Contains plane-of-array irradiance data. Required column names include ``'poa_global'``, ``'poa_direct'`` and ``'poa_diffuse'``. Columns with weather-related data are ssigned to the @@ -1395,6 +1405,15 @@ def prepare_inputs_from_poa(self, data): ``'wind_speed'`` are not provided, air temperature of 20 C and wind speed of 0 m/s are assumed. + If there are multiple arrays in the system then `data` must be + a tuple with the same length as the number of arrays. + + Raises + ------ + ValueError + If the number of DataFrames passed in `data` is not the same + as the number of arrays in the system. + Notes ----- Assigns attributes: ``weather``, ``total_irrad``, ``solar_position``, @@ -1404,7 +1423,9 @@ def prepare_inputs_from_poa(self, data): -------- pvlib.modelchain.ModelChain.prepare_inputs """ - + self._check_poa_length(data) + if isinstance(data, tuple): + _validate_weather_indices(data) self._assign_weather(data) self._verify_df(data, required=['poa_global', 'poa_direct', diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 12593c9877..a87659710e 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -653,6 +653,39 @@ def test_prepare_inputs_from_poa(sapm_dc_snl_ac_system, location, assert_frame_equal(mc.results.total_irrad, total_irrad) +def test_prepare_poa_wrong_number_arrays( + sapm_dc_snl_ac_system_Array, location, total_irrad, weather): + error_str = "POA must be provided independently for each " \ + r"array\. Input must be a tuple of length 2\." + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + poa = pd.concat([weather, total_irrad], axis=1) + with pytest.raises(ValueError, match=error_str): + mc.prepare_inputs_from_poa(poa) + with pytest.raises(ValueError, match=error_str): + mc.prepare_inputs_from_poa((poa,)) + with pytest.raises(ValueError, match=error_str): + mc.prepare_inputs_from_poa((poa, poa, poa)) + + +def test_prepare_poa_arrays_different_indices( + sapm_dc_snl_ac_system_Array, location, total_irrad, weather): + error_str = r"Weather DataFrames must have same index\." + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + poa = pd.concat([weather, total_irrad], axis=1) + with pytest.raises(ValueError, match=error_str): + mc.prepare_inputs_from_poa((poa, poa.shift(periods=1, freq='infer'))) + + +def test_prepare_poa_arrays_missing_column( + sapm_dc_snl_ac_system_Array, location, weather, total_irrad): + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + poa = pd.concat([weather, total_irrad], axis=1) + with pytest.raises(ValueError, match=r"Incomplete input data\. " + r"Data needs to contain .*\. " + r"Detected data contains: .*"): + mc.prepare_inputs_from_poa((poa, poa.drop(columns='poa_global'))) + + def test__prepare_temperature(sapm_dc_snl_ac_system, location, weather, total_irrad): data = weather.copy() From f7fd6c797892a2f648693466ef76bcd0cb503dc0 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 30 Nov 2020 14:31:32 -0700 Subject: [PATCH 109/236] Multiple inputs to ModelChain.run_model_from_effective_irradiance() Multi-array systems require multiple effective irradiance DataFrames. --- pvlib/modelchain.py | 29 ++++++++++++++++------- pvlib/tests/test_modelchain.py | 43 ++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 86fd5aca8b..cf3ef8b5f2 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1380,14 +1380,14 @@ def prepare_inputs(self, weather): return self - def _check_poa_length(self, data): + def _check_length(self, data): """Check that the number of elements in `data` is the same as the number of arrays in `self.system`.""" if self.system.num_arrays == 1 and not isinstance(data, tuple): return if not isinstance(data, tuple) or len(data) != self.system.num_arrays: - raise ValueError("POA must be provided independently for " - "each array. Input must be a tuple of length " + raise ValueError("Input must be provided independently for " + "each array. You must pass a tuple of length " f"{self.system.num_arrays}.") def prepare_inputs_from_poa(self, data): @@ -1423,7 +1423,7 @@ def prepare_inputs_from_poa(self, data): -------- pvlib.modelchain.ModelChain.prepare_inputs """ - self._check_poa_length(data) + self._check_length(data) if isinstance(data, tuple): _validate_weather_indices(data) self._assign_weather(data) @@ -1647,17 +1647,28 @@ def run_model_from_effective_irradiance(self, data=None): Parameters ---------- - data : DataFrame, default None + data : DataFrame or tuple of DataFrame, default None Required column is ``'effective_irradiance'``. If optional column ``'cell_temperature'`` is provided, these values are used instead of `temperature_model`. If optional column ``'module_temperature'`` is provided, `temperature_model` must be ``'sapm'``. + If the system has multiple arrays, `data` must be a tuple with + the same length as the number of arrays in the system where + each element provides the effective irradiance and weather + for each array. + Returns ------- self + Raises + ------ + ValueError + If the number of arrays is different than the number of data + frames passed in `data` or the DataFrames have different indices. + Notes ----- Assigns attributes: ``weather``, ``total_irrad``, @@ -1669,12 +1680,12 @@ def run_model_from_effective_irradiance(self, data=None): pvlib.modelchain.ModelChain.run_model_from pvlib.modelchain.ModelChain.run_model_from_poa """ - + self._check_length(data) + _validate_weather_indices(data) self._assign_weather(data) self._assign_total_irrad(data) - # TODO handle multiple irradiances (replicate self.system.num_arrays - # times?) - self.results.effective_irradiance = data['effective_irradiance'] + self.results.effective_irradiance = _tuple_from_dfs( + data, 'effective_irradiance') self._run_from_effective_irrad(data) return self diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index a87659710e..f5ae279e79 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -655,8 +655,8 @@ def test_prepare_inputs_from_poa(sapm_dc_snl_ac_system, location, def test_prepare_poa_wrong_number_arrays( sapm_dc_snl_ac_system_Array, location, total_irrad, weather): - error_str = "POA must be provided independently for each " \ - r"array\. Input must be a tuple of length 2\." + error_str = "Input must be provided independently for each " \ + r"array\. You must pass a tuple of length 2\." mc = ModelChain(sapm_dc_snl_ac_system_Array, location) poa = pd.concat([weather, total_irrad], axis=1) with pytest.raises(ValueError, match=error_str): @@ -811,6 +811,45 @@ def test_run_model_from_effective_irradiance(sapm_dc_snl_ac_system, location, assert_series_equal(ac, expected) +def test_run_model_from_effective_irradiance_arrays_error( + sapm_dc_snl_ac_system_Array, location, weather, total_irrad): + data = weather.copy() + data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad + data['effetive_irradiance'] = data['poa_global'] + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + error_str = r"Input must be provided independently for each array\. " \ + r"You must pass a tuple of length 2\." + with pytest.raises(ValueError, match=error_str): + mc.run_model_from_effective_irradiance(data) + with pytest.raises(ValueError, match=error_str): + mc.run_model_from_effective_irradiance((data,)) + with pytest.raises(ValueError, match=error_str): + mc.run_model_from_effective_irradiance((data, data, data)) + with pytest.raises(ValueError, + match=r"Weather DataFrames must have same index\."): + mc.run_model_from_effective_irradiance( + (data, data.shift(periods=1, freq='infer')) + ) + + +def test_run_model_from_effective_irradiance_arrays( + sapm_dc_snl_ac_system_Array, location, weather, total_irrad): + data = weather.copy() + data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad + data['effective_irradiance'] = data['poa_global'] + data['cell_temperature'] = 40 + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + mc.run_model_from_effective_irradiance((data, data)) + # arrays have different orientation, but should give same dc power + # because we are the same passing effective irradiance and cell + # temperature. + assert_frame_equal(mc.dc[0], mc.dc[1]) + data_two = data.copy() + data_two['effective_irradiance'] = data['poa_global'] * 0.5 + mc.run_model_from_effective_irradiance((data, data_two)) + assert (mc.dc[0] != mc.dc[1]).all().all() + + def poadc(mc): mc.results.dc = mc.results.total_irrad['poa_global'] * 0.2 mc.results.dc.name = None # assert_series_equal will fail without this From 919353beaa2496f39eff2b73a83a7fcc2126ddbf Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 30 Nov 2020 14:33:10 -0700 Subject: [PATCH 110/236] Clean up whitespace --- pvlib/tests/test_modelchain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index f5ae279e79..9343460567 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1498,4 +1498,3 @@ def test_complete_irradiance_arrays_wrong_length( mc.complete_irradiance((weather,)) with pytest.raises(ValueError, match=error_str): mc.complete_irradiance((weather, weather, weather)) - From 59db97cbadfe7312a9b8a4e8b5f9c5fb0a445168 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 30 Nov 2020 14:39:20 -0700 Subject: [PATCH 111/236] Only validate indices if a tuple is passed --- pvlib/modelchain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index cf3ef8b5f2..a51b2336a8 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1681,7 +1681,8 @@ def run_model_from_effective_irradiance(self, data=None): pvlib.modelchain.ModelChain.run_model_from_poa """ self._check_length(data) - _validate_weather_indices(data) + if isinstance(data, tuple): + _validate_weather_indices(data) self._assign_weather(data) self._assign_total_irrad(data) self.results.effective_irradiance = _tuple_from_dfs( From 280a2a5840099bc4235696c56065e84e6ec0e42b Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 30 Nov 2020 15:09:00 -0700 Subject: [PATCH 112/236] Add test to cover undefined attribute Because we hook `__getattr__` in order to deprecate the old results attributes (e.g. `ModelChain.dc`) we need to test that an AttributeError is raised when an unknown attribute is requested. --- pvlib/tests/test_modelchain.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 9343460567..cf93c46945 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1498,3 +1498,9 @@ def test_complete_irradiance_arrays_wrong_length( mc.complete_irradiance((weather,)) with pytest.raises(ValueError, match=error_str): mc.complete_irradiance((weather, weather, weather)) + + +def test_unknown_attribute(sapm_dc_snl_ac_system, location): + mc = ModelChain(sapm_dc_snl_ac_system, location) + with pytest.raises(AttributeError): + mc.unknown_attribute From eb31432f728cf09e050a223580eb713437041d26 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 30 Nov 2020 15:19:39 -0700 Subject: [PATCH 113/236] Verify all arrays have consistent parameters Cliff wrote this. This commit just invokes the method and updates it so it works with only one array. --- pvlib/modelchain.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index a51b2336a8..5792b8372c 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -381,7 +381,7 @@ def __init__(self, system, location, self.airmass_model = airmass_model # check that every array has parameters for the same models - self._check_consistent_params + self._check_consistent_params() # calls setters self.dc_model = dc_model @@ -637,6 +637,8 @@ def orientation_strategy(self, strategy): self._orientation_strategy = strategy def _check_consistent_params(self): + if self.system.num_arrays == 1: + return # check consistent module_parameters params = np.unique( [set(module_parameters.keys()) From cc87fd3727d79ba5351d9d3aedc3d7216aa2ea87 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 30 Nov 2020 15:33:52 -0700 Subject: [PATCH 114/236] Don't test that all combinations of indices are equal Because we are just comparing for equality we can infer from a == b and b == c that a == c and don't have to explicitly check every combination. --- pvlib/modelchain.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 5792b8372c..a6941f1f74 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1694,10 +1694,20 @@ def run_model_from_effective_irradiance(self, data=None): return self +def _pairwise(iterable): + """s -> (s0,s1), (s1,s2), (s2, s3), ... + + From the itertools cookbook. + """ + a, b = itertools.tee(iterable) + next(b, None) + return zip(a, b) + + def _validate_weather_indices(data): indexes = map(lambda df: df.index, data) if not all(itertools.starmap(pd.Index.equals, - itertools.combinations(indexes, 2))): + _pairwise(indexes))): raise ValueError("Weather DataFrames must have same index.") From 926044fa38b2554ec25102599eebcb2bd9524652 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 1 Dec 2020 07:43:07 -0700 Subject: [PATCH 115/236] Use ModelChain.results.dc instead of ModelChain.dc ModelChain.dc will be deprecated when this is merged. --- pvlib/tests/test_modelchain.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index cf93c46945..60c8963302 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -511,7 +511,7 @@ def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays, location): weather_two = pd.DataFrame({'dni': 500, 'ghi': 300, 'dhi': 75}, index=times) mc.run_model((weather_one, weather_two)) - assert (mc.dc[0] != mc.dc[1]).all() + assert (mc.results.dc[0] != mc.results.dc[1]).all() def test_run_model_perez(sapm_dc_snl_ac_system, location): @@ -843,11 +843,11 @@ def test_run_model_from_effective_irradiance_arrays( # arrays have different orientation, but should give same dc power # because we are the same passing effective irradiance and cell # temperature. - assert_frame_equal(mc.dc[0], mc.dc[1]) + assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) data_two = data.copy() data_two['effective_irradiance'] = data['poa_global'] * 0.5 mc.run_model_from_effective_irradiance((data, data_two)) - assert (mc.dc[0] != mc.dc[1]).all().all() + assert (mc.results.dc[0] != mc.results.dc[1]).all().all() def poadc(mc): From a430ab55355faf5776f36fbd39c69739e4387bab Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 1 Dec 2020 09:29:14 -0700 Subject: [PATCH 116/236] Consolidate multi-input validation Perform all validation for multi-array/multi-weather input in a single method, `ModelChain._check_multiple_input()`. --- pvlib/modelchain.py | 52 ++++++++++++++++------------------ pvlib/tests/test_modelchain.py | 41 ++++++++++++++------------- 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index a6941f1f74..7aea3f5651 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1144,9 +1144,7 @@ def complete_irradiance(self, weather): >>> mc = ModelChain(my_system, my_location) # doctest: +SKIP >>> mc.run_model(my_weather) # doctest: +SKIP """ - if isinstance(weather, tuple): - self._check_weather_length(weather) - _validate_weather_indices(weather) + self._check_multiple_input(weather) self.weather = weather self._assign_times() self.results.solar_position = self.location.get_solarposition( @@ -1294,13 +1292,6 @@ def _assign_times(self): else: self.times = self.weather.index - def _check_weather_length(self, data): - if len(data) != self.system.num_arrays: - raise ValueError( - "Weather must be same length as number of arrays in system. " - f"Expected {self.system.num_arrays}, got {len(data)}." - ) - def prepare_inputs(self, weather): """ Prepare the solar position, irradiance, and weather inputs to @@ -1335,9 +1326,7 @@ def prepare_inputs(self, weather): -------- ModelChain.complete_irradiance """ - if isinstance(weather, tuple): - self._check_weather_length(weather) - _validate_weather_indices(weather) + self._check_multiple_input(weather, strict=False) self._verify_df(weather, required=['ghi', 'dni', 'dhi']) self._assign_weather(weather) self._assign_times() @@ -1382,15 +1371,26 @@ def prepare_inputs(self, weather): return self - def _check_length(self, data): + def _check_multiple_input(self, data, strict=True): """Check that the number of elements in `data` is the same as - the number of arrays in `self.system`.""" - if self.system.num_arrays == 1 and not isinstance(data, tuple): + the number of arrays in `self.system`. If `strict` is False then + `data` does not have to be a tuple and length validation is not + performed. If `data` is a tuple of the correct length the indices + of each DataFrame it contains are compared for equality and an + error is raised if they differ. + """ + if (not strict or self.system.num_arrays == 1) \ + and not isinstance(data, tuple): return - if not isinstance(data, tuple) or len(data) != self.system.num_arrays: - raise ValueError("Input must be provided independently for " - "each array. You must pass a tuple of length " - f"{self.system.num_arrays}.") + if strict and not isinstance(data, tuple): + raise ValueError("Input must be a tuple of length " + f"{self.system.num_arrays}, " + f"got {type(data).__name__}.") + if len(data) != self.system.num_arrays: + raise ValueError("Input must be same length as number of arrays " + f"in system. Expected {self.system.num_arrays}, " + f"got {len(data)}.") + _validate_indices(data) def prepare_inputs_from_poa(self, data): """ @@ -1425,9 +1425,7 @@ def prepare_inputs_from_poa(self, data): -------- pvlib.modelchain.ModelChain.prepare_inputs """ - self._check_length(data) - if isinstance(data, tuple): - _validate_weather_indices(data) + self._check_multiple_input(data) self._assign_weather(data) self._verify_df(data, required=['poa_global', 'poa_direct', @@ -1682,9 +1680,7 @@ def run_model_from_effective_irradiance(self, data=None): pvlib.modelchain.ModelChain.run_model_from pvlib.modelchain.ModelChain.run_model_from_poa """ - self._check_length(data) - if isinstance(data, tuple): - _validate_weather_indices(data) + self._check_multiple_input(data) self._assign_weather(data) self._assign_total_irrad(data) self.results.effective_irradiance = _tuple_from_dfs( @@ -1704,11 +1700,11 @@ def _pairwise(iterable): return zip(a, b) -def _validate_weather_indices(data): +def _validate_indices(data): indexes = map(lambda df: df.index, data) if not all(itertools.starmap(pd.Index.equals, _pairwise(indexes))): - raise ValueError("Weather DataFrames must have same index.") + raise ValueError("Input DataFrames must have same index.") def _array_keys(dicts, array): diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 60c8963302..b947f4d5cd 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -442,11 +442,11 @@ def test_prepare_inputs_weather_wrong_length( mc = ModelChain(sapm_dc_snl_ac_system_Array, location) weather = pd.DataFrame({'ghi': [1], 'dhi': [1], 'dni': [1]}) with pytest.raises(ValueError, - match="Weather must be same length as number of arrays " + match="Input must be same length as number of arrays " r"in system\. Expected 2, got 1\."): mc.prepare_inputs((weather,)) with pytest.raises(ValueError, - match="Weather must be same length as number of arrays " + match="Input must be same length as number of arrays " r"in system\. Expected 2, got 3\."): mc.prepare_inputs((weather, weather, weather)) @@ -455,14 +455,14 @@ def test_ModelChain_times_error_arrays(sapm_dc_snl_ac_system_Array, location): """ModelChain.times is assigned a single index given multiple weather DataFrames. """ + error_str = r"Input DataFrames must have same index\." mc = ModelChain(sapm_dc_snl_ac_system_Array, location) irradiance = {'ghi': [1, 2], 'dhi': [1, 2], 'dni': [1, 2]} times_one = pd.date_range(start='1/1/2020', freq='6H', periods=2) times_two = pd.date_range(start='1/1/2020 00:15', freq='6H', periods=2) weather_one = pd.DataFrame(irradiance, index=times_one) weather_two = pd.DataFrame(irradiance, index=times_two) - with pytest.raises(ValueError, match="Weather DataFrames must have " - r"same index\."): + with pytest.raises(ValueError, match=error_str): mc.prepare_inputs((weather_one, weather_two)) # test with overlapping, but differently sized indices. times_three = pd.date_range(start='1/1/2020', freq='6H', periods=3) @@ -471,8 +471,7 @@ def test_ModelChain_times_error_arrays(sapm_dc_snl_ac_system_Array, location): irradiance_three['dhi'].append(3) irradiance_three['dni'].append(3) weather_three = pd.DataFrame(irradiance_three, index=times_three) - with pytest.raises(ValueError, match="Weather DataFrames must have " - r"same index\."): + with pytest.raises(ValueError, match=error_str): mc.prepare_inputs((weather_one, weather_three)) @@ -655,21 +654,22 @@ def test_prepare_inputs_from_poa(sapm_dc_snl_ac_system, location, def test_prepare_poa_wrong_number_arrays( sapm_dc_snl_ac_system_Array, location, total_irrad, weather): - error_str = "Input must be provided independently for each " \ - r"array\. You must pass a tuple of length 2\." + len_error = r"Input must be same length as number of arrays in system\. " \ + r"Expected 2, got [0-9]+\." + type_error = r"Input must be a tuple of length 2, got .*\." mc = ModelChain(sapm_dc_snl_ac_system_Array, location) poa = pd.concat([weather, total_irrad], axis=1) - with pytest.raises(ValueError, match=error_str): + with pytest.raises(ValueError, match=type_error): mc.prepare_inputs_from_poa(poa) - with pytest.raises(ValueError, match=error_str): + with pytest.raises(ValueError, match=len_error): mc.prepare_inputs_from_poa((poa,)) - with pytest.raises(ValueError, match=error_str): + with pytest.raises(ValueError, match=len_error): mc.prepare_inputs_from_poa((poa, poa, poa)) def test_prepare_poa_arrays_different_indices( sapm_dc_snl_ac_system_Array, location, total_irrad, weather): - error_str = r"Weather DataFrames must have same index\." + error_str = r"Input DataFrames must have same index\." mc = ModelChain(sapm_dc_snl_ac_system_Array, location) poa = pd.concat([weather, total_irrad], axis=1) with pytest.raises(ValueError, match=error_str): @@ -817,16 +817,17 @@ def test_run_model_from_effective_irradiance_arrays_error( data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad data['effetive_irradiance'] = data['poa_global'] mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - error_str = r"Input must be provided independently for each array\. " \ - r"You must pass a tuple of length 2\." - with pytest.raises(ValueError, match=error_str): + len_error = r"Input must be same length as number of arrays in system\. " \ + r"Expected 2, got [0-9]+\." + type_error = r"Input must be a tuple of length 2, got DataFrame\." + with pytest.raises(ValueError, match=type_error): mc.run_model_from_effective_irradiance(data) - with pytest.raises(ValueError, match=error_str): + with pytest.raises(ValueError, match=len_error): mc.run_model_from_effective_irradiance((data,)) - with pytest.raises(ValueError, match=error_str): + with pytest.raises(ValueError, match=len_error): mc.run_model_from_effective_irradiance((data, data, data)) with pytest.raises(ValueError, - match=r"Weather DataFrames must have same index\."): + match=r"Input DataFrames must have same index\."): mc.run_model_from_effective_irradiance( (data, data.shift(periods=1, freq='infer')) ) @@ -1461,7 +1462,7 @@ def test_complete_irradiance_arrays( 'ghi': [9, 5]}, index=times) mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) with pytest.raises(ValueError, - match=r"Weather DataFrames must have same index\."): + match=r"Input DataFrames must have same index\."): mc.complete_irradiance((weather, weather[1:])) mc.complete_irradiance((weather, weather)) for mc_weather in mc.weather: @@ -1492,7 +1493,7 @@ def test_complete_irradiance_arrays_wrong_length( weather = pd.DataFrame({'dni': [2, 3], 'dhi': [4, 6], 'ghi': [9, 5]}, index=times) - error_str = "Weather must be same length as number " \ + error_str = "Input must be same length as number " \ r"of arrays in system\. Expected 2, got [0-9]+\." with pytest.raises(ValueError, match=error_str): mc.complete_irradiance((weather,)) From 5c1cdb1e41bf92800ad927e6ae93e0303ddb7744 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 1 Dec 2020 12:43:24 -0700 Subject: [PATCH 117/236] Add tests for multi-array module/string paramters Tests PVSystem.modules_per_string and PVSystem.strings_per_inverter. --- pvlib/tests/test_pvsystem.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 469bd5e679..eca8e76ea2 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1557,6 +1557,30 @@ def test_PVSystem_get_albedo(two_array_system): assert two_array_system.albedo == (0.25, 0.25) +def test_PVSystem_modules_per_string(): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(modules_per_string=1), + pvsystem.Array(modules_per_string=2)] + ) + assert system.modules_per_string == (1, 2) + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(modules_per_string=5)] + ) + assert system.modules_per_string == 5 + + +def test_PVSystem_strings_per_inverter(): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(strings=2), + pvsystem.Array(strings=1)] + ) + assert system.strings_per_inverter == (2, 1) + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(strings=5)] + ) + assert system.strings_per_inverter == 5 + + @fail_on_pvlib_version('0.9') def test_PVSystem_localize_with_location(): system = pvsystem.PVSystem(module='blah', inverter='blarg') From fa5a06f057a0d63bec23cb8370e7d675fa2dc088 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 1 Dec 2020 13:01:09 -0700 Subject: [PATCH 118/236] Refactor all same index check --- pvlib/modelchain.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 7aea3f5651..58fc1f1bd1 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1390,7 +1390,7 @@ def _check_multiple_input(self, data, strict=True): raise ValueError("Input must be same length as number of arrays " f"in system. Expected {self.system.num_arrays}, " f"got {len(data)}.") - _validate_indices(data) + _all_same_index(data) def prepare_inputs_from_poa(self, data): """ @@ -1690,21 +1690,12 @@ def run_model_from_effective_irradiance(self, data=None): return self -def _pairwise(iterable): - """s -> (s0,s1), (s1,s2), (s2, s3), ... - - From the itertools cookbook. - """ - a, b = itertools.tee(iterable) - next(b, None) - return zip(a, b) - - -def _validate_indices(data): +def _all_same_index(data): indexes = map(lambda df: df.index, data) - if not all(itertools.starmap(pd.Index.equals, - _pairwise(indexes))): - raise ValueError("Input DataFrames must have same index.") + next(indexes, None) + for index in indexes: + if not index.equals(data[0].index): + raise ValueError("Input DataFrames must have same index.") def _array_keys(dicts, array): From 5078e9def8e8d5cd867c10b84e110b683f9a20f4 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 1 Dec 2020 13:37:29 -0700 Subject: [PATCH 119/236] Prevent instantiation of SingleAxisTracker with multiple arrays Supporting tracking systems with multiple Arrays is a can of worms that we don't want to open right now. Until support can be added we will raise a ValueError if a user tries to instantiate a tracking system with multiple arrays. --- pvlib/tests/test_tracking.py | 16 +++++++++++++++- pvlib/tracking.py | 18 +++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 1a39d98cf8..5aea3911d4 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -8,7 +8,7 @@ import pvlib from pvlib.location import Location -from pvlib import tracking +from pvlib import tracking, pvsystem from conftest import DATA_DIR SINGLEAXIS_COL_ORDER = ['tracker_theta', 'aoi', @@ -303,6 +303,20 @@ def test_SingleAxisTracker_creation(): assert system.inverter == 'blarg' +def test_SingleAxisTracker_one_array_only(): + system = tracking.SingleAxisTracker( + arrays=[pvsystem.Array(module='foo')] + ) + assert system.module == 'foo' + with pytest.raises(ValueError, + match="SingleAxisTracker does not currently support " + r"multiple arrays\."): + tracking.SingleAxisTracker( + arrays=[pvsystem.Array(module='foo'), + pvsystem.Array(module='bar')] + ) + + def test_SingleAxisTracker_tracking(): system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, axis_azimuth=180, gcr=2.0/7.0, diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 73bc79ceb4..3a76b0518b 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -57,7 +57,13 @@ class SingleAxisTracker(PVSystem): `cross_axis_tilt`. [degrees] **kwargs - Passed to :py:class:`~pvlib.pvsystem.PVSystem`. + Passed to :py:class:`~pvlib.pvsystem.PVSystem`. If the `arrays` + parameter is specified it must have only a single Array. + + Raises + ------ + ValueError + If more than one Array is specified. See also -------- @@ -69,6 +75,7 @@ class SingleAxisTracker(PVSystem): def __init__(self, axis_tilt=0, axis_azimuth=0, max_angle=90, backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0.0, **kwargs): + _ensure_single_array(kwargs) self.axis_tilt = axis_tilt self.axis_azimuth = axis_azimuth self.max_angle = max_angle @@ -239,6 +246,15 @@ def get_irradiance(self, surface_tilt, surface_azimuth, **kwargs) +def _ensure_single_array(kwargs): + """Raise an error if `kwargs` indicates the system has more + than one array.""" + arrays = kwargs.get('arrays', []) + if len(arrays) > 1: + raise ValueError("SingleAxisTracker does not currently support " + "multiple arrays.") + + @deprecated('0.8', alternative='SingleAxisTracker, Location, and ModelChain', name='LocalizedSingleAxisTracker', removal='0.9') class LocalizedSingleAxisTracker(SingleAxisTracker, Location): From a18b1890a67e5cc1dcd5fb3ccd3f075f8572f42d Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 1 Dec 2020 14:03:31 -0700 Subject: [PATCH 120/236] Test that inconsistent array parameters causes error If given a multi-array PVSystem where arrays have inconsistent module or temperature model parameters a ValueError should be raised. --- pvlib/modelchain.py | 2 +- pvlib/tests/test_modelchain.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 58fc1f1bd1..6d21e2c082 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -644,7 +644,7 @@ def _check_consistent_params(self): [set(module_parameters.keys()) for module_parameters in self.system.module_parameters]) if len(params) > 1: - raise ValueError('PVSystem arrays have module_parameters with' + raise ValueError('PVSystem arrays have module_parameters with ' 'different keys.') # check consistent temperature_model_parameters params = np.unique( diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index b947f4d5cd..67bb78b446 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1505,3 +1505,25 @@ def test_unknown_attribute(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location) with pytest.raises(AttributeError): mc.unknown_attribute + + +def test_inconsistent_array_params(location): + module_error = "PVSystem arrays have module_parameters " \ + r"with different keys\." + temperature_error = "PVSystem arrays temperature_model_parameters " \ + r"have different keys\. All arrays should have " \ + r"keys for the same cell temperature model\." + different_module_system = pvsystem.PVSystem( + arrays=[pvsystem.Array(module_parameters={'foo': 1}), + pvsystem.Array(module_parameters={'foo': 2}), + pvsystem.Array(module_parameters={'bar': 1})] + ) + with pytest.raises(ValueError, match=module_error): + _ = ModelChain(different_module_system, location) + different_temp_system = pvsystem.PVSystem( + arrays=[pvsystem.Array(temperature_model_parameters={'a': 1}), + pvsystem.Array(temperature_model_parameters={'b': 2}), + pvsystem.Array(temperature_model_parameters={'b': 3})] + ) + with pytest.raises(ValueError, match=temperature_error): + _ = ModelChain(different_temp_system, location) From e28ac7e58f25f67e206f0b85ff2e355522d1c379 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 1 Dec 2020 14:30:38 -0700 Subject: [PATCH 121/236] Add test coverage for pvsystem.Array._infer_xxxx() methods --- pvlib/tests/test_pvsystem.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index eca8e76ea2..199ac908cd 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -684,6 +684,17 @@ def test_Array__infer_temperature_model_params(): expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'pvsyst']['freestanding'] assert expected == array._infer_temperature_model_params() + array = pvsystem.Array(module_parameters={}, + racking_model='insulated', + module_type=None) + expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ + 'pvsyst']['insulated'] + assert expected == array._infer_temperature_model_params() + + +def test_Array__infer_cell_type(): + array = pvsystem.Array(module_parameters={}) + assert array._infer_cell_type() is None def test_calcparams_desoto(cec_module_params): From e1be2bfeed07ff82afd6b6027aa21f3f4df8d590 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 1 Dec 2020 15:30:11 -0700 Subject: [PATCH 122/236] Add pvsystem.Array to Classes section of api.rst --- docs/sphinx/source/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 845ad393fa..e881216a81 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -20,6 +20,7 @@ corresponding procedural code. location.Location pvsystem.PVSystem + pvsystem.Array tracking.SingleAxisTracker modelchain.ModelChain From 0301ce778e2aed45d691640c7e86b0ab47482bda Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 09:12:08 -0700 Subject: [PATCH 123/236] Add PVSystem.sandia_multi() method Adds a wrapper for the `pvlib.inverter.sandia_multi` inverter model to PVSystem. --- pvlib/pvsystem.py | 13 +++++++++++ pvlib/tests/test_pvsystem.py | 45 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index a1c7240637..87db25db89 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -862,6 +862,19 @@ def snlinverter(self, v_dc, p_dc): """ return inverter.sandia(v_dc, p_dc, self.inverter_parameters) + def sandia_multi(self, v_dc, p_dc): + """Uses :py:func:`pvlib.inverter.sandia_multi` to calculate AC power + based on ``self.inverter_parameters`` and the input voltage and power. + + The parameters `v_dc` and `p_dc` must be tuples of the same length as + ``self.num_arrays`` if the system has more than one array. + + See :py:func:`pvlib.inverter.sandia_multi` for details. + """ + v_dc = self._validate_per_array(v_dc) + p_dc = self._validate_per_array(p_dc) + return inverter.sandia_multi(v_dc, p_dc, self.inverter_parameters) + def adrinverter(self, v_dc, p_dc): """Uses :py:func:`pvlib.inverter.adr` to calculate AC power based on ``self.inverter_parameters`` and the input voltage and power. diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 199ac908cd..430c3f6e9d 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1389,6 +1389,51 @@ def test_PVSystem_snlinverter(cec_inverter_parameters): assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000])) +def test_PVSystem_sandia_multi(cec_inverter_parameters): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(), pvsystem.Array()], + inverter=cec_inverter_parameters['Name'], + inverter_parameters=cec_inverter_parameters, + ) + vdcs = pd.Series(np.linspace(0, 50, 3)) + idcs = pd.Series(np.linspace(0, 11, 3)) / 2 + pdcs = idcs * vdcs + pacs = system.sandia_multi((vdcs, vdcs), (pdcs, pdcs)) + assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000])) + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.sandia_multi(vdcs, (pdcs, pdcs)) + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.sandia_multi(vdcs, (pdcs,)) + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.sandia_multi((vdcs, vdcs), (pdcs, pdcs, pdcs)) + + +def test_PVSystem_sandia_multi_single_array(cec_inverter_parameters): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array()], + inverter=cec_inverter_parameters['Name'], + inverter_parameters=cec_inverter_parameters, + ) + vdcs = pd.Series(np.linspace(0,50,3)) + idcs = pd.Series(np.linspace(0,11,3)) + pdcs = idcs * vdcs + + pacs = system.sandia_multi(vdcs, pdcs) + assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000])) + pacs = system.sandia_multi((vdcs,), (pdcs,)) + assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000])) + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.sandia_multi((vdcs, vdcs), pdcs) + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.sandia_multi((vdcs,), (pdcs, pdcs)) + + + def test_PVSystem_creation(): pv_system = pvsystem.PVSystem(module='blah', inverter='blarg') # ensure that parameter attributes are dict-like. GH 294 From 08946f8110da941263c3e03c2b9cfef510ba2b6b Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 09:14:38 -0700 Subject: [PATCH 124/236] Fix whitespace --- pvlib/tests/test_pvsystem.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 430c3f6e9d..afa6a001b1 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1417,8 +1417,8 @@ def test_PVSystem_sandia_multi_single_array(cec_inverter_parameters): inverter=cec_inverter_parameters['Name'], inverter_parameters=cec_inverter_parameters, ) - vdcs = pd.Series(np.linspace(0,50,3)) - idcs = pd.Series(np.linspace(0,11,3)) + vdcs = pd.Series(np.linspace(0, 50, 3)) + idcs = pd.Series(np.linspace(0, 11, 3)) pdcs = idcs * vdcs pacs = system.sandia_multi(vdcs, pdcs) @@ -1433,7 +1433,6 @@ def test_PVSystem_sandia_multi_single_array(cec_inverter_parameters): system.sandia_multi((vdcs,), (pdcs, pdcs)) - def test_PVSystem_creation(): pv_system = pvsystem.PVSystem(module='blah', inverter='blarg') # ensure that parameter attributes are dict-like. GH 294 From 51679010482b70fb2bb6d9c7cf5105a3f80d6097 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 09:52:40 -0700 Subject: [PATCH 125/236] Add 'sandia_multi' as a valid key for ModelChain.ac_model Adds the ModelChain.sandia_multi_inverter() method which calls PVSystem.sandia_multi to set ModelChain.results.ac. --- pvlib/modelchain.py | 9 +++++++++ pvlib/tests/test_modelchain.py | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 6d21e2c082..442066c804 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -787,6 +787,8 @@ def ac_model(self, model): " ac_model = 'sandia' instead.", pvlibDeprecationWarning) self._ac_model = self.snlinverter + elif model == 'sandia_multi': + self._ac_model = self.sandia_multi_inverter elif model in ['adr', 'adrinverter']: if model == 'adrinverter': warnings.warn("ac_model = 'adrinverter' is deprecated and" @@ -816,6 +818,13 @@ def infer_ac_model(self): 'system.inverter_parameters or explicitly ' 'set the model with the ac_model kwarg.') + def sandia_multi_inverter(self): + self.results.ac = self.system.sandia_multi( + _tuple_from_dfs(self.results.dc, 'v_mp'), + _tuple_from_dfs(self.results.dc, 'p_mp') + ) + return self + def snlinverter(self): self.results.ac = self.system.snlinverter(self.results.dc['v_mp'], self.results.dc['p_mp']) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 67bb78b446..b572820677 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -998,14 +998,17 @@ def acdc(mc): mc.results.ac = mc.results.dc -@pytest.mark.parametrize('ac_model', ['sandia', 'adr', 'pvwatts']) +@pytest.mark.parametrize('ac_model', ['sandia', 'adr', + 'pvwatts', 'sandia_multi']) def test_ac_models(sapm_dc_snl_ac_system, cec_dc_adr_ac_system, pvwatts_dc_pvwatts_ac_system, location, ac_model, weather, mocker): ac_systems = {'sandia': sapm_dc_snl_ac_system, + 'sandia_multi': sapm_dc_snl_ac_system, 'adr': cec_dc_adr_ac_system, 'pvwatts': pvwatts_dc_pvwatts_ac_system} ac_method_name = {'sandia': 'snlinverter', + 'sandia_multi': 'sandia_multi', 'adr': 'adrinverter', 'pvwatts': 'pvwatts_ac'} system = ac_systems[ac_model] From db6d73628e0b3816e33b2cf9df55771e5d2efca4 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 10:37:28 -0700 Subject: [PATCH 126/236] Add sandia_multi_inverter to ModelChain.infer_ac_model() Since no other inverter models currently support multiple dc inputs we raise a value error whenever the system has more than one array and is missing the inverter parameters required for sandia_multi_inverter. --- pvlib/modelchain.py | 9 ++++++--- pvlib/tests/test_modelchain.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 442066c804..0286f24215 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -806,11 +806,14 @@ def ac_model(self, model): def infer_ac_model(self): """Infer AC power model from system attributes.""" inverter_params = set(self.system.inverter_parameters.keys()) - if {'C0', 'C1', 'C2'} <= inverter_params: + single_array = self.system.num_arrays == 1 + if not single_array and {'C0', 'C1', 'C2'} <= inverter_params: + return self.sandia_multi_inverter + elif single_array and {'C0', 'C1', 'C2'} <= inverter_params: return self.snlinverter - elif {'ADRCoefficients'} <= inverter_params: + elif single_array and {'ADRCoefficients'} <= inverter_params: return self.adrinverter - elif {'pdc0'} <= inverter_params: + elif single_array and {'pdc0'} <= inverter_params: return self.pvwatts_inverter else: raise ValueError('could not infer AC model from ' diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index b572820677..c138d33499 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -411,6 +411,18 @@ def test_run_model_from_irradiance_arrays_no_loss( ) +@pytest.mark.parametrize('inverter', ['adr', 'pvwatts']) +def test_ModelChain_invalid_inverter_params_arrays( + inverter, sapm_dc_snl_ac_system_same_arrays, + location, adr_inverter_parameters): + inverter_params = {'adr': adr_inverter_parameters, + 'pvwatts': {'pdc0': 220, 'eta_inv_nom': 0.95}} + sapm_dc_snl_ac_system_same_arrays.inverter_parameters = \ + inverter_params[inverter] + with pytest.raises(ValueError, match='could not infer AC model from'): + ModelChain(sapm_dc_snl_ac_system_same_arrays, location) + + def test_prepare_inputs_no_irradiance(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location) weather = pd.DataFrame() From 99a63e7a4cd3eba4db3927876ce0ee578a8f9b97 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 12:29:22 -0700 Subject: [PATCH 127/236] Update ModelChain tests to use sandia_multi inverter model --- pvlib/tests/test_modelchain.py | 36 +++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index c138d33499..188fd3a6ce 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -341,10 +341,11 @@ def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location): @pytest.fixture(scope='function') -def multi_array_pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m): - module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} +def multi_array_sapm_dc_snl_ac_system( + sapm_temperature_cs5p_220m, sapm_module_params, cec_inverter_parameters): + module_parameters = sapm_module_params temp_model_parameters = sapm_temperature_cs5p_220m.copy() - inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} + inverter_parameters = cec_inverter_parameters array_one = pvsystem.Array( surface_tilt=32.2, surface_azimuth=180, module_parameters=module_parameters, @@ -373,23 +374,23 @@ def multi_array_pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m): def test_run_model_from_irradiance_arrays_no_loss( - multi_array_pvwatts_dc_pvwatts_ac_system, location): + multi_array_sapm_dc_snl_ac_system, location): mc_both = ModelChain( - multi_array_pvwatts_dc_pvwatts_ac_system['two_array_system'], + multi_array_sapm_dc_snl_ac_system['two_array_system'], location, aoi_model='no_loss', spectral_model='no_loss', losses_model='no_loss' ) mc_one = ModelChain( - multi_array_pvwatts_dc_pvwatts_ac_system['array_one_system'], + multi_array_sapm_dc_snl_ac_system['array_one_system'], location, aoi_model='no_loss', spectral_model='no_loss', losses_model='no_loss' ) mc_two = ModelChain( - multi_array_pvwatts_dc_pvwatts_ac_system['array_two_system'], + multi_array_sapm_dc_snl_ac_system['array_two_system'], location, aoi_model='no_loss', spectral_model='no_loss', @@ -401,11 +402,11 @@ def test_run_model_from_irradiance_arrays_no_loss( mc_one.run_model(irradiance) mc_two.run_model(irradiance) mc_both.run_model(irradiance) - assert_series_equal( + assert_frame_equal( mc_both.results.dc[0], mc_one.results.dc ) - assert_series_equal( + assert_frame_equal( mc_both.results.dc[1], mc_two.results.dc ) @@ -516,13 +517,18 @@ def test_prepare_inputs_missing_irrad_component( def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays, location): mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) - times = pd.date_range('20200101 1200-0700', periods=2, freq='6H') - weather_one = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, + times = pd.date_range('20200101 1200-0700', periods=2, freq='2H') + weather_one = pd.DataFrame({'dni': [900, 800], + 'ghi': [600, 500], + 'dhi': [150, 100]}, index=times) - weather_two = pd.DataFrame({'dni': 500, 'ghi': 300, 'dhi': 75}, + weather_two = pd.DataFrame({'dni': [500, 400], + 'ghi': [300, 200], + 'dhi': [75, 65]}, index=times) mc.run_model((weather_one, weather_two)) - assert (mc.results.dc[0] != mc.results.dc[1]).all() + assert (mc.results.dc[0] != mc.results.dc[1]).all().all() + assert not mc.results.ac.empty def test_run_model_perez(sapm_dc_snl_ac_system, location): @@ -1258,8 +1264,10 @@ def test_bad_get_orientation(): # tests for PVSystem with multiple Arrays def test_with_sapm_pvsystem_arrays(sapm_dc_snl_ac_system_Array, location, weather): - mc = ModelChain.with_sapm(sapm_dc_snl_ac_system_Array, location) + mc = ModelChain.with_sapm(sapm_dc_snl_ac_system_Array, location, + ac_model='sandia_multi') assert mc.dc_model == mc.sapm + assert mc.ac_model == mc.sandia_multi_inverter mc.run_model(weather) assert mc.results From 85ee956462288e45f016d4d6c3012ecc8beefe16 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 12:31:58 -0700 Subject: [PATCH 128/236] Shorten long line --- pvlib/tests/test_modelchain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 188fd3a6ce..706d79650a 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -342,7 +342,8 @@ def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location): @pytest.fixture(scope='function') def multi_array_sapm_dc_snl_ac_system( - sapm_temperature_cs5p_220m, sapm_module_params, cec_inverter_parameters): + sapm_temperature_cs5p_220m, sapm_module_params, + cec_inverter_parameters): module_parameters = sapm_module_params temp_model_parameters = sapm_temperature_cs5p_220m.copy() inverter_parameters = cec_inverter_parameters From cf6e94ccacee8bd88348b095319d8801c02766de Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 12:48:45 -0700 Subject: [PATCH 129/236] Support multiple arrays in ModelChain.pvwatts_losses --- pvlib/modelchain.py | 7 +++++-- pvlib/tests/test_modelchain.py | 14 ++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 0286f24215..f55a20bb0b 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1075,8 +1075,11 @@ def infer_losses_model(self): def pvwatts_losses(self): self.losses = (100 - self.system.pvwatts_losses()) / 100. - # TODO handle multiple arrays - self.results.dc *= self.losses + if self.system.num_arrays > 1: + for dc in self.results.dc: + dc *= self.losses + else: + self.results.dc *= self.losses return self def no_extra_losses(self): diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 706d79650a..f95b087c71 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1181,21 +1181,23 @@ def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather, assert not np.allclose(mc.results.dc, dc_with_loss, equal_nan=True) -def test_losses_models_pvwatts_arrays(multi_array_pvwatts_dc_pvwatts_ac_system, +def test_losses_models_pvwatts_arrays(multi_array_sapm_dc_snl_ac_system, location, weather): age = 1 - system_both = multi_array_pvwatts_dc_pvwatts_ac_system['two_array_system'] + system_both = multi_array_sapm_dc_snl_ac_system['two_array_system'] system_both.losses_parameters = dict(age=age) - mc = ModelChain(system_both, location, dc_model='pvwatts', + mc = ModelChain(system_both, location, aoi_model='no_loss', spectral_model='no_loss', losses_model='pvwatts') mc.run_model(weather) dc_with_loss = mc.results.dc - mc = ModelChain(system_both, location, dc_model='pvwatts', + mc = ModelChain(system_both, location, aoi_model='no_loss', spectral_model='no_loss', - losses_model='pvwatts') + losses_model='no_loss') mc.run_model(weather) - assert not np.allclose(mc.results.dc, dc_with_loss, equal_nan=True) + assert not np.allclose(mc.results.dc[0], dc_with_loss[0], equal_nan=True) + assert not np.allclose(mc.results.dc[1], dc_with_loss[1], equal_nan=True) + assert not mc.results.ac.empty def test_losses_models_ext_def(pvwatts_dc_pvwatts_ac_system, location, weather, From f5f309d2a64976db426f361780a4e0fe38b8de52 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 12:55:58 -0700 Subject: [PATCH 130/236] Decorate test_complete_irradiance_arrays with @requires_tables --- pvlib/tests/test_modelchain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index f95b087c71..9f2be936e6 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1478,6 +1478,7 @@ def test_complete_irradiance(sapm_dc_snl_ac_system, location): @pytest.mark.filterwarnings("ignore:This function is not safe at the moment") +@requires_tables def test_complete_irradiance_arrays( sapm_dc_snl_ac_system_same_arrays, location): """ModelChain.complete_irradiance can accept a tuple of weather From a0047a41414fdd972477c15785fd99963362a1cc Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 13:12:27 -0700 Subject: [PATCH 131/236] Pass explicit freq to DataFrame.shift() in poa_arrays test --- pvlib/tests/test_modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 9f2be936e6..9154af73b6 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -692,7 +692,7 @@ def test_prepare_poa_arrays_different_indices( mc = ModelChain(sapm_dc_snl_ac_system_Array, location) poa = pd.concat([weather, total_irrad], axis=1) with pytest.raises(ValueError, match=error_str): - mc.prepare_inputs_from_poa((poa, poa.shift(periods=1, freq='infer'))) + mc.prepare_inputs_from_poa((poa, poa.shift(periods=1, freq='6H'))) def test_prepare_poa_arrays_missing_column( From e58db3a13b0b014750f2efb6fd2dee81d7fb13af Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 13:23:36 -0700 Subject: [PATCH 132/236] Pass explicit freq to DataFrame.shift() in ModelChain tests In test_run_model_from_effective_irradiance_arrays_error() A different index is generated by shifting the original input. Later versions of pandas support freq='infer', but not the minimum supported version for pvlib. --- pvlib/tests/test_modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 9154af73b6..ff53e1d4e1 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -848,7 +848,7 @@ def test_run_model_from_effective_irradiance_arrays_error( with pytest.raises(ValueError, match=r"Input DataFrames must have same index\."): mc.run_model_from_effective_irradiance( - (data, data.shift(periods=1, freq='infer')) + (data, data.shift(periods=1, freq='6H')) ) From 54fc167246950ece59cef8bc059b304a9e171a33 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 14:16:46 -0700 Subject: [PATCH 133/236] Add description of multi-array PVSystem to pvsystem.rst --- docs/sphinx/source/pvsystem.rst | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 52b59746e7..965508e30d 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -166,6 +166,54 @@ modules each. print(data_scaled) +.. _multiarray: + +PVSystem with multiple Arrays +----------------------------- + +It is possible to model a system with multiple arrays by passing a list of +:py:class:`~pvlib.pvsystem.Array` to the :py:class:`~pvlib.pvsystem.PVSystem` +constructor. The :py:class:`~pvlib.pvsystem.Array` class subsumes all the +:py:class:`~pvlib.pvsystem.PVSystem` attributes that might differ from array +to array. These attributes include `surface_tilt`, `surface_azimuth`, +`module_parameters`, `temperature_model_parameters`, `modules_per_string`, +`strings_per_inverter`, `albedo`, `surface_type`, `module_type`, and +`racking_model`. + +.. ipython:: python + + array_one = pvsystem.Array(surface_tilt=30, surface_azimuth=90) + array_two = pvsystem.Array(surface_tilt=30, surface_azimuth=220) + system = pvsystem.PVSystem(arrays=[array_one, array_two]) + system.num_arrays + + +When instantiating a :py:class:`~pvlib.pvsystem.PVSystem` by passing a list +of arrays, as above, you may not pass any of the above parameters to the +`PVSystem` constructor. Each non-default parameter must be specified +individually for each array. + +The output of `PVSystem` methods and attributes changes when the system has +multiple arrays. For any of the attributes listed above, accessing them will +return a tuple with the value of the attribute for each array, specified in +the same order as the `arrays` parameter. For example, using the system +constructed above: + +.. ipython:: python + + system.surface_tilt + system.surface_azimuth + +Similarly, other `PVSystem` methods that either return values that vary +depending on array characteristics, or take values that may differ between +arrays return (or accept) tuples. + +.. ipython:: python + + aoi = system.get_aoi(30, 180) + print(aoi) + system.get_iam(aoi) + .. _sat: SingleAxisTracker From ca77bef0160b1bfbcfcdf0e970ad68ed58b06421 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 14:53:30 -0700 Subject: [PATCH 134/236] Remove commented-out code in ModelChainResults --- pvlib/modelchain.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index f55a20bb0b..661dbb9fdf 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -269,7 +269,6 @@ class ModelChainResult: T = TypeVar('T') PerArray = Union[T, Tuple[T, ...]] # system-level information - # weather: pd.DataFrame = field(default=None) solar_position: pd.DataFrame = field(default=None) airmass: pd.DataFrame = field(default=None) ac: pd.Series = field(default=None) @@ -281,7 +280,6 @@ class ModelChainResult: cell_temperature: Optional[PerArray[pd.Series]] = field(default=None) effective_irradiance: Optional[PerArray[pd.Series]] = field(default=None) dc: Optional[PerArray[pd.Series]] = field(default=None) - # losses: dont_know_tye_type = field(default=None) array_ac: Optional[PerArray[pd.Series]] = field(default=None) diode_params: Optional[PerArray[pd.DataFrame]] = field(default=None) From e4da96b2582e0e0cf95e09d8978ef40bbda5836a Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 14:55:07 -0700 Subject: [PATCH 135/236] Docstring edits --- pvlib/modelchain.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 661dbb9fdf..31f1781914 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1121,7 +1121,7 @@ def complete_irradiance(self, weather): ``'wind_speed'``, ``'temp_air'``. All irradiance components are required. Air temperature of 20 C and wind speed of 0 m/s will be added to the DataFrame if not provided. - If weather is a tuple it must be the same length as the number + If `weather` is a tuple it must be the same length as the number of arrays in the system and the indices for each DataFrame must be the same. @@ -1670,7 +1670,7 @@ def run_model_from_effective_irradiance(self, data=None): If the system has multiple arrays, `data` must be a tuple with the same length as the number of arrays in the system where each element provides the effective irradiance and weather - for each array. + for the corresponding array. Returns ------- @@ -1720,9 +1720,11 @@ def _array_keys(dicts, array): def _tuple_from_dfs(dfs, name): - ''' Extract a column from each df in dfs, return as Series or tuple of - Series - ''' + """Extract a column from each DataFrame in `dfs` if `dfs` is a tuple. + + Returns a tuple of Series if `dfs` is a tuple or a Series if `dfs` is + a DataFrame. + """ if isinstance(dfs, tuple): return tuple(df[name] for df in dfs) else: From a43125f34476f5ef597f7e3e8fa6578841b482bd Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 14:59:12 -0700 Subject: [PATCH 136/236] Remove stray whitespace from _tuple_from_dfs() docstring --- pvlib/modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 31f1781914..e0198958f4 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1721,7 +1721,7 @@ def _array_keys(dicts, array): def _tuple_from_dfs(dfs, name): """Extract a column from each DataFrame in `dfs` if `dfs` is a tuple. - + Returns a tuple of Series if `dfs` is a tuple or a Series if `dfs` is a DataFrame. """ From 413899047eef31ce1596137ccb039eacd48e485c Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 4 Dec 2020 15:03:01 -0700 Subject: [PATCH 137/236] Add docstring to ModelChain._check_consistent_params() --- pvlib/modelchain.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index e0198958f4..2ea64fa9aa 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -635,6 +635,9 @@ def orientation_strategy(self, strategy): self._orientation_strategy = strategy def _check_consistent_params(self): + """Ensure that each all arrays in ``self.system`` have the same + module and temperature model parameters. If parameters differ + a ValueError is raised.""" if self.system.num_arrays == 1: return # check consistent module_parameters From 03b9795cbd53c7461882101743a80d1320dba0c2 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 7 Dec 2020 12:07:35 -0700 Subject: [PATCH 138/236] Improvements in pvsystem.rst Co-authored-by: Cliff Hansen --- docs/sphinx/source/pvsystem.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 965508e30d..72f89cafcb 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -204,13 +204,12 @@ constructed above: system.surface_tilt system.surface_azimuth -Similarly, other `PVSystem` methods that either return values that vary -depending on array characteristics, or take values that may differ between -arrays return (or accept) tuples. +Similarly, other `PVSystem` methods expect tuples as input and return tuples +for values that differ among arrays. .. ipython:: python - aoi = system.get_aoi(30, 180) + aoi = system.get_aoi(solar_zenith=30, solar_azimuth=180) print(aoi) system.get_iam(aoi) @@ -224,4 +223,4 @@ The :py:class:`~pvlib.tracking.SingleAxisTracker` is a subclass of includes a few more keyword arguments and attributes that are specific to trackers, plus the :py:meth:`~pvlib.tracking.SingleAxisTracker.singleaxis` method. It also -overrides the `get_aoi` and `get_irradiance` methods. \ No newline at end of file +overrides the `get_aoi` and `get_irradiance` methods. From d542241d8a65a043b7d16636839c88706c464db6 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 7 Dec 2020 12:20:28 -0700 Subject: [PATCH 139/236] Docstring edits in pvsystem.py Co-authored-by: Cliff Hansen --- pvlib/pvsystem.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 87db25db89..417ee209d3 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -262,14 +262,13 @@ def _validate_per_array(self, values, system_wide=False): """Check that `values` is a tuple of the same length as `self._arrays`. - If it is a single vlaue it is packed in to a length-1 tuple before - the check. If the length is not the same a ValueError is raised, - otherwise the tuple is returned. - - When `system_wide` is True, single values are accepted even if there - is more than one Array. In this case the single value is replicated - in a tuple of the same length as `self._arrays` and that tuple is - returned. + If `values` is not a tuple it is packed in to a length-1 tuple before + the check. If the lengths are not the same a ValueError is raised, + otherwise the tuple `values` is returned. + + When `system_wide` is True and `values` is not a tuple, `values` + is replicated to a tuple of the same length as `self._arrays` and that + tuple is returned. """ if system_wide and not isinstance(values, tuple): return (values,) * self.num_arrays @@ -1096,7 +1095,7 @@ class Array: """ An Array is a set of of modules at a specific orientation. - Specifically, an array is defined by a tilt, azimuth, the + Specifically, an array is defined by tilt, azimuth, the module parameters, the number of strings of modules and the number of modules on each string. @@ -1113,8 +1112,8 @@ class Array: albedo : None or float, default None The ground albedo. If ``None``, will attempt to use - ``surface_type`` and ``irradiance.SURFACE_ALBEDOS`` - to lookup albedo. + ``surface_type`` to look up an albedo value in + ``irradiance.SURFACE_ALBEDOS`` surface_type : None or string, default None The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` @@ -1130,16 +1129,16 @@ class Array: 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. + Parameters for the module model, e.g., SAPM, CEC, or other. temperature_model_parameters : None, dict or Series, default None. - Temperature model parameters as defined by the SAPM, Pvsyst, or other. + Parameters for the module temperature model, e.g., SAPM, Pvsyst, or other. modules_per_string: int, default 1 Number of modules per string in the array. strings: int, default 1 - Number of strings in the array. + Number of parallel strings in the array. racking_model : None or string, default 'open_rack' Valid strings are 'open_rack', 'close_mount', and 'insulated_back'. From 555c830d0740b4e14a2be7496d79721cdd66d14f Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 7 Dec 2020 12:42:35 -0700 Subject: [PATCH 140/236] Shorten line in pvsystem.Array docstring --- pvlib/pvsystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 417ee209d3..e7d0dc8d47 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1132,7 +1132,8 @@ class Array: Parameters for the module model, e.g., SAPM, CEC, or other. temperature_model_parameters : None, dict or Series, default None. - Parameters for the module temperature model, e.g., SAPM, Pvsyst, or other. + Parameters for the module temperature model, e.g., SAPM, Pvsyst, or + other. modules_per_string: int, default 1 Number of modules per string in the array. From 6c4e9f0771485b24cddabe0ebb821c75abe241c0 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 8 Dec 2020 07:27:38 -0700 Subject: [PATCH 141/236] Fix typo in ModelChain.complete_irradiance() docstring Co-authored-by: Will Holmgren --- pvlib/modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 2ea64fa9aa..fdf5a21b23 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1119,7 +1119,7 @@ def complete_irradiance(self, weather): Parameters ---------- - weather : DataFrame or tuple of DatFrame + weather : DataFrame or tuple of DataFrame Column names must be ``'dni'``, ``'ghi'``, ``'dhi'``, ``'wind_speed'``, ``'temp_air'``. All irradiance components are required. Air temperature of 20 C and wind speed From 494560cf8e9c6f3163b441cd971b6fa193fa7a22 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 8 Dec 2020 09:07:41 -0700 Subject: [PATCH 142/236] Avoid use of `inplace=True` in ModelChain._singlediode() --- pvlib/modelchain.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index fdf5a21b23..e31afce717 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -750,10 +750,9 @@ def _make_diode_params(photocurrent, saturation_current, self.results.dc ) if self.system.num_arrays == 1: - self.results.dc.fillna(0, inplace=True) + self.results.dc = self.results.dc.fillna(0) else: - for dc in self.results.dc: - dc.fillna(0, inplace=True) + self.results.dc = tuple(dc.fillna(0) for dc in self.results.dc) return self def desoto(self): From 8960f25943f6a68d0213e33a173ddab843075144 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 8 Dec 2020 09:37:30 -0700 Subject: [PATCH 143/236] Make list of deprecated ModelChain attrs a class variable --- pvlib/modelchain.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index e31afce717..6e2d97696a 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -359,6 +359,12 @@ class ModelChain: Name of ModelChain instance. """ + # list of deprecated attributes + _deprecated_attrs = ['solar_position', 'airmass', 'total_irrad', + 'aoi', 'aoi_modifier', 'spectral_modifier', + 'cell_temperature', 'effective_irradiance', + 'dc', 'ac', 'diode_params'] + def __init__(self, system, location, orientation_strategy=None, clearsky_model='ineichen', @@ -403,12 +409,7 @@ def __init__(self, system, location, ) def __getattr__(self, key): - # here to deprecate old attributes - deprecated_attrs = ['solar_position', 'airmass', 'total_irrad', - 'aoi', 'aoi_modifier', 'spectral_modifier', - 'cell_temperature', 'effective_irradiance', - 'dc', 'diode_params'] - if key in deprecated_attrs: + if key in ModelChain._deprecated_attrs: msg = f'ModelChain.{key} is deprecated and will' \ f' be removed in v1.0. Use' \ f' ModelChain.results.{key} instead' @@ -421,12 +422,7 @@ def __getattr__(self, key): raise AttributeError def __setattr__(self, key, value): - # here to deprecate old attributes - deprecated_attrs = ['solar_position', 'airmass', 'total_irrad', - 'aoi', 'aoi_modifier', 'spectral_modifier', - 'cell_temperature', 'effective_irradiance', - 'dc', 'diode_params'] - if key in deprecated_attrs: + if key in ModelChain._deprecated_attrs: msg = f'ModelChain.{key} is deprecated from v0.9. Use' \ f' ModelChain.results.{key} instead' warnings.warn(msg, pvlibDeprecationWarning) From ed2c72aa136a068bffd4bd83ea32b773077537df Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 8 Dec 2020 09:53:17 -0700 Subject: [PATCH 144/236] Include 'dataclasses' as a requirement in setup.py Only included if python version is 3.6 since dataclasses is part of the standard library for 3.7+. --- ci/azure/posix.yml | 4 ---- setup.py | 6 ++++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ci/azure/posix.yml b/ci/azure/posix.yml index fc35137293..806cd27e77 100644 --- a/ci/azure/posix.yml +++ b/ci/azure/posix.yml @@ -20,10 +20,6 @@ jobs: inputs: versionSpec: '$(python.version)' - - script: pip install dataclasses - condition: eq(variables['python.version'], '3.6') - displayName: Install dataclasses if python 3.6 - - script: | pip install pytest pytest-cov pytest-mock pytest-timeout pytest-azurepipelines pytest-rerunfailures pytest-remotedata pip install -e . diff --git a/setup.py b/setup.py index b89a3c9983..ab718c9d9c 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import os +import sys try: from setuptools import setup @@ -42,6 +43,11 @@ 'pytz', 'requests', 'scipy >= 1.2.0'] + +# include dataclasses as a dependency only on python 3.6 +if sys.version_info.major == 3 and sys.version_info.minor == 6: + INSTALL_REQUIRES.append('dataclasses') + TESTS_REQUIRE = ['nose', 'pytest', 'pytest-cov', 'pytest-mock', 'pytest-timeout', 'pytest-rerunfailures', 'pytest-remotedata'] EXTRAS_REQUIRE = { From 42e096a0cde6d6b15e416a5f3c5955aa27eb6e83 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 8 Dec 2020 10:08:56 -0700 Subject: [PATCH 145/236] Remove dataclasses from CI environment for python 3.7+ --- ci/requirements-py37.yml | 1 - ci/requirements-py38.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/ci/requirements-py37.yml b/ci/requirements-py37.yml index 79dfa233ad..1542bb35d9 100644 --- a/ci/requirements-py37.yml +++ b/ci/requirements-py37.yml @@ -5,7 +5,6 @@ channels: dependencies: - coveralls - cython - - dataclasses - ephem - netcdf4 - nose diff --git a/ci/requirements-py38.yml b/ci/requirements-py38.yml index 661938e8a5..6db508fd53 100644 --- a/ci/requirements-py38.yml +++ b/ci/requirements-py38.yml @@ -6,7 +6,6 @@ dependencies: - coveralls - cython - ephem - - dataclasses - netcdf4 - nose - numba From c03625bdbbe8e392168d2dbcff69acb40c9c0722 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 8 Dec 2020 13:04:25 -0700 Subject: [PATCH 146/236] Replace modelchain._array_keys() with modelchain._common_keys() _array_keys() just returned the keys of one specific array, what we actually need is to return the set of keys that are common to every dict in a tuple. --- pvlib/modelchain.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 6e2d97696a..3bbe6bdcd8 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -670,7 +670,7 @@ def dc_model(self, model): # validate module parameters missing_params = ( _DC_MODEL_PARAMS[model] - - _array_keys(self.system.module_parameters, 0)) + _common_keys(self.system.module_parameters)) if missing_params: # some parameters are not in module.keys() raise ValueError(model + ' selected for the DC model but ' 'one or more required parameters are ' @@ -692,7 +692,7 @@ def dc_model(self, model): def infer_dc_model(self): """Infer DC power model from array module parameters.""" - params = _array_keys(self.system.module_parameters, 0) + params = _common_keys(self.system.module_parameters) if {'A0', 'A1', 'C7'} <= params: return self.sapm, 'sapm' elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', @@ -864,7 +864,7 @@ def aoi_model(self, model): self._aoi_model = partial(model, self) def infer_aoi_model(self): - params = _array_keys(self.system.module_parameters, 0) + params = _common_keys(self.system.module_parameters) if {'K', 'L', 'n'} <= params: return self.physical_aoi_loss elif {'B5', 'B4', 'B3', 'B2', 'B1', 'B0'} <= params: @@ -931,7 +931,7 @@ def spectral_model(self, model): def infer_spectral_model(self): """Infer spectral model from system attributes.""" - params = _array_keys(self.system.module_parameters, 0) + params = _common_keys(self.system.module_parameters) if {'A4', 'A3', 'A2', 'A1', 'A0'} <= params: return self.sapm_spectral_loss elif ((('Technology' in params or @@ -991,13 +991,13 @@ def temperature_model(self, model): raise ValueError( f'Temperature model {self._temperature_model.__name__} is' f'inconsistent with PVSystem temperature model parameters' - f'{_array_keys(self.system.temperature_model_parameters, 0)}') # noqa: E501 + f'{_common_keys(self.system.temperature_model_parameters)}') # noqa: E501 else: self._temperature_model = partial(model, self) def infer_temperature_model(self): """Infer temperature model from system attributes.""" - params = _array_keys(self.system.temperature_model_parameters, 0) + params = _common_keys(self.system.temperature_model_parameters) # remove or statement in v0.9 if {'a', 'b', 'deltaT'} <= params or ( not params and self.system.racking_model is None @@ -1709,11 +1709,9 @@ def _all_same_index(data): raise ValueError("Input DataFrames must have same index.") -def _array_keys(dicts, array): - """Return a set of keys from element `array` of `dicts` if it is a tuple - otherwise return the set of keys in dicts.""" +def _common_keys(dicts): if isinstance(dicts, tuple): - return set(dicts[array]) + return set.intersection(*map(set, dicts)) return set(dicts) From a7567c9378070a02130a37e812b40d961c2890ec Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 8 Dec 2020 13:19:06 -0700 Subject: [PATCH 147/236] Update ModelChain.__getattr__ deprecation message Note that the deprecated attributes will be removed in 0.10, not 1.0. --- pvlib/modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 3bbe6bdcd8..67388071bb 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -411,7 +411,7 @@ def __init__(self, system, location, def __getattr__(self, key): if key in ModelChain._deprecated_attrs: msg = f'ModelChain.{key} is deprecated and will' \ - f' be removed in v1.0. Use' \ + f' be removed in v0.10. Use' \ f' ModelChain.results.{key} instead' warnings.warn(msg, pvlibDeprecationWarning) return getattr(self.results, key) From 07b3d05896ae6d84f826dd34686825e4b2dee216 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 8 Dec 2020 14:04:10 -0700 Subject: [PATCH 148/236] Expose PVSystem.arrays as a public attribute This attribute holds the tuple of arrays in the PVSystem. --- pvlib/pvsystem.py | 80 ++++++++++++++++++------------------ pvlib/tests/test_pvsystem.py | 1 + 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index e7d0dc8d47..d786591ea7 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -53,7 +53,7 @@ def _combine_localized_attributes(pvsystem=None, location=None, **kwargs): """ if pvsystem is not None: pv_dict = pvsystem.__dict__ - pv_dict = {**pv_dict, **pv_dict['_arrays'][0].__dict__} + pv_dict = {**pv_dict, **pv_dict['arrays'][0].__dict__} else: pv_dict = {} @@ -215,7 +215,7 @@ def __init__(self, **kwargs): if arrays is None: - self._arrays = (Array( + self.arrays = (Array( surface_tilt, surface_azimuth, albedo, @@ -229,7 +229,7 @@ def __init__(self, racking_model ),) else: - self._arrays = tuple(arrays) + self.arrays = tuple(arrays) self.inverter = inverter if inverter_parameters is None: @@ -252,7 +252,7 @@ def __init__(self, def __repr__(self): repr = f'PVSystem:\n name: {self.name}\n ' - for array in self._arrays: + for array in self.arrays: repr += '\n '.join(array.__repr__().split('\n')) repr += '\n ' repr += f'inverter: {self.inverter}' @@ -260,21 +260,21 @@ def __repr__(self): def _validate_per_array(self, values, system_wide=False): """Check that `values` is a tuple of the same length as - `self._arrays`. + `self.arrays`. If `values` is not a tuple it is packed in to a length-1 tuple before the check. If the lengths are not the same a ValueError is raised, otherwise the tuple `values` is returned. When `system_wide` is True and `values` is not a tuple, `values` - is replicated to a tuple of the same length as `self._arrays` and that + is replicated to a tuple of the same length as `self.arrays` and that tuple is returned. """ if system_wide and not isinstance(values, tuple): return (values,) * self.num_arrays if not isinstance(values, tuple): values = (values,) - if len(values) != len(self._arrays): + if len(values) != len(self.arrays): raise ValueError("Length mismatch for per-array parameter") return values @@ -290,7 +290,7 @@ def _infer_cell_type(self): ------- cell_type: str """ - return tuple(array._infer_cell_type() for array in self._arrays) + return tuple(array._infer_cell_type() for array in self.arrays) @_unwrap_single_value def get_aoi(self, solar_zenith, solar_azimuth): @@ -310,7 +310,7 @@ def get_aoi(self, solar_zenith, solar_azimuth): """ return tuple(array.get_aoi(solar_zenith, solar_azimuth) - for array in self._arrays) + for array in self.arrays) @_unwrap_single_value def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, @@ -357,7 +357,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni, ghi, dhi, dni_extra, airmass) for array, dni, ghi, dhi in zip( - self._arrays, dni, ghi, dhi + self.arrays, dni, ghi, dhi ) ) @@ -390,7 +390,7 @@ def get_iam(self, aoi, iam_model='physical'): """ aoi = self._validate_per_array(aoi) return tuple(array.get_iam(aoi, iam_model) - for array, aoi in zip(self._arrays, aoi)) + for array, aoi in zip(self.arrays, aoi)) @_unwrap_single_value def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): @@ -430,7 +430,7 @@ def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): **build_kwargs(array.module_parameters) ) for array, effective_irradiance, temp_cell - in zip(self._arrays, effective_irradiance, temp_cell) + in zip(self.arrays, effective_irradiance, temp_cell) ) @_unwrap_single_value @@ -471,7 +471,7 @@ def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): **build_kwargs(array.module_parameters) ) for array, effective_irradiance, temp_cell - in zip(self._arrays, effective_irradiance, temp_cell) + in zip(self.arrays, effective_irradiance, temp_cell) ) @_unwrap_single_value @@ -511,7 +511,7 @@ def calcparams_pvsyst(self, effective_irradiance, temp_cell): **build_kwargs(array.module_parameters) ) for array, effective_irradiance, temp_cell - in zip(self._arrays, effective_irradiance, temp_cell) + in zip(self.arrays, effective_irradiance, temp_cell) ) @_unwrap_single_value @@ -542,7 +542,7 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): return tuple( sapm(effective_irradiance, temp_cell, array.module_parameters) for array, effective_irradiance, temp_cell - in zip(self._arrays, effective_irradiance, temp_cell) + in zip(self.arrays, effective_irradiance, temp_cell) ) @_unwrap_single_value @@ -568,7 +568,7 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): poa_global = self._validate_per_array(poa_global) temp_air = self._validate_per_array(temp_air, system_wide=True) wind_speed = self._validate_per_array(wind_speed, system_wide=True) - for array in self._arrays: + for array in self.arrays: # warn user about change in default behavior in 0.9. if (array.temperature_model_parameters == {} and array.module_type is None and array.racking_model is None): @@ -591,7 +591,7 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): **build_kwargs(array.temperature_model_parameters) ) for array, poa_global, temp_air, wind_speed in zip( - self._arrays, poa_global, temp_air, wind_speed + self.arrays, poa_global, temp_air, wind_speed ) ) @@ -613,7 +613,7 @@ def sapm_spectral_loss(self, airmass_absolute): """ return tuple( sapm_spectral_loss(airmass_absolute, array.module_parameters) - for array in self._arrays + for array in self.arrays ) @_unwrap_single_value @@ -652,7 +652,7 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, poa_direct, poa_diffuse, airmass_absolute, aoi, array.module_parameters) for array, poa_direct, poa_diffuse, aoi - in zip(self._arrays, poa_direct, poa_diffuse, aoi) + in zip(self.arrays, poa_direct, poa_diffuse, aoi) ) @_unwrap_single_value @@ -690,7 +690,7 @@ def build_celltemp_kwargs(array): temperature.pvsyst_cell(poa_global, temp_air, wind_speed, **build_celltemp_kwargs(array)) for array, poa_global, temp_air, wind_speed in zip( - self._arrays, poa_global, temp_air, wind_speed + self.arrays, poa_global, temp_air, wind_speed ) ) @@ -725,7 +725,7 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): **_build_kwargs( ['u0', 'u1'], array.temperature_model_parameters)) for array, poa_global, temp_air, wind_speed in zip( - self._arrays, poa_global, temp_air, wind_speed + self.arrays, poa_global, temp_air, wind_speed ) ) @@ -778,7 +778,7 @@ def _build_kwargs_fuentes(array): poa_global, temp_air, wind_speed, **_build_kwargs_fuentes(array)) for array, poa_global, temp_air, wind_speed in zip( - self._arrays, poa_global, temp_air, wind_speed + self.arrays, poa_global, temp_air, wind_speed ) ) @@ -830,7 +830,7 @@ def _spectral_correction(array): pw, airmass_absolute, module_type, coefficients ) - return tuple(_spectral_correction(array) for array in self._arrays) + return tuple(_spectral_correction(array) for array in self.arrays) def singlediode(self, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, @@ -904,7 +904,7 @@ def scale_voltage_current_power(self, data): scale_voltage_current_power(data, voltage=array.modules_per_string, current=array.strings) - for array, data in zip(self._arrays, data) + for array, data in zip(self.arrays, data) ) @_unwrap_single_value @@ -924,7 +924,7 @@ def pvwatts_dc(self, g_poa_effective, temp_cell): array.module_parameters['gamma_pdc'], **_build_kwargs(['temp_ref'], array.module_parameters)) for array, g_poa_effective, temp_cell - in zip(self._arrays, g_poa_effective, temp_cell) + in zip(self.arrays, g_poa_effective, temp_cell) ) def pvwatts_losses(self): @@ -984,78 +984,78 @@ def localize(self, location=None, latitude=None, longitude=None, @property @_unwrap_single_value def module_parameters(self): - return tuple(array.module_parameters for array in self._arrays) + return tuple(array.module_parameters for array in self.arrays) @property @_unwrap_single_value def module(self): - return tuple(array.module for array in self._arrays) + return tuple(array.module for array in self.arrays) @property @_unwrap_single_value def module_type(self): - return tuple(array.module_type for array in self._arrays) + return tuple(array.module_type for array in self.arrays) @property @_unwrap_single_value def temperature_model_parameters(self): return tuple(array.temperature_model_parameters - for array in self._arrays) + for array in self.arrays) @temperature_model_parameters.setter def temperature_model_parameters(self, value): - for array in self._arrays: + for array in self.arrays: array.temperature_model_parameters = value @property @_unwrap_single_value def surface_tilt(self): - return tuple(array.surface_tilt for array in self._arrays) + return tuple(array.surface_tilt for array in self.arrays) @surface_tilt.setter def surface_tilt(self, value): - for array in self._arrays: + for array in self.arrays: array.surface_tilt = value @property @_unwrap_single_value def surface_azimuth(self): - return tuple(array.surface_azimuth for array in self._arrays) + return tuple(array.surface_azimuth for array in self.arrays) @surface_azimuth.setter def surface_azimuth(self, value): - for array in self._arrays: + for array in self.arrays: array.surface_azimuth = value @property @_unwrap_single_value def albedo(self): - return tuple(array.albedo for array in self._arrays) + return tuple(array.albedo for array in self.arrays) @property @_unwrap_single_value def racking_model(self): - return tuple(array.racking_model for array in self._arrays) + return tuple(array.racking_model for array in self.arrays) @racking_model.setter def racking_model(self, value): - for array in self._arrays: + for array in self.arrays: array.racking_model = value @property @_unwrap_single_value def modules_per_string(self): - return tuple(array.modules_per_string for array in self._arrays) + return tuple(array.modules_per_string for array in self.arrays) @property @_unwrap_single_value def strings_per_inverter(self): - return tuple(array.strings for array in self._arrays) + return tuple(array.strings for array in self.arrays) @property def num_arrays(self): """The number of Arrays in the system.""" - return len(self._arrays) + return len(self.arrays) @deprecated('0.8', alternative='PVSystem, Location, and ModelChain', diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index afa6a001b1..2da0d1152a 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1447,6 +1447,7 @@ def test_PVSystem_multiple_array_creation(): assert pv_system.surface_tilt == (32, 15) assert pv_system.surface_azimuth == (180, 180) assert pv_system.module_parameters == ({}, {'pdc0': 1}) + assert pv_system.arrays == (array_one, array_two) with pytest.raises(TypeError): pvsystem.PVSystem(arrays=array_one) From 8936e6000447b7d60d3866c57decec7352b2092e Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 8 Dec 2020 14:39:26 -0700 Subject: [PATCH 149/236] Iterate over arrays in ModelChain._check_consistent_params() Rather than iterating over individual parameters for each Array we iterate over the arrays. Effectively the same, but much more readable. --- pvlib/modelchain.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 67388071bb..2c1c70fad3 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -634,20 +634,17 @@ def _check_consistent_params(self): """Ensure that each all arrays in ``self.system`` have the same module and temperature model parameters. If parameters differ a ValueError is raised.""" - if self.system.num_arrays == 1: - return # check consistent module_parameters params = np.unique( - [set(module_parameters.keys()) - for module_parameters in self.system.module_parameters]) + [set(array.module_parameters.keys()) + for array in self.system.arrays]) if len(params) > 1: raise ValueError('PVSystem arrays have module_parameters with ' 'different keys.') # check consistent temperature_model_parameters params = np.unique( - [set(temperature_model_parameters.keys()) - for temperature_model_parameters - in self.system.temperature_model_parameters]) + [set(array.temperature_model_parameters.keys()) + for array in self.system.arrays]) if len(params) > 1: raise ValueError('PVSystem arrays temperature_model_parameters ' 'have different keys. All arrays should have ' From f45480755f9f5fd45d45002ad93a332219e4308d Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 8 Dec 2020 14:55:00 -0700 Subject: [PATCH 150/236] Refactor ModelChain.effective_irradiance_model() Reduce code duplication by calling nested def for the single-array case. --- pvlib/modelchain.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 2c1c70fad3..632881718d 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1086,17 +1086,17 @@ def _eff_irrad(module_parameters, total_irrad, spect_mod, aoi_mod): fd * total_irrad['poa_diffuse']) if isinstance(self.results.total_irrad, tuple): self.results.effective_irradiance = tuple( - _eff_irrad(array, ti, sm, am) for + _eff_irrad(array.module_parameters, ti, sm, am) for array, ti, sm, am in zip( - self.system.module_parameters, self.results.total_irrad, + self.system.arrays, self.results.total_irrad, self.results.spectral_modifier, self.results.aoi_modifier)) else: - fd = self.system.module_parameters.get('FD', 1.) - self.results.effective_irradiance = \ - self.results.spectral_modifier * \ - (self.results.total_irrad['poa_direct'] * - self.results.aoi_modifier - + fd * self.results.total_irrad['poa_diffuse']) + self.results.effective_irradiance = _eff_irrad( + self.system.module_parameters, + self.results.total_irrad, + self.results.spectral_modifier, + self.results.aoi_modifier + ) return self def complete_irradiance(self, weather): From c572153ceb11a5bd011288496651bf7d43a65193 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 9 Dec 2020 10:53:54 -0700 Subject: [PATCH 151/236] rewrite pvsystem.rst --- docs/sphinx/source/pvsystem.rst | 146 +++++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 41 deletions(-) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 72f89cafcb..719af6917b 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -10,11 +10,24 @@ PVSystem from pvlib import pvsystem -The :py:class:`~pvlib.pvsystem.PVSystem` class wraps many of the -functions in the :py:mod:`~pvlib.pvsystem` module. This simplifies the -API by eliminating the need for a user to specify arguments such as -module and inverter properties when calling PVSystem methods. -:py:class:`~pvlib.pvsystem.PVSystem` is not better or worse than the +The :py:class:`~pvlib.pvsystem.PVSystem` represents one inverter and the +PV modules that supply DC power to the inverter. A PV system may be on fixed +mounting or single axis trackers. The :py:class`~pvlib.pvsystem.PVSystem` +is supported by the :py:class:`~pvlib.pvsystem.Array` which represents the +PV modules in the :py:class`~pvlib.pvsystem.PVSystem`. An instance of +:py:class:`~pvlib.pvsystem.PVSystem` has a single inverter, but can have +multiple instances of :py:class:`~pvlib.pvsystem.Array`. Arrays can have +different tilt, orientation, and number or type of modules. + +The :py:class:`~pvlib.pvsystem.PVSystem` class methods wrap many of the +functions in the :py:mod:`~pvlib.pvsystem` module. Similarly, the +:py:class:`~pvlib.pvsystem.Array` wraps several functions with its class +methods. Methods that wrap functions have similar names as the wrapped function. +This practice simplifies the API for :py:class:`~pvlib.pvsystem.PVSystem` +and :py:class:`~pvlib.pvsystem.Array` methods by eliminating the need to specify +arguments that are stored as attributes of these classes, such as +module and inverter properties when calling PVSystem methods. Using +:py:class:`~pvlib.pvsystem.PVSystem` is not better or worse than using the functions it wraps -- it is simply an alternative way of organizing your data and calculations. @@ -40,17 +53,37 @@ data that influences the PV system (e.g. the weather). The data that represents the PV system is *intrinsic*. The data that influences the PV system is *extrinsic*. -Intrinsic data is stored in object attributes. For example, the data -that describes a PV system's module parameters is stored in -`PVSystem.module_parameters`. +Intrinsic data is stored in object attributes. For example, the parameters +that describe a PV system's inverter is stored in +`PVSystem.inverter_parameters`. .. ipython:: python - module_parameters = {'pdc0': 10, 'gamma_pdc': -0.004} - system = pvsystem.PVSystem(module_parameters=module_parameters) + inverter_parameters = {'pdc0': 5000, 'eta_inv_nom': 0.96} + system = pvsystem.PVSystem(inverter_parameters=inverter_parameters) + print(system.inverter_parameters) + +The parameters that describe a PV system's modules can be provided to +`PVSystem.module_parameters` (in the case of a single array) or by providing +a list of instances of the :py:class:`~pvlib.pvsystem.Array`: + +.. ipython:: python + + module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} + system = pvsystem.PVSystem(module_parameters=module_parameters, + inverter_parameters=inverter_parameters) print(system.module_parameters) -Extrinsic data is passed to a PVSystem as method arguments. For example, +.. ipython:: python + + module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} + array = pvsystem.Array(module_parameters=module_parameters) + system = pvsystem.PVSystem(arrays=[array], + inverter_parameters=inverter_parameters) + print(system.module_parameters) + print(system.inverter_parameters) + +Extrinsic data is passed to a PVSystem instance as method arguments. For example, the :py:meth:`~pvlib.pvsystem.PVSystem.pvwatts_dc` method accepts extrinsic data irradiance and temperature. @@ -95,34 +128,63 @@ as well as the incidence angle modifier methods. PVSystem attributes ------------------- -Here we review the most commonly used PVSystem attributes. Please see -the :py:class:`~pvlib.pvsystem.PVSystem` class documentation for a -comprehensive list. - -The first PVSystem parameters are `surface_tilt` and `surface_azimuth`. -These parameters are used in PVSystem methods such as -:py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` and -:py:meth:`~pvlib.pvsystem.PVSystem.get_irradiance`. Angle of incidence +Here we review the most commonly used PVSystem and Array attributes. +Please see the :py:class:`~pvlib.pvsystem.PVSystem` and +:py:class:`~pvlib.pvsystem.Array` class documentation for a +comprehensive list of attributes. + +The first parameters which describe the DC part of a PV system are the tilt +and azimuth of the modules. In the case of a PV system with a single array, +these parameters can be specified using the `PVSystem.surface_tilt` and +`PVSystem.surface_azimuth` attributes. In the case of a PV system with +several arrays, the parameters are specified for each array using +the attributes `Array.surface_tilt` and `Array.surface_azimuth`. + +The `surface_tilt` and `surface_azimuth` attributes are used in PVSystem +(or Array) methods such as :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` or +:py:meth:`~pvlib.pvsystem.Array.get_aoi`. The angle of incidence (AOI) (AOI) calculations require `surface_tilt`, `surface_azimuth` and also -the sun position. The :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` method -uses the `surface_tilt` and `surface_azimuth` attributes in its PVSystem +the extrinsic sun position. The :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` method +uses the `surface_tilt` and `surface_azimuth` attributes from its PVSystem object, and so requires only `solar_zenith` and `solar_azimuth` as -arguments. +arguments. The :py:meth:`~pvlib.pvsystem.Array.get_aoi` operates in a similar +manner. .. ipython:: python - # 20 deg tilt, south-facing - system = pvsystem.PVSystem(surface_tilt=20, surface_azimuth=180) - print(system.surface_tilt, system.surface_azimuth) + # single south-facing array at 20 deg tilt + system_one_array = pvsystem.PVSystem(surface_tilt=20, surface_azimuth=180) + print(system_one_array.surface_tilt, system_one_array.surface_azimuth) + + # call get_aoi with solar_zenith, solar_azimuth + aoi = system_one_array.get_aoi(solar_zenith=30, solar_azimuth=180) + print(aoi) + +.. ipython:: python + # two arrays each at 30 deg tilt with different facing + array_one = pvsystem.Array(surface_tilt=30, surface_azimuth=90) + array_two = pvsystem.Array(surface_tilt=30, surface_azimuth=220) + system_multiarray = pvsystem.PVSystem(arrays=[array_one, array_two]) + print(system_multiarray.num_arrays) # call get_aoi with solar_zenith, solar_azimuth - aoi = system.get_aoi(30, 180) + aoi = system_multiarray.get_aoi(solar_zenith=30, solar_azimuth=180) print(aoi) +Note that when the PV system includes more than one array, the output of +:py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` is a *tuple* with the order of the +elements corresponding to the order of the arrays. If the AOI is desired for +a specific array, :py:meth:`~pvlib.pvsystem.Array.get_aoi` returns the AOI for +the array represented by the method's object. + +.. ipython:: python + + aoi = array_one.get_aoi(solar_zenith=30, solar_azimuth=180) + print(aoi) `module_parameters` and `inverter_parameters` contain the data necessary for computing DC and AC power using one of the available -PVSystem methods. These are typically specified using data from +PVSystem methods. These attributes are typically specified using data from the :py:func:`~pvlib.pvsystem.retrieve_sam` function: .. ipython:: python @@ -133,8 +195,8 @@ the :py:func:`~pvlib.pvsystem.retrieve_sam` function: module_parameters = modules['Canadian_Solar_Inc__CS5P_220M'] inverters = pvsystem.retrieve_sam('cecinverter') inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_'] - system = pvsystem.PVSystem(module_parameters=module_parameters, inverter_parameters=inverter_parameters) - + system_one_array = pvsystem.PVSystem(module_parameters=module_parameters, + inverter_parameters=inverter_parameters) The module and/or inverter parameters can also be specified manually. This is useful for specifying modules and inverters that are not @@ -153,8 +215,8 @@ The attributes `modules_per_string` and `strings_per_inverter` are used in the :py:meth:`~pvlib.pvsystem.PVSystem.scale_voltage_current_power` method. Some DC power models in :py:class:`~pvlib.modelchain.ModelChain` automatically call this method and make use of these attributes. As an -example, consider a system with 35 modules arranged into 5 strings of 7 -modules each. +example, consider a system with a single array comprising 35 modules +arranged into 5 strings of 7 modules each. .. ipython:: python @@ -173,8 +235,8 @@ PVSystem with multiple Arrays It is possible to model a system with multiple arrays by passing a list of :py:class:`~pvlib.pvsystem.Array` to the :py:class:`~pvlib.pvsystem.PVSystem` -constructor. The :py:class:`~pvlib.pvsystem.Array` class subsumes all the -:py:class:`~pvlib.pvsystem.PVSystem` attributes that might differ from array +constructor. The :py:class:`~pvlib.pvsystem.Array` class includes those +:py:class:`~pvlib.pvsystem.PVSystem` attributes that may differ from array to array. These attributes include `surface_tilt`, `surface_azimuth`, `module_parameters`, `temperature_model_parameters`, `modules_per_string`, `strings_per_inverter`, `albedo`, `surface_type`, `module_type`, and @@ -187,16 +249,18 @@ to array. These attributes include `surface_tilt`, `surface_azimuth`, system = pvsystem.PVSystem(arrays=[array_one, array_two]) system.num_arrays - -When instantiating a :py:class:`~pvlib.pvsystem.PVSystem` by passing a list -of arrays, as above, you may not pass any of the above parameters to the -`PVSystem` constructor. Each non-default parameter must be specified -individually for each array. +When instantiating a :py:class:`~pvlib.pvsystem.PVSystem` with a list +of :py:class:`~pvlib.pvsystem.Array`, each parameter must be specified individually +for each array when the :py:class:`~pvlib.pvsystem.PVSystem` instances are constructed. +For example, if all arrays are at the same tilt you must specify that tilt for +every array. When using a list of :py:class:`~pvlib.pvsystem.Array` you shouldn't +also pass any attributes for Arrays to the `PVSystem` attributes; these values +are ignored. The output of `PVSystem` methods and attributes changes when the system has -multiple arrays. For any of the attributes listed above, accessing them will -return a tuple with the value of the attribute for each array, specified in -the same order as the `arrays` parameter. For example, using the system +multiple arrays. Accessing any of Array attributes on the PVSystem object returns +return a tuple with the value of the attribute for each array, in +the same order as the `PVSystem.arrays` parameter. For example, using the system constructed above: .. ipython:: python From 11fe336fbc5a640368344cf10cc51ba9bc76b5de Mon Sep 17 00:00:00 2001 From: Will Vining Date: Wed, 9 Dec 2020 07:50:26 -0700 Subject: [PATCH 152/236] Raise a TypeError in ModelChain._check_multiple_input() When input must be a tuple, we should raise a TypeError, rather than a ValueError. --- pvlib/modelchain.py | 6 +++--- pvlib/tests/test_modelchain.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 632881718d..154f0f4713 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1391,9 +1391,9 @@ def _check_multiple_input(self, data, strict=True): and not isinstance(data, tuple): return if strict and not isinstance(data, tuple): - raise ValueError("Input must be a tuple of length " - f"{self.system.num_arrays}, " - f"got {type(data).__name__}.") + raise TypeError("Input must be a tuple of length " + f"{self.system.num_arrays}, " + f"got {type(data).__name__}.") if len(data) != self.system.num_arrays: raise ValueError("Input must be same length as number of arrays " f"in system. Expected {self.system.num_arrays}, " diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index ff53e1d4e1..3b60dcd4bf 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -678,7 +678,7 @@ def test_prepare_poa_wrong_number_arrays( type_error = r"Input must be a tuple of length 2, got .*\." mc = ModelChain(sapm_dc_snl_ac_system_Array, location) poa = pd.concat([weather, total_irrad], axis=1) - with pytest.raises(ValueError, match=type_error): + with pytest.raises(TypeError, match=type_error): mc.prepare_inputs_from_poa(poa) with pytest.raises(ValueError, match=len_error): mc.prepare_inputs_from_poa((poa,)) @@ -839,7 +839,7 @@ def test_run_model_from_effective_irradiance_arrays_error( len_error = r"Input must be same length as number of arrays in system\. " \ r"Expected 2, got [0-9]+\." type_error = r"Input must be a tuple of length 2, got DataFrame\." - with pytest.raises(ValueError, match=type_error): + with pytest.raises(TypeError, match=type_error): mc.run_model_from_effective_irradiance(data) with pytest.raises(ValueError, match=len_error): mc.run_model_from_effective_irradiance((data,)) From 06203dd202abc2bf069d119c96ff1580524c73b9 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 11 Dec 2020 12:41:30 -0700 Subject: [PATCH 153/236] Note that indices assumed to be same in ModelChain._assign_times() --- pvlib/modelchain.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 154f0f4713..34b5a0a817 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1293,7 +1293,10 @@ def _assign_times(self): """Assign self.times according the the index of self.weather. If there are multiple DataFrames in self.weather then the index - of the first one is assigned + of the first one is assigned. It is assumed that the indices of + each data frame in `weather` are the same. This can be verified + by calling :py:func:`_all_same_index` or + :py:meth:`self._check_multiple_weather` before calling this method. """ if isinstance(self.weather, tuple): self.times = self.weather[0].index From 9c5360081e9b1afdc29a4936c91652f33a7a56ca Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 11 Dec 2020 12:49:13 -0700 Subject: [PATCH 154/236] Add comment in ModelChain.complete_irradiance() Note why `ModelChain._assign_weather()` is not used here. --- pvlib/modelchain.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 34b5a0a817..3934398950 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1153,6 +1153,8 @@ def complete_irradiance(self, weather): >>> mc.run_model(my_weather) # doctest: +SKIP """ self._check_multiple_input(weather) + # Don't use ModelChain._assign_weather() here because it adds + # temperature and wind-speed columns which we do not need here. self.weather = weather self._assign_times() self.results.solar_position = self.location.get_solarposition( From 9c1061262f2600734cd4dc1474e940067a3a07ad Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 11 Dec 2020 13:05:04 -0700 Subject: [PATCH 155/236] Copy input before assigning in ModelChain.complete_irradiance() To ensure that input data is not modified copy the data frames that are passed to ModelChain.complete_irradiance() before assigning `ModelChain.weather`. --- pvlib/modelchain.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 3934398950..a2480280d3 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1155,7 +1155,7 @@ def complete_irradiance(self, weather): self._check_multiple_input(weather) # Don't use ModelChain._assign_weather() here because it adds # temperature and wind-speed columns which we do not need here. - self.weather = weather + self.weather = _copy(weather) self._assign_times() self.results.solar_position = self.location.get_solarposition( self.times, method=self.solar_position_method) @@ -1703,6 +1703,14 @@ def run_model_from_effective_irradiance(self, data=None): return self +def _copy(data): + """Return a copy of each DataFrame in `data` if it is a tuple, + otherwise return a copy of `data`.""" + if not isinstance(data, tuple): + return data.copy() + return tuple(df.copy() for df in data) + + def _all_same_index(data): indexes = map(lambda df: df.index, data) next(indexes, None) From 57ba56fda93416d55b6415c43d9a9a6a729afa80 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 11 Dec 2020 13:46:42 -0700 Subject: [PATCH 156/236] Document ModelChain.run_model() and run_model_from_poa() input type Expand docstring to describe tuple input where each element specifies the input for the corresponding Array. --- pvlib/modelchain.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index a2480280d3..e859650f16 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1542,7 +1542,7 @@ def run_model(self, weather): Parameters ---------- - weather : DataFrame + weather : DataFrame or tuple of DataFrame Irradiance column names must include ``'dni'``, ``'ghi'``, and ``'dhi'``. If optional columns ``'temp_air'`` and ``'wind_speed'`` are not provided, air temperature of 20 C and wind speed of 0 m/s @@ -1551,6 +1551,13 @@ def run_model(self, weather): of `temperature_model`. If optional column `module_temperature` is provided, `temperature_model` must be ``'sapm'``. + If `weather` is a tuple it must have the same length as the number + of arrays in the system being modeled. Each element should specify + the POA (and other input) to the corresponding array in the system + being modeled. For example, ``weather[0]`` contains weather data + for ``system.arrays[0]``. Each element must satisfy the + requirements described above. + Returns ------- self @@ -1586,7 +1593,7 @@ def run_model_from_poa(self, data): Parameters ---------- - data : DataFrame + data : DataFrame or tuple of DataFrame Required column names include ``'poa_global'``, ``'poa_direct'`` and ``'poa_diffuse'``. If optional columns ``'temp_air'`` and ``'wind_speed'`` are not provided, air @@ -1596,6 +1603,13 @@ def run_model_from_poa(self, data): ``'module_temperature'`` is provided, `temperature_model` must be ``'sapm'``. + If `data` is a tuple it must have the same length as the number + of arrays in the system being modeled. Each element should specify + the POA (and other input) to the corresponding array in the system + being modeled. For example, ``data[0]`` contains POA irradiance + for ``system.arrays[0]``. Each element must satisfy the + requirements described above. + Returns ------- self From 46f0bfd3efcbc6552230afc65742a2dd27434f6c Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 11 Dec 2020 14:27:39 -0700 Subject: [PATCH 157/236] Don't allow SingleAxisTracker Array with non-None tilt/azimuth If a list of one Array is given a value error is raised when it has non-None tilt or azimuth. Rather than just assigning None we raise an error so that the Array object itself is not modified (since it could be in use by the user somewhere else). When only parameters are passed (as opposed to an Array object) we override the values. Since the Array is created internally and we know it is not in use elsewhere this isn't a problem. --- pvlib/tests/test_tracking.py | 17 ++++++++++++++++- pvlib/tracking.py | 30 +++++++++++++++++++----------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 5aea3911d4..3e21487ee9 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -305,7 +305,11 @@ def test_SingleAxisTracker_creation(): def test_SingleAxisTracker_one_array_only(): system = tracking.SingleAxisTracker( - arrays=[pvsystem.Array(module='foo')] + arrays=[pvsystem.Array( + module='foo', + surface_tilt=None, + surface_azimuth=None + )] ) assert system.module == 'foo' with pytest.raises(ValueError, @@ -315,6 +319,17 @@ def test_SingleAxisTracker_one_array_only(): arrays=[pvsystem.Array(module='foo'), pvsystem.Array(module='bar')] ) + with pytest.raises(ValueError, + match="Array must not have surface_tilt "): + tracking.SingleAxisTracker(arrays=[pvsystem.Array(module='foo')]) + with pytest.raises(ValueError, + match="Array must not have surface_tilt "): + tracking.SingleAxisTracker( + arrays=[pvsystem.Array(surface_azimuth=None)]) + with pytest.raises(ValueError, + match="Array must not have surface_tilt "): + tracking.SingleAxisTracker( + arrays=[pvsystem.Array(surface_tilt=None)]) def test_SingleAxisTracker_tracking(): diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 3a76b0518b..d86381ad2e 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -58,12 +58,16 @@ class SingleAxisTracker(PVSystem): **kwargs Passed to :py:class:`~pvlib.pvsystem.PVSystem`. If the `arrays` - parameter is specified it must have only a single Array. + parameter is specified it must have only a single Array. Furthermore + if a :py:class:`~pvlib.pvsystem.Array` is provided it must have + ``surface_tilt`` and ``surface_azimuth`` equal to None. Raises ------ ValueError If more than one Array is specified. + ValueError + If an Array is provided with a surface tilt or azimuth not None. See also -------- @@ -75,7 +79,20 @@ class SingleAxisTracker(PVSystem): def __init__(self, axis_tilt=0, axis_azimuth=0, max_angle=90, backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0.0, **kwargs): - _ensure_single_array(kwargs) + arrays = kwargs.get('arrays', []) + if len(arrays) > 1: + raise ValueError("SingleAxisTracker does not currently support " + "multiple arrays.") + elif len(arrays) == 1: + surface_tilt = arrays[0].surface_tilt + surface_azimuth = arrays[0].surface_azimuth + if surface_tilt is not None or surface_azimuth is not None: + raise ValueError( + "Array must not have surface_tilt or " + "surface_azimuth assigned. You must pass an " + "Array with these fields set to None." + ) + self.axis_tilt = axis_tilt self.axis_azimuth = axis_azimuth self.max_angle = max_angle @@ -246,15 +263,6 @@ def get_irradiance(self, surface_tilt, surface_azimuth, **kwargs) -def _ensure_single_array(kwargs): - """Raise an error if `kwargs` indicates the system has more - than one array.""" - arrays = kwargs.get('arrays', []) - if len(arrays) > 1: - raise ValueError("SingleAxisTracker does not currently support " - "multiple arrays.") - - @deprecated('0.8', alternative='SingleAxisTracker, Location, and ModelChain', name='LocalizedSingleAxisTracker', removal='0.9') class LocalizedSingleAxisTracker(SingleAxisTracker, Location): From 23c50fa219961b03955226b3a6e903d29b5ce28c Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 11 Dec 2020 14:35:10 -0700 Subject: [PATCH 158/236] Improve documentation of ValueErrors from ModelChain methods Readability improvements by breaking up multiple reasons for a value error into separate items. --- pvlib/modelchain.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index e859650f16..09e31a3955 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1325,10 +1325,13 @@ def prepare_inputs(self, weather): Raises ------ ValueError - If the `weather` DataFrame(s) are missing an irradiance component, - if `weather` is a tuple and the DataFrames it contains do not - all have the same index, or if `weather` is a tuple with a - different length than the number of Arrays in the system. + If the `weather` DataFrame(s) are missing an irradiance component. + ValueError + If `weather` is a tuple and the DataFrames it contains have + different indices. + ValueError + If `weather` is a tuple with a different length than the number + of Arrays in the system. Notes ----- @@ -1694,7 +1697,10 @@ def run_model_from_effective_irradiance(self, data=None): ------ ValueError If the number of arrays is different than the number of data - frames passed in `data` or the DataFrames have different indices. + frames passed in `data` + ValueError + If `data` is a tuple and the DataFrames it contains have + different indices. Notes ----- From 286667f00e33d1b60eb3d5840026ac0faa1bef13 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 11 Dec 2020 15:15:31 -0700 Subject: [PATCH 159/236] Separate ac model inference for multiple arrays Adds an internal _infer_ac_model_multi() method to handle inference when system.num_arrays > 1. This lets us raise a more usefule error message in this case. In addition several _xxxx_params() predicates are added to prevent duplication of the specific tests on the inverter_params set in _infer_ac_model_multi(). Seems like over-kill for now, but an inverter.pvwatts_multi() model is in the works, so this will save some work when integrating that. --- pvlib/modelchain.py | 48 ++++++++++++++++++++++++++-------- pvlib/tests/test_modelchain.py | 3 ++- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 09e31a3955..9597489892 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -799,20 +799,28 @@ def ac_model(self, model): def infer_ac_model(self): """Infer AC power model from system attributes.""" inverter_params = set(self.system.inverter_parameters.keys()) - single_array = self.system.num_arrays == 1 - if not single_array and {'C0', 'C1', 'C2'} <= inverter_params: - return self.sandia_multi_inverter - elif single_array and {'C0', 'C1', 'C2'} <= inverter_params: + if self.system.num_arrays > 1: + return self._infer_ac_model_multi(inverter_params) + if _snl_params(inverter_params): return self.snlinverter - elif single_array and {'ADRCoefficients'} <= inverter_params: + if _adr_params(inverter_params): return self.adrinverter - elif single_array and {'pdc0'} <= inverter_params: + if _pvwatts_params(inverter_params): return self.pvwatts_inverter - else: - raise ValueError('could not infer AC model from ' - 'system.inverter_parameters. Check ' - 'system.inverter_parameters or explicitly ' - 'set the model with the ac_model kwarg.') + raise ValueError('could not infer AC model from ' + 'system.inverter_parameters. Check ' + 'system.inverter_parameters or explicitly ' + 'set the model with the ac_model kwarg.') + + def _infer_ac_model_multi(self, inverter_params): + if _snl_params(inverter_params): + return self.sandia_multi_inverter + raise ValueError('could not infer multi-array AC model from ' + 'system.inverter_parameters. Not all ac models ' + 'support systems with mutiple arrays. ' + 'Only sandia_multi supports multiple ' + 'arrays. Check system.inverter_parameters or ' + 'explicitly set the model with the ac_model kwarg.') def sandia_multi_inverter(self): self.results.ac = self.system.sandia_multi( @@ -1723,6 +1731,24 @@ def run_model_from_effective_irradiance(self, data=None): return self +def _snl_params(inverter_params): + """Return True if `inverter_params` includes parameters for the + Sandia inverter model.""" + return {'C0', 'C1', 'C2'} <= inverter_params + + +def _adr_params(inverter_params): + """Return True if `inverter_params` includes parameters for the ADR + inverter model.""" + return {'ADRCoefficients'} <= inverter_params + + +def _pvwatts_params(inverter_params): + """Return True if `inverter_params` includes parameters for the + PVWatts inverter model.""" + return {'pdc0'} <= inverter_params + + def _copy(data): """Return a copy of each DataFrame in `data` if it is a tuple, otherwise return a copy of `data`.""" diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 3b60dcd4bf..0f3f46db7d 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -421,7 +421,8 @@ def test_ModelChain_invalid_inverter_params_arrays( 'pvwatts': {'pdc0': 220, 'eta_inv_nom': 0.95}} sapm_dc_snl_ac_system_same_arrays.inverter_parameters = \ inverter_params[inverter] - with pytest.raises(ValueError, match='could not infer AC model from'): + with pytest.raises(ValueError, + match=r'Only sandia_multi supports multiple arrays\.'): ModelChain(sapm_dc_snl_ac_system_same_arrays, location) From 9b6ef5ce60d62fe8370c30a434c2beb63602ed07 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 14 Dec 2020 07:15:47 -0700 Subject: [PATCH 160/236] Update docstring for ModelChain.run_model_from_effective_irradiance() Was referring to ModelChain.run_model_from() in the "See also" section. That doesn't exist; changed to ModelChain.run_model() Co-authored-by: Will Holmgren --- pvlib/modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 9597489892..c563ce5ebe 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1718,7 +1718,7 @@ def run_model_from_effective_irradiance(self, data=None): See also -------- - pvlib.modelchain.ModelChain.run_model_from + pvlib.modelchain.ModelChain.run_model pvlib.modelchain.ModelChain.run_model_from_poa """ self._check_multiple_input(data) From db8d04616d8bb8c1d00ceebf5f81130755b64b29 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 14 Dec 2020 08:18:05 -0700 Subject: [PATCH 161/236] Expand error messages for missing array params in ModelChain Note that all Arrays must have the correct/required model parameters when an error is raised for missing parameters related to the inference or assignment of the various models used in an instance of ModelChain. --- pvlib/modelchain.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index c563ce5ebe..00d9e313f0 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -670,8 +670,9 @@ def dc_model(self, model): _common_keys(self.system.module_parameters)) if missing_params: # some parameters are not in module.keys() raise ValueError(model + ' selected for the DC model but ' - 'one or more required parameters are ' - 'missing : ' + str(missing_params)) + 'one or more arrays are missing ' + 'one or more required parameters ' + ' : ' + str(missing_params)) if model == 'sapm': self._dc_model = self.sapm elif model == 'desoto': @@ -881,7 +882,8 @@ def infer_aoi_model(self): else: raise ValueError('could not infer AOI model from ' 'system.module_parameters. Check that the ' - 'system.module_parameters contain parameters for ' + 'module_parameters for all Arrays in ' + 'system.arrays contain parameters for ' 'the physical, aoi, ashrae or martin_ruiz model; ' 'explicitly set the model with the aoi_model ' 'kwarg; or set aoi_model="no_loss".') @@ -947,7 +949,8 @@ def infer_spectral_model(self): else: raise ValueError('could not infer spectral model from ' 'system.module_parameters. Check that the ' - 'system.module_parameters contain valid ' + 'module_parameters for all Arrays in ' + 'system.arrays contain valid ' 'first_solar_spectral_coefficients, a valid ' 'Material or Technology value, or set ' 'spectral_model="no_loss".') @@ -994,9 +997,12 @@ def temperature_model(self, model): name_from_params = self.infer_temperature_model().__name__ if self._temperature_model.__name__ != name_from_params: raise ValueError( - f'Temperature model {self._temperature_model.__name__} is' - f'inconsistent with PVSystem temperature model parameters' - f'{_common_keys(self.system.temperature_model_parameters)}') # noqa: E501 + f'Temperature model {self._temperature_model.__name__} is ' + f'inconsistent with PVSystem temperature model ' + f'parameters. All Arrays in system.arrays must have ' + f'consistent parameters. ' + f'{_common_keys(self.system.temperature_model_parameters)}' + ) else: self._temperature_model = partial(model, self) @@ -1016,7 +1022,9 @@ def infer_temperature_model(self): return self.fuentes_temp else: raise ValueError(f'could not infer temperature model from ' - f'system.temperature_module_parameters {params}.') + f'system.temperature_module_parameters. Check ' + f'all Arrays in system.arrays contain parameters ' + f'for the same model (or models) {params}.') def _set_celltemp(self, model): """Set self.results.cell_temp using the given cell temperature model. From 784e1adf2d96e2a98548ae838396ecf7470ce4af Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 14 Dec 2020 08:53:51 -0700 Subject: [PATCH 162/236] Remove ModelChain._check_consistent_params Rework test to maintain coverage on inconsistent array parameters, but expect the error to come from a different place. Mainly this means that the error message has changed and more input is needed to the constructor to make sure that execution goes all the way to the assignment of the temperature model. --- pvlib/modelchain.py | 31 +++-------------------- pvlib/tests/test_modelchain.py | 45 ++++++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 40 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 00d9e313f0..cd1f628e65 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -10,7 +10,6 @@ import itertools import warnings import pandas as pd -import numpy as np from dataclasses import dataclass, field from typing import Union, Tuple, Optional, TypeVar @@ -384,9 +383,6 @@ def __init__(self, system, location, self.solar_position_method = solar_position_method self.airmass_model = airmass_model - # check that every array has parameters for the same models - self._check_consistent_params() - # calls setters self.dc_model = dc_model self.ac_model = ac_model @@ -630,26 +626,6 @@ def orientation_strategy(self, strategy): self._orientation_strategy = strategy - def _check_consistent_params(self): - """Ensure that each all arrays in ``self.system`` have the same - module and temperature model parameters. If parameters differ - a ValueError is raised.""" - # check consistent module_parameters - params = np.unique( - [set(array.module_parameters.keys()) - for array in self.system.arrays]) - if len(params) > 1: - raise ValueError('PVSystem arrays have module_parameters with ' - 'different keys.') - # check consistent temperature_model_parameters - params = np.unique( - [set(array.temperature_model_parameters.keys()) - for array in self.system.arrays]) - if len(params) > 1: - raise ValueError('PVSystem arrays temperature_model_parameters ' - 'have different keys. All arrays should have ' - 'keys for the same cell temperature model.') - @property def dc_model(self): return self._dc_model @@ -1022,9 +998,10 @@ def infer_temperature_model(self): return self.fuentes_temp else: raise ValueError(f'could not infer temperature model from ' - f'system.temperature_module_parameters. Check ' - f'all Arrays in system.arrays contain parameters ' - f'for the same model (or models) {params}.') + f'system.temperature_model_parameters. Check ' + f'that all Arrays in system.arrays have ' + f'parameters for the same model (or models) ' + f'{params}.') def _set_celltemp(self, model): """Set self.results.cell_temp using the given cell temperature model. diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 0f3f46db7d..d0cbb4f682 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1536,22 +1536,41 @@ def test_unknown_attribute(sapm_dc_snl_ac_system, location): def test_inconsistent_array_params(location): - module_error = "PVSystem arrays have module_parameters " \ - r"with different keys\." - temperature_error = "PVSystem arrays temperature_model_parameters " \ - r"have different keys\. All arrays should have " \ - r"keys for the same cell temperature model\." + module_error = ".* selected for the DC model but one or more arrays are " \ + "missing one or more required parameters" + temperature_error = "could not infer temperature model from " \ + r"system\.temperature_model_parameters\. Check " \ + r"that all Arrays in system\.arrays have " \ + r"parameters for the same model \(or models\) .*" different_module_system = pvsystem.PVSystem( - arrays=[pvsystem.Array(module_parameters={'foo': 1}), - pvsystem.Array(module_parameters={'foo': 2}), - pvsystem.Array(module_parameters={'bar': 1})] + arrays=[ + pvsystem.Array( + module_parameters=pvsystem._DC_MODEL_PARAMS['sapm']), + pvsystem.Array( + module_parameters=pvsystem._DC_MODEL_PARAMS['cec']), + pvsystem.Array( + module_parameters=pvsystem._DC_MODEL_PARAMS['cec'])] ) with pytest.raises(ValueError, match=module_error): - _ = ModelChain(different_module_system, location) + ModelChain(different_module_system, location, dc_model='cec') different_temp_system = pvsystem.PVSystem( - arrays=[pvsystem.Array(temperature_model_parameters={'a': 1}), - pvsystem.Array(temperature_model_parameters={'b': 2}), - pvsystem.Array(temperature_model_parameters={'b': 3})] + arrays=[ + pvsystem.Array( + module_parameters=pvsystem._DC_MODEL_PARAMS['cec'], + temperature_model_parameters={'a': 1, + 'b': 1, + 'deltaT': 1}), + pvsystem.Array( + module_parameters=pvsystem._DC_MODEL_PARAMS['cec'], + temperature_model_parameters={'a': 2, + 'b': 2, + 'deltaT': 2}), + pvsystem.Array( + module_parameters=pvsystem._DC_MODEL_PARAMS['cec'], + temperature_model_parameters={'b': 3, 'deltaT': 3})] ) with pytest.raises(ValueError, match=temperature_error): - _ = ModelChain(different_temp_system, location) + ModelChain(different_temp_system, location, + ac_model='sandia_multi', + aoi_model='no_loss', spectral_model='no_loss', + temperature_model='sapm') From 6d1bff528d3503280b7eb5e85b92cd42ab4a79f4 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 14 Dec 2020 09:50:39 -0700 Subject: [PATCH 163/236] Test ModelChain.infer_ac_model() error with one array Make sure that an error is raised for a model chain with a single- array system and invalid inverter_params. --- pvlib/tests/test_modelchain.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index d0cbb4f682..1f0da1b14b 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1076,6 +1076,17 @@ def test_ac_model_not_a_model(pvwatts_dc_pvwatts_ac_system, location, weather): spectral_model='no_loss') +def test_infer_ac_model_invalid_params(location): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array( + module_parameters=pvsystem._DC_MODEL_PARAMS['pvwatts'] + )], + inverter_parameters={'foo': 1, 'bar': 2} + ) + with pytest.raises(ValueError, match='could not infer AC model'): + ModelChain(system, location) + + def constant_aoi_loss(mc): mc.results.aoi_modifier = 0.9 From 192a5504b652635e981a2ae0a0517157e1b3bdd7 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 14 Dec 2020 10:41:56 -0700 Subject: [PATCH 164/236] Capitalize 'Arrays' when referring to the "constituents" of a PVSystem Apply consistent capitalization to make it clear when we are referring to the idea of an array or an Array instance in a PVSystem. --- pvlib/modelchain.py | 38 +++++++++++++++++----------------- pvlib/tests/test_modelchain.py | 14 ++++++------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index cd1f628e65..484daa04a3 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -646,7 +646,7 @@ def dc_model(self, model): _common_keys(self.system.module_parameters)) if missing_params: # some parameters are not in module.keys() raise ValueError(model + ' selected for the DC model but ' - 'one or more arrays are missing ' + 'one or more Arrays are missing ' 'one or more required parameters ' ' : ' + str(missing_params)) if model == 'sapm': @@ -665,7 +665,7 @@ def dc_model(self, model): self._dc_model = partial(model, self) def infer_dc_model(self): - """Infer DC power model from array module parameters.""" + """Infer DC power model from Array module parameters.""" params = _common_keys(self.system.module_parameters) if {'A0', 'A1', 'C7'} <= params: return self.sapm, 'sapm' @@ -794,9 +794,9 @@ def _infer_ac_model_multi(self, inverter_params): return self.sandia_multi_inverter raise ValueError('could not infer multi-array AC model from ' 'system.inverter_parameters. Not all ac models ' - 'support systems with mutiple arrays. ' + 'support systems with mutiple Arrays. ' 'Only sandia_multi supports multiple ' - 'arrays. Check system.inverter_parameters or ' + 'Arrays. Check system.inverter_parameters or ' 'explicitly set the model with the ac_model kwarg.') def sandia_multi_inverter(self): @@ -1110,7 +1110,7 @@ def complete_irradiance(self, weather): are required. Air temperature of 20 C and wind speed of 0 m/s will be added to the DataFrame if not provided. If `weather` is a tuple it must be the same length as the number - of arrays in the system and the indices for each DataFrame must + of Arrays in the system and the indices for each DataFrame must be the same. Returns @@ -1121,7 +1121,7 @@ def complete_irradiance(self, weather): ------ ValueError if the number of dataframes in `weather` is not the same as the - number of arrays in the system or if the indices of all elements + number of Arrays in the system or if the indices of all elements of `weather` are not the same. Notes @@ -1382,7 +1382,7 @@ def prepare_inputs(self, weather): def _check_multiple_input(self, data, strict=True): """Check that the number of elements in `data` is the same as - the number of arrays in `self.system`. If `strict` is False then + the number of Arrays in `self.system`. If `strict` is False then `data` does not have to be a tuple and length validation is not performed. If `data` is a tuple of the correct length the indices of each DataFrame it contains are compared for equality and an @@ -1396,7 +1396,7 @@ def _check_multiple_input(self, data, strict=True): f"{self.system.num_arrays}, " f"got {type(data).__name__}.") if len(data) != self.system.num_arrays: - raise ValueError("Input must be same length as number of arrays " + raise ValueError("Input must be same length as number of Arrays " f"in system. Expected {self.system.num_arrays}, " f"got {len(data)}.") _all_same_index(data) @@ -1416,14 +1416,14 @@ def prepare_inputs_from_poa(self, data): ``'wind_speed'`` are not provided, air temperature of 20 C and wind speed of 0 m/s are assumed. - If there are multiple arrays in the system then `data` must be - a tuple with the same length as the number of arrays. + If there are multiple Arrays in the system then `data` must be + a tuple with the same length as the number of Arrays. Raises ------ ValueError If the number of DataFrames passed in `data` is not the same - as the number of arrays in the system. + as the number of Arrays in the system. Notes ----- @@ -1548,8 +1548,8 @@ def run_model(self, weather): is provided, `temperature_model` must be ``'sapm'``. If `weather` is a tuple it must have the same length as the number - of arrays in the system being modeled. Each element should specify - the POA (and other input) to the corresponding array in the system + of Arrays in the system being modeled. Each element should specify + the POA (and other input) to the corresponding Array in the system being modeled. For example, ``weather[0]`` contains weather data for ``system.arrays[0]``. Each element must satisfy the requirements described above. @@ -1600,8 +1600,8 @@ def run_model_from_poa(self, data): ``'sapm'``. If `data` is a tuple it must have the same length as the number - of arrays in the system being modeled. Each element should specify - the POA (and other input) to the corresponding array in the system + of Arrays in the system being modeled. Each element should specify + the POA (and other input) to the corresponding Array in the system being modeled. For example, ``data[0]`` contains POA irradiance for ``system.arrays[0]``. Each element must satisfy the requirements described above. @@ -1677,10 +1677,10 @@ def run_model_from_effective_irradiance(self, data=None): ``'module_temperature'`` is provided, `temperature_model` must be ``'sapm'``. - If the system has multiple arrays, `data` must be a tuple with - the same length as the number of arrays in the system where + If the system has multiple Arrays, `data` must be a tuple with + the same length as the number of Arrays in the system where each element provides the effective irradiance and weather - for the corresponding array. + for the corresponding Array. Returns ------- @@ -1689,7 +1689,7 @@ def run_model_from_effective_irradiance(self, data=None): Raises ------ ValueError - If the number of arrays is different than the number of data + If the number of Arrays is different than the number of data frames passed in `data` ValueError If `data` is a tuple and the DataFrames it contains have diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 1f0da1b14b..3b68f0b78f 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -422,7 +422,7 @@ def test_ModelChain_invalid_inverter_params_arrays( sapm_dc_snl_ac_system_same_arrays.inverter_parameters = \ inverter_params[inverter] with pytest.raises(ValueError, - match=r'Only sandia_multi supports multiple arrays\.'): + match=r'Only sandia_multi supports multiple Arrays\.'): ModelChain(sapm_dc_snl_ac_system_same_arrays, location) @@ -457,11 +457,11 @@ def test_prepare_inputs_weather_wrong_length( mc = ModelChain(sapm_dc_snl_ac_system_Array, location) weather = pd.DataFrame({'ghi': [1], 'dhi': [1], 'dni': [1]}) with pytest.raises(ValueError, - match="Input must be same length as number of arrays " + match="Input must be same length as number of Arrays " r"in system\. Expected 2, got 1\."): mc.prepare_inputs((weather,)) with pytest.raises(ValueError, - match="Input must be same length as number of arrays " + match="Input must be same length as number of Arrays " r"in system\. Expected 2, got 3\."): mc.prepare_inputs((weather, weather, weather)) @@ -674,7 +674,7 @@ def test_prepare_inputs_from_poa(sapm_dc_snl_ac_system, location, def test_prepare_poa_wrong_number_arrays( sapm_dc_snl_ac_system_Array, location, total_irrad, weather): - len_error = r"Input must be same length as number of arrays in system\. " \ + len_error = r"Input must be same length as number of Arrays in system\. " \ r"Expected 2, got [0-9]+\." type_error = r"Input must be a tuple of length 2, got .*\." mc = ModelChain(sapm_dc_snl_ac_system_Array, location) @@ -837,7 +837,7 @@ def test_run_model_from_effective_irradiance_arrays_error( data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad data['effetive_irradiance'] = data['poa_global'] mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - len_error = r"Input must be same length as number of arrays in system\. " \ + len_error = r"Input must be same length as number of Arrays in system\. " \ r"Expected 2, got [0-9]+\." type_error = r"Input must be a tuple of length 2, got DataFrame\." with pytest.raises(TypeError, match=type_error): @@ -1533,7 +1533,7 @@ def test_complete_irradiance_arrays_wrong_length( 'dhi': [4, 6], 'ghi': [9, 5]}, index=times) error_str = "Input must be same length as number " \ - r"of arrays in system\. Expected 2, got [0-9]+\." + r"of Arrays in system\. Expected 2, got [0-9]+\." with pytest.raises(ValueError, match=error_str): mc.complete_irradiance((weather,)) with pytest.raises(ValueError, match=error_str): @@ -1547,7 +1547,7 @@ def test_unknown_attribute(sapm_dc_snl_ac_system, location): def test_inconsistent_array_params(location): - module_error = ".* selected for the DC model but one or more arrays are " \ + module_error = ".* selected for the DC model but one or more Arrays are " \ "missing one or more required parameters" temperature_error = "could not infer temperature model from " \ r"system\.temperature_model_parameters\. Check " \ From 7c4a0af32c67d923a567b80e4a388710b9a61dbb Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 15 Dec 2020 08:48:21 -0700 Subject: [PATCH 165/236] Return early from ModelChain._prepare_temperature() If all Arrays have a cell_temperature or module_temperature given in the input data then return early and don't try to compute cell temperature based on weather. This supports calling ModelChain.run_from_effective_irradiance() with only 'cell_temperature' and 'effective_irradiance' columns present in the input. --- pvlib/modelchain.py | 9 +++++++-- pvlib/tests/test_modelchain.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 484daa04a3..f675ca43ab 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1513,11 +1513,16 @@ def _prepare_temperature(self, data=None): data = (data,) * self.system.num_arrays elif not isinstance(data, tuple): return self._prepare_temperature_single_array(data) - given_cell_temperature = itertools.starmap( + given_cell_temperature = tuple(itertools.starmap( self._get_cell_temperature, zip(data, self.results.total_irrad, self.system.temperature_model_parameters) - ) + )) + # If cell temperature has been specified for all arrays return + # immediately and do not try to compute it. + if all(cell_temp is not None for cell_temp in given_cell_temperature): + self.results.cell_temperature = given_cell_temperature + return self # Calculate cell temperature from weather data. Cell temperature models # expect total_irrad['poa_global']. self.temperature_model() diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 3b68f0b78f..01cedd6e29 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -871,6 +871,23 @@ def test_run_model_from_effective_irradiance_arrays( assert (mc.results.dc[0] != mc.results.dc[1]).all().all() +def test_run_model_from_effective_irradiance_minimal_input( + sapm_dc_snl_ac_system, sapm_dc_snl_ac_system_Array, + location, total_irrad): + data = pd.DataFrame({'effective_irradiance': total_irrad['poa_global'], + 'cell_temperature': 40}, + index=total_irrad.index) + mc = ModelChain(sapm_dc_snl_ac_system, location) + mc.run_model_from_effective_irradiance(data) + assert not mc.results.dc.empty + assert not mc.results.ac.empty + # test with multiple arrays + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + mc.run_model_from_effective_irradiance((data, data)) + assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) + assert not mc.results.ac.empty + + def poadc(mc): mc.results.dc = mc.results.total_irrad['poa_global'] * 0.2 mc.results.dc.name = None # assert_series_equal will fail without this From 404ffd190b35beb0220b9d7c95c7a712d5066613 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 15 Dec 2020 08:58:49 -0700 Subject: [PATCH 166/236] More test coverage of ModelChain.run_model_from_effective_irradiance() --- pvlib/tests/test_modelchain.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 01cedd6e29..4e1470f45f 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -879,6 +879,8 @@ def test_run_model_from_effective_irradiance_minimal_input( index=total_irrad.index) mc = ModelChain(sapm_dc_snl_ac_system, location) mc.run_model_from_effective_irradiance(data) + # make sure, for a single Array, the result is the correct type and value + assert_series_equal(mc.results.cell_temperature, data['cell_temperature']) assert not mc.results.dc.empty assert not mc.results.ac.empty # test with multiple arrays From 529313ac7a3c8efc6e46e201ea6b5e4398430519 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 15 Dec 2020 10:07:40 -0700 Subject: [PATCH 167/236] Note poa_global requirements for run_model_from_effective_irradiance() Add not about when poa_global is required in the ModelChain.run_model_from_effective_irradiance() docstring. --- pvlib/modelchain.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index f675ca43ab..cb2c70301e 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1685,7 +1685,10 @@ def run_model_from_effective_irradiance(self, data=None): If the system has multiple Arrays, `data` must be a tuple with the same length as the number of Arrays in the system where each element provides the effective irradiance and weather - for the corresponding Array. + for the corresponding Array. Note that if any of the DataFrames + in `data` are missing a ``'cell_temperature'`` column, you must + provide a ``'poa_global'`` column in *every* DataFrame (not just + the one(s) without ``'cell_temperature'``). Returns ------- From ab8f982fb834e92b59ba805ddda9f94e9e0a5c22 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 15 Dec 2020 10:25:01 -0700 Subject: [PATCH 168/236] Move ModelChain.tracking to ModelChain.results.tracking Add tracking to deprecated attribute list. --- pvlib/modelchain.py | 19 ++++++++++--------- pvlib/tests/test_modelchain.py | 12 ++++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index cb2c70301e..85110d5a1d 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -272,6 +272,7 @@ class ModelChainResult: airmass: pd.DataFrame = field(default=None) ac: pd.Series = field(default=None) # per DC array information + tracking: Optional[pd.DataFrame] = field(default=None) total_irrad: Optional[PerArray[pd.DataFrame]] = field(default=None) aoi: Optional[PerArray[pd.Series]] = field(default=None) aoi_modifier: Optional[PerArray[pd.Series]] = field(default=None) @@ -362,7 +363,7 @@ class ModelChain: _deprecated_attrs = ['solar_position', 'airmass', 'total_irrad', 'aoi', 'aoi_modifier', 'spectral_modifier', 'cell_temperature', 'effective_irradiance', - 'dc', 'ac', 'diode_params'] + 'dc', 'ac', 'diode_params', 'tracking'] def __init__(self, system, location, orientation_strategy=None, @@ -1210,16 +1211,16 @@ def _prep_inputs_tracking(self): """ Calculate tracker position and AOI """ - self.tracking = self.system.singleaxis( + self.results.tracking = self.system.singleaxis( self.results.solar_position['apparent_zenith'], self.results.solar_position['azimuth']) - self.tracking['surface_tilt'] = ( - self.tracking['surface_tilt'] + self.results.tracking['surface_tilt'] = ( + self.results.tracking['surface_tilt'] .fillna(self.system.axis_tilt)) - self.tracking['surface_azimuth'] = ( - self.tracking['surface_azimuth'] + self.results.tracking['surface_azimuth'] = ( + self.results.tracking['surface_azimuth'] .fillna(self.system.axis_azimuth)) - self.results.aoi = self.tracking['aoi'] + self.results.aoi = self.results.tracking['aoi'] return self def _prep_inputs_fixed(self): @@ -1360,8 +1361,8 @@ def prepare_inputs(self, weather): self._prep_inputs_tracking() get_irradiance = partial( self.system.get_irradiance, - self.tracking['surface_tilt'], - self.tracking['surface_azimuth'], + self.results.tracking['surface_tilt'], + self.results.tracking['surface_azimuth'], self.results.solar_position['apparent_zenith'], self.results.solar_position['azimuth']) else: diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 4e1470f45f..ea4631ca73 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -641,8 +641,10 @@ def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker): mc = ModelChain(system, location) mc.run_model(weather) assert system.singleaxis.call_count == 1 - assert (mc.tracking.columns == ['tracker_theta', 'aoi', 'surface_azimuth', - 'surface_tilt']).all() + assert (mc.results.tracking.columns == ['tracker_theta', + 'aoi', + 'surface_azimuth', + 'surface_tilt']).all() assert mc.results.ac[0] > 0 assert np.isnan(mc.results.ac[1]) @@ -811,8 +813,10 @@ def test_run_model_from_poa_tracking(sapm_dc_snl_ac_system, location, mc = ModelChain(system, location, aoi_model='no_loss', spectral_model='no_loss') ac = mc.run_model_from_poa(total_irrad).results.ac - assert (mc.tracking.columns == ['tracker_theta', 'aoi', 'surface_azimuth', - 'surface_tilt']).all() + assert (mc.results.tracking.columns == ['tracker_theta', + 'aoi', + 'surface_azimuth', + 'surface_tilt']).all() expected = pd.Series(np.array([149.280238, 96.678385]), index=total_irrad.index) assert_series_equal(ac, expected) From a15eaa72f70142b0406cc54613124b90d0c06168 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 15 Dec 2020 10:28:59 -0700 Subject: [PATCH 169/236] Improve pvsystem.Array docstring Note that Array represents an array at a 'fixed orientation' --- pvlib/pvsystem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index d786591ea7..81693b83f4 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1093,11 +1093,11 @@ def __repr__(self): class Array: """ - An Array is a set of of modules at a specific orientation. + An Array is a set of of modules at a fixed orientation. Specifically, an array is defined by tilt, azimuth, the - module parameters, the number of strings of modules and the - number of modules on each string. + module parameters, the number of parallel strings of modules + and the number of modules on each string. Parameters ---------- From ce7f95df45ef3cf3e985b451e691f29cbc770132 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 15 Dec 2020 12:41:22 -0700 Subject: [PATCH 170/236] Better documentation for ModelChain._check_multiple_input() Provides a more thorough and informative description of the `strict` parameter. --- pvlib/modelchain.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 85110d5a1d..4af8c2a365 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1383,11 +1383,16 @@ def prepare_inputs(self, weather): def _check_multiple_input(self, data, strict=True): """Check that the number of elements in `data` is the same as - the number of Arrays in `self.system`. If `strict` is False then - `data` does not have to be a tuple and length validation is not - performed. If `data` is a tuple of the correct length the indices - of each DataFrame it contains are compared for equality and an - error is raised if they differ. + the number of Arrays in `self.system`. + + In most cases if ``self.system.num_arrays`` is greater than 1 we + want to raise an error when `data` is not a tuple; however, that + behavior can be suppressed by setting ``strict=False``. This is + useful for validating inputs such as GHI, DHI, DNI, wind speed, or + air temperature that can be applied a ``PVSystem`` as a system-wide + input. In this case we want to ensure that when a tuple is provided + it has the same length as the number of Arrays, but we do not want + to fail if the input is not a tuple. """ if (not strict or self.system.num_arrays == 1) \ and not isinstance(data, tuple): From b53d2be1d908748749914097ccfadc50527325ac Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 15 Dec 2020 13:14:54 -0700 Subject: [PATCH 171/236] Raise ValueError from ModelChain._prepare_temperature() If the input data is missing cell_temperature for some Arrays then the input must specify poa_global for _all_ Arrays. --- pvlib/modelchain.py | 20 ++++++++++++++++++-- pvlib/tests/test_modelchain.py | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 4af8c2a365..e9d8f45ac9 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1529,8 +1529,24 @@ def _prepare_temperature(self, data=None): if all(cell_temp is not None for cell_temp in given_cell_temperature): self.results.cell_temperature = given_cell_temperature return self - # Calculate cell temperature from weather data. Cell temperature models - # expect total_irrad['poa_global']. + # Calculate cell temperature from weather data. If cell_temperature + # has not been provided for some arrays then it is computed with + # ModelChain.temperature_model(). Because this operates on all Arrays + # simultaneously, 'poa_global' must be known for all arrays, including + # those that have a known cell temperature. + try: + self._verify_df(self.results.total_irrad, ['poa_global']) + except ValueError: + # Provide a more informative error message. Because only + # run_model_from_effective_irradiance() can get to this point + # without known POA we can suggest a very specific remedy in the + # error message. + raise ValueError("Incomplete input data. Data must contain " + "'poa_global'. For systems with multiple Arrays " + "if you have provided 'cell_temperature' for " + "only a subset of Arrays you must provide " + "'poa_global' for all Arrays, including those " + "that have a known 'cell_temperature'.") self.temperature_model() # replace calculated cell temperature with temperature given in `data` # where available. diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index ea4631ca73..04877cc374 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -894,6 +894,23 @@ def test_run_model_from_effective_irradiance_minimal_input( assert not mc.results.ac.empty +def test_run_model_from_effective_irradiance_missing_poa( + sapm_dc_snl_ac_system_Array, location, total_irrad): + data_incomplete = pd.DataFrame( + {'effective_irradiance': total_irrad['poa_global'], + 'poa_global': total_irrad['poa_global']}, + index=total_irrad.index) + data_complete = pd.DataFrame( + {'effective_irradiance': total_irrad['poa_global'], + 'cell_temperature': 30}, + index=total_irrad.index) + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + with pytest.raises(ValueError, + match="you must provide 'poa_global' for every Array"): + mc.run_model_from_effective_irradiance( + (data_complete, data_incomplete)) + + def poadc(mc): mc.results.dc = mc.results.total_irrad['poa_global'] * 0.2 mc.results.dc.name = None # assert_series_equal will fail without this From 224c96890c69630346680d51d2873043e5c5c4cf Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 15 Dec 2020 13:21:50 -0700 Subject: [PATCH 172/236] Update expected error message for run_model_from_effective_irradiance() --- pvlib/tests/test_modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 04877cc374..53cf23d1b1 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -906,7 +906,7 @@ def test_run_model_from_effective_irradiance_missing_poa( index=total_irrad.index) mc = ModelChain(sapm_dc_snl_ac_system_Array, location) with pytest.raises(ValueError, - match="you must provide 'poa_global' for every Array"): + match="you must provide 'poa_global' for all Arrays"): mc.run_model_from_effective_irradiance( (data_complete, data_incomplete)) From 07b9b5b69c0a66d75cbf5ed4a16469dc46920b0b Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 15 Dec 2020 14:29:05 -0700 Subject: [PATCH 173/236] coerce weather/data to tuple in run_model methods --- pvlib/modelchain.py | 69 +++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 85110d5a1d..3f3f38b310 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -17,7 +17,7 @@ temperature, tools) from pvlib.tracking import SingleAxisTracker import pvlib.irradiance # avoid name conflict with full import -from pvlib.pvsystem import _DC_MODEL_PARAMS +from pvlib.pvsystem import _DC_MODEL_PARAMS, _unwrap_single_value from pvlib._deprecation import pvlibDeprecationWarning from pvlib.tools import _build_kwargs @@ -1544,7 +1544,7 @@ def run_model(self, weather): Parameters ---------- - weather : DataFrame or tuple of DataFrame + weather : DataFrame, or list or tuple of DataFrame Irradiance column names must include ``'dni'``, ``'ghi'``, and ``'dhi'``. If optional columns ``'temp_air'`` and ``'wind_speed'`` are not provided, air temperature of 20 C and wind speed of 0 m/s @@ -1553,17 +1553,21 @@ def run_model(self, weather): of `temperature_model`. If optional column `module_temperature` is provided, `temperature_model` must be ``'sapm'``. - If `weather` is a tuple it must have the same length as the number - of Arrays in the system being modeled. Each element should specify - the POA (and other input) to the corresponding Array in the system - being modeled. For example, ``weather[0]`` contains weather data - for ``system.arrays[0]``. Each element must satisfy the - requirements described above. + If list or tuple, must be of the same length and order as the + Arrays of the ModelChain's PVSystem. Returns ------- self + Raises + ------ + ValueError + If the number of DataFrames in `data` is different than the number + of Arrays in the PVSystem. + ValueError + If the DataFrames in `data` have different indexes. + Notes ----- Assigns attributes: ``solar_position``, ``airmass``, ``weather``, @@ -1576,6 +1580,7 @@ def run_model(self, weather): pvlib.modelchain.ModelChain.run_model_from_poa pvlib.modelchain.ModelChain.run_model_from_effective_irradiance """ + weather = _to_tuple(weather) self.prepare_inputs(weather) self.aoi_model() self.spectral_model() @@ -1595,7 +1600,7 @@ def run_model_from_poa(self, data): Parameters ---------- - data : DataFrame or tuple of DataFrame + data : DataFrame, or list or tuple of DataFrame Required column names include ``'poa_global'``, ``'poa_direct'`` and ``'poa_diffuse'``. If optional columns ``'temp_air'`` and ``'wind_speed'`` are not provided, air @@ -1605,17 +1610,23 @@ def run_model_from_poa(self, data): ``'module_temperature'`` is provided, `temperature_model` must be ``'sapm'``. - If `data` is a tuple it must have the same length as the number - of Arrays in the system being modeled. Each element should specify - the POA (and other input) to the corresponding Array in the system - being modeled. For example, ``data[0]`` contains POA irradiance - for ``system.arrays[0]``. Each element must satisfy the - requirements described above. + If the ModelChain's PVSystem has multiple arrays, `data` must be a + list or tuple with the same length and order as the PVsystem's + Arrays. Each element of `data` provides the irradiance and weather + for the corresponding array. Returns ------- self + Raises + ------ + ValueError + If the number of DataFrames in `data` is different than the number + of Arrays in the PVSystem. + ValueError + If the DataFrames in `data` have different indexes. + Notes ----- Assigns attributes: ``solar_position``, ``airmass``, ``weather``, @@ -1628,7 +1639,7 @@ def run_model_from_poa(self, data): pvlib.modelchain.ModelChain.run_model pvlib.modelchain.ModelChain.run_model_from_effective_irradiance """ - + data = _to_tuple(data) self.prepare_inputs_from_poa(data) self.aoi_model() @@ -1676,20 +1687,17 @@ def run_model_from_effective_irradiance(self, data=None): Parameters ---------- - data : DataFrame or tuple of DataFrame, default None + data : DataFrame, or list or tuple of DataFrame Required column is ``'effective_irradiance'``. If optional column ``'cell_temperature'`` is provided, these values are used instead of `temperature_model`. If optional column ``'module_temperature'`` is provided, `temperature_model` must be ``'sapm'``. - If the system has multiple Arrays, `data` must be a tuple with - the same length as the number of Arrays in the system where - each element provides the effective irradiance and weather - for the corresponding Array. Note that if any of the DataFrames - in `data` are missing a ``'cell_temperature'`` column, you must - provide a ``'poa_global'`` column in *every* DataFrame (not just - the one(s) without ``'cell_temperature'``). + If the ModelChain's PVSystem has multiple arrays, `data` must be a + list or tuple with the same length and order as the PVsystem's + Arrays. Each element of `data` provides the irradiance and weather + for the corresponding array. Returns ------- @@ -1698,11 +1706,10 @@ def run_model_from_effective_irradiance(self, data=None): Raises ------ ValueError - If the number of Arrays is different than the number of data - frames passed in `data` + If the number of DataFrames in `data` is different than the number + of Arrays in the PVSystem. ValueError - If `data` is a tuple and the DataFrames it contains have - different indices. + If the DataFrames in `data` have different indexes. Notes ----- @@ -1715,6 +1722,7 @@ def run_model_from_effective_irradiance(self, data=None): pvlib.modelchain.ModelChain.run_model pvlib.modelchain.ModelChain.run_model_from_poa """ + data = _to_tuple(data) self._check_multiple_input(data) self._assign_weather(data) self._assign_total_irrad(data) @@ -1775,3 +1783,8 @@ def _tuple_from_dfs(dfs, name): return tuple(df[name] for df in dfs) else: return dfs[name] + + +@_unwrap_single_value +def _to_tuple(x): + return tuple(x) From d0fc6eb18ec3f5aa2dea9488d8064acfc5ea9c4a Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 15 Dec 2020 16:06:19 -0700 Subject: [PATCH 174/236] don't use decorator, _to_tuple in complete_irradiance --- pvlib/modelchain.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 4f3b34967d..f521725e64 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -17,7 +17,7 @@ temperature, tools) from pvlib.tracking import SingleAxisTracker import pvlib.irradiance # avoid name conflict with full import -from pvlib.pvsystem import _DC_MODEL_PARAMS, _unwrap_single_value +from pvlib.pvsystem import _DC_MODEL_PARAMS from pvlib._deprecation import pvlibDeprecationWarning from pvlib.tools import _build_kwargs @@ -1149,6 +1149,7 @@ def complete_irradiance(self, weather): self._check_multiple_input(weather) # Don't use ModelChain._assign_weather() here because it adds # temperature and wind-speed columns which we do not need here. + weather = _to_tuple(weather) self.weather = _copy(weather) self._assign_times() self.results.solar_position = self.location.get_solarposition( @@ -1806,6 +1807,7 @@ def _tuple_from_dfs(dfs, name): return dfs[name] -@_unwrap_single_value def _to_tuple(x): + if not isinstance(x, (tuple, list)): + return x return tuple(x) From fbb27a8a77a496a48394e7a0f1bd2accda345b44 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 16 Dec 2020 13:37:58 -0700 Subject: [PATCH 175/236] adjust modelchain, add tests for [weather, weather], add test_run_model_from_poa_arrays --- pvlib/modelchain.py | 2 +- pvlib/tests/test_modelchain.py | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index f521725e64..615411a08a 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1146,10 +1146,10 @@ def complete_irradiance(self, weather): >>> mc = ModelChain(my_system, my_location) # doctest: +SKIP >>> mc.run_model(my_weather) # doctest: +SKIP """ + weather = _to_tuple(weather) self._check_multiple_input(weather) # Don't use ModelChain._assign_weather() here because it adds # temperature and wind-speed columns which we do not need here. - weather = _to_tuple(weather) self.weather = _copy(weather) self._assign_times() self.results.solar_position = self.location.get_solarposition( diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 53cf23d1b1..3ef6b341f9 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -411,6 +411,26 @@ def test_run_model_from_irradiance_arrays_no_loss( mc_both.results.dc[1], mc_two.results.dc ) + # repeat test with tuple/list of weather + mc_both_results = mc_both.results.copy() + mc_both.run_model((irradiance, irradiance)) + assert_frame_equal( + mc_both.results.dc[0], + mc_both_results.dc[0] + ) + assert_frame_equal( + mc_both.results.dc[1], + mc_both_results.dc[1] + ) + mc_both.run_model([irradiance, irradiance]) + assert_frame_equal( + mc_both.results.dc[0], + mc_both_results.dc[0] + ) + assert_frame_equal( + mc_both.results.dc[1], + mc_both_results.dc[1] + ) @pytest.mark.parametrize('inverter', ['adr', 'pvwatts']) @@ -531,6 +551,10 @@ def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays, location): mc.run_model((weather_one, weather_two)) assert (mc.results.dc[0] != mc.results.dc[1]).all().all() assert not mc.results.ac.empty + # test with list of weather + mc.run_model([weather_one, weather_two]) + assert (mc.results.dc[0] != mc.results.dc[1]).all().all() + assert not mc.results.ac.empty def test_run_model_perez(sapm_dc_snl_ac_system, location): @@ -802,6 +826,22 @@ def test_run_model_from_poa(sapm_dc_snl_ac_system, location, total_irrad): assert_series_equal(ac, expected) +def test_run_model_from_poa_arrays(sapm_dc_snl_ac_system_Array, location, + weather, total_irrad): + data = weather.copy() + data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad + mc = ModelChain(sapm_dc_snl_ac_system_Array, location, aoi_model='no_loss', + spectral_model='no_loss') + mc.run_model_from_poa((data, data)) + # arrays have different orientation, but should give same dc power + # because we are the same passing POA irradiance and air + # temperature. + assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) + # test with list instead of tuple + mc.run_model_from_poa([data, data]) + assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) + + def test_run_model_from_poa_tracking(sapm_dc_snl_ac_system, location, total_irrad): system = SingleAxisTracker( @@ -869,6 +909,10 @@ def test_run_model_from_effective_irradiance_arrays( # because we are the same passing effective irradiance and cell # temperature. assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) + # test with list instead of tuple + mc.run_model_from_effective_irradiance([data, data]) + assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) + # test that unequal inputs create unequal results data_two = data.copy() data_two['effective_irradiance'] = data['poa_global'] * 0.5 mc.run_model_from_effective_irradiance((data, data_two)) From 6ca45cc8d441700d8e6a545c59030f803358b6a3 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 16 Dec 2020 14:21:53 -0700 Subject: [PATCH 176/236] Parameterize types in tests Co-authored-by: Will Vining --- pvlib/tests/test_modelchain.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 3ef6b341f9..76e767e555 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -826,20 +826,18 @@ def test_run_model_from_poa(sapm_dc_snl_ac_system, location, total_irrad): assert_series_equal(ac, expected) +@pytest.mark.parametrize("input_type", [tuple, list]) def test_run_model_from_poa_arrays(sapm_dc_snl_ac_system_Array, location, - weather, total_irrad): + weather, total_irrad, input_type): data = weather.copy() data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad mc = ModelChain(sapm_dc_snl_ac_system_Array, location, aoi_model='no_loss', spectral_model='no_loss') - mc.run_model_from_poa((data, data)) + mc.run_model_from_poa(input_type((data, data))) # arrays have different orientation, but should give same dc power # because we are the same passing POA irradiance and air # temperature. assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) - # test with list instead of tuple - mc.run_model_from_poa([data, data]) - assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) def test_run_model_from_poa_tracking(sapm_dc_snl_ac_system, location, From aecf15668753eb2d5bfdd74ab7164465ab290eb9 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 16 Dec 2020 15:22:17 -0700 Subject: [PATCH 177/236] add tuple/list type tests --- pvlib/modelchain.py | 27 +++++++-------- pvlib/tests/test_modelchain.py | 62 ++++++++++++++++++++++------------ 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 615411a08a..adc06f103f 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1307,26 +1307,25 @@ def prepare_inputs(self, weather): Parameters ---------- - weather : DataFrame or tuple of DataFrame - Column names must be ``'dni'``, ``'ghi'``, ``'dhi'``, - ``'wind_speed'``, ``'temp_air'``. All irradiance components - are required. Air temperature of 20 C and wind speed - of 0 m/s will be added to the DataFrame if not provided. + weather : DataFrame, or tuple or list of DataFrame + Required column names include ``'dni'``, ``'ghi'``, ``'dhi'``. + Optional column names are ``'wind_speed'``, ``'temp_air'``; if not + provided, air temperature of 20 C and wind speed + of 0 m/s will be added to the DataFrame. - If `weather` is a tuple each DataFrame it contains must have - the same index and it must be the same length as the number - of Arrays in the system. + If `weather` is a tuple or list, it must be of the same length and + order as the Arrays of the ModelChain's PVSystem. Raises ------ ValueError - If the `weather` DataFrame(s) are missing an irradiance component. + If any `weather` DataFrame(s) is missing an irradiance component. ValueError - If `weather` is a tuple and the DataFrames it contains have + If `weather` is a tuple or list and the DataFrames it contains have different indices. ValueError - If `weather` is a tuple with a different length than the number - of Arrays in the system. + If `weather` is a tuple or list with a different length than the + number of Arrays in the system. Notes ----- @@ -1423,8 +1422,8 @@ def prepare_inputs_from_poa(self, data): ``'wind_speed'`` are not provided, air temperature of 20 C and wind speed of 0 m/s are assumed. - If there are multiple Arrays in the system then `data` must be - a tuple with the same length as the number of Arrays. + If list or tuple, must be of the same length and order as the + Arrays of the ModelChain's PVSystem. Raises ------ diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 76e767e555..9a791f4fa7 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -328,6 +328,7 @@ def test_orientation_strategy(strategy, expected, sapm_dc_snl_ac_system, assert sapm_dc_snl_ac_system.surface_tilt == expected[0] assert sapm_dc_snl_ac_system.surface_azimuth == expected[1] + def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location) times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') @@ -446,6 +447,16 @@ def test_ModelChain_invalid_inverter_params_arrays( ModelChain(sapm_dc_snl_ac_system_same_arrays, location) +@pytest.mark.parametrize("input_type", [tuple, list]) +def test_prepare_inputs_multi_weather( + sapm_dc_snl_ac_system_Array, location, input_type): + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + weather = pd.DataFrame({'ghi': [1], 'dhi': [1], 'dni': [1]}) + mc.prepare_inputs(input_type((weather, weather))) + num_arrays = sapm_dc_snl_ac_system_Array.num_arrays + assert len(mc.results.total_irrad) == num_arrays + + def test_prepare_inputs_no_irradiance(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location) weather = pd.DataFrame() @@ -537,7 +548,9 @@ def test_prepare_inputs_missing_irrad_component( mc.prepare_inputs(weather) -def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays, location): +@pytest.mark.parametrize("input_type", [tuple, list]) +def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays, location, + input_type): mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) times = pd.date_range('20200101 1200-0700', periods=2, freq='2H') weather_one = pd.DataFrame({'dni': [900, 800], @@ -548,11 +561,7 @@ def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays, location): 'ghi': [300, 200], 'dhi': [75, 65]}, index=times) - mc.run_model((weather_one, weather_two)) - assert (mc.results.dc[0] != mc.results.dc[1]).all().all() - assert not mc.results.ac.empty - # test with list of weather - mc.run_model([weather_one, weather_two]) + mc.run_model(input_type((weather_one, weather_two))) assert (mc.results.dc[0] != mc.results.dc[1]).all().all() assert not mc.results.ac.empty @@ -698,7 +707,18 @@ def test_prepare_inputs_from_poa(sapm_dc_snl_ac_system, location, assert_frame_equal(mc.results.total_irrad, total_irrad) -def test_prepare_poa_wrong_number_arrays( +@pytest.mark.parametrize("input_type", [tuple, list]) +def test_prepare_inputs_from_poa_multi_data( + sapm_dc_snl_ac_system_Array, location, total_irrad, weather, + input_type): + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + poa = pd.concat([weather, total_irrad], axis=1) + mc.prepare_inputs_from_poa(input_type((poa, poa))) + num_arrays = sapm_dc_snl_ac_system_Array.num_arrays + assert len(mc.results.total_irrad) == num_arrays + + +def test_prepare_inputs_from_poa_wrong_number_arrays( sapm_dc_snl_ac_system_Array, location, total_irrad, weather): len_error = r"Input must be same length as number of Arrays in system\. " \ r"Expected 2, got [0-9]+\." @@ -713,7 +733,7 @@ def test_prepare_poa_wrong_number_arrays( mc.prepare_inputs_from_poa((poa, poa, poa)) -def test_prepare_poa_arrays_different_indices( +def test_prepare_inputs_from_poa_arrays_different_indices( sapm_dc_snl_ac_system_Array, location, total_irrad, weather): error_str = r"Input DataFrames must have same index\." mc = ModelChain(sapm_dc_snl_ac_system_Array, location) @@ -722,7 +742,7 @@ def test_prepare_poa_arrays_different_indices( mc.prepare_inputs_from_poa((poa, poa.shift(periods=1, freq='6H'))) -def test_prepare_poa_arrays_missing_column( +def test_prepare_inputs_from_poa_arrays_missing_column( sapm_dc_snl_ac_system_Array, location, weather, total_irrad): mc = ModelChain(sapm_dc_snl_ac_system_Array, location) poa = pd.concat([weather, total_irrad], axis=1) @@ -895,25 +915,24 @@ def test_run_model_from_effective_irradiance_arrays_error( ) +@pytest.mark.parametrize("input_type", [tuple, list]) def test_run_model_from_effective_irradiance_arrays( - sapm_dc_snl_ac_system_Array, location, weather, total_irrad): + sapm_dc_snl_ac_system_Array, location, weather, total_irrad, + input_type): data = weather.copy() data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad data['effective_irradiance'] = data['poa_global'] data['cell_temperature'] = 40 mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - mc.run_model_from_effective_irradiance((data, data)) + mc.run_model_from_effective_irradiance(input_type((data, data))) # arrays have different orientation, but should give same dc power # because we are the same passing effective irradiance and cell # temperature. assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) - # test with list instead of tuple - mc.run_model_from_effective_irradiance([data, data]) - assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) # test that unequal inputs create unequal results data_two = data.copy() data_two['effective_irradiance'] = data['poa_global'] * 0.5 - mc.run_model_from_effective_irradiance((data, data_two)) + mc.run_model_from_effective_irradiance(input_type(data, data_two)) assert (mc.results.dc[0] != mc.results.dc[1]).all().all() @@ -1027,7 +1046,6 @@ def test_singlediode_dc_arrays(location, dc_model, assert isinstance(dc, (pd.Series, pd.DataFrame)) - @pytest.mark.parametrize('dc_model', ['sapm', 'cec', 'cec_native']) def test_infer_spectral_model(location, sapm_dc_snl_ac_system, cec_dc_snl_ac_system, @@ -1572,9 +1590,10 @@ def test_complete_irradiance(sapm_dc_snl_ac_system, location): @pytest.mark.filterwarnings("ignore:This function is not safe at the moment") +@pytest.mark.parametrize("input_type", [tuple, list]) @requires_tables def test_complete_irradiance_arrays( - sapm_dc_snl_ac_system_same_arrays, location): + sapm_dc_snl_ac_system_same_arrays, location, input_type): """ModelChain.complete_irradiance can accept a tuple of weather DataFrames.""" times = pd.date_range(start='2020-01-01 0700-0700', periods=2, freq='H') @@ -1584,8 +1603,8 @@ def test_complete_irradiance_arrays( mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) with pytest.raises(ValueError, match=r"Input DataFrames must have same index\."): - mc.complete_irradiance((weather, weather[1:])) - mc.complete_irradiance((weather, weather)) + mc.complete_irradiance(input_type((weather, weather[1:]))) + mc.complete_irradiance(input_type((weather, weather))) for mc_weather in mc.weather: assert_series_equal(mc_weather['dni'], pd.Series([2, 3], index=times, name='dni')) @@ -1594,10 +1613,11 @@ def test_complete_irradiance_arrays( assert_series_equal(mc_weather['ghi'], pd.Series([9, 5], index=times, name='ghi')) mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) - mc.complete_irradiance((weather[['ghi', 'dhi']], weather[['dhi', 'dni']])) + mc.complete_irradiance(input_type((weather[['ghi', 'dhi']], + weather[['dhi', 'dni']]))) assert 'dni' in mc.weather[0].columns assert 'ghi' in mc.weather[1].columns - mc.complete_irradiance((weather, weather[['ghi', 'dni']])) + mc.complete_irradiance(input_type((weather, weather[['ghi', 'dni']]))) assert_series_equal(mc.weather[0]['dhi'], pd.Series([4, 6], index=times, name='dhi')) assert_series_equal(mc.weather[0]['ghi'], From 60e2c5dd0b6b0a9b521f606b5d357e146ee23804 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Thu, 17 Dec 2020 08:10:27 -0700 Subject: [PATCH 178/236] Use unwrap=True to simplify ModelChain._singldiode() This lets us expect a consistent type from the `PVSystem` methods regardless of the number of arrays. Because of that we can remove the control-flow that made the function hard to read. This comes at the cost of needing to unwrap the single value ourselves before returning, but a single if statement at the end of the method is much easier to reason about. Because of the way mocks interact with wrapped functions we need to move the mock from the object layer to the function layer, or it will fail because the signature of the wrapped PVSystem methods does not include the `unwrap` parameter. --- pvlib/modelchain.py | 29 +++++++++++++---------------- pvlib/tests/test_modelchain.py | 2 +- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index e9d8f45ac9..1175035064 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -705,25 +705,22 @@ def _make_diode_params(photocurrent, saturation_current, 'nNsVth': nNsVth} ) params = calcparams_model_function(self.results.effective_irradiance, - self.results.cell_temperature) - if self.system.num_arrays == 1: - self.results.diode_params = _make_diode_params(*params) - self.results.dc = self.system.singlediode(*params) - else: - self.results.diode_params = tuple( - _make_diode_params(*params) for params in params - ) - self.results.dc = tuple( - self.system.singlediode(*params) - for params in params - ) + self.results.cell_temperature, + unwrap=False) + self.results.diode_params = tuple(itertools.starmap( + _make_diode_params, params)) + self.results.dc = tuple(itertools.starmap( + self.system.singlediode, params)) self.results.dc = self.system.scale_voltage_current_power( - self.results.dc + self.results.dc, + unwrap=False ) + self.results.dc = tuple(dc.fillna(0) for dc in self.results.dc) + # If the system has one Array, unwrap the single return value + # to preserve the original behavior of ModelChain if self.system.num_arrays == 1: - self.results.dc = self.results.dc.fillna(0) - else: - self.results.dc = tuple(dc.fillna(0) for dc in self.results.dc) + self.results.diode_params = self.results.diode_params[0] + self.results.dc = self.results.dc[0] return self def desoto(self): diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 53cf23d1b1..6cc1508571 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -947,7 +947,7 @@ def test_infer_dc_model(sapm_dc_snl_ac_system, cec_dc_snl_ac_system, # 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]) + m = mocker.spy(pvsystem, dc_model_function[dc_model]) mc = ModelChain(system, location, aoi_model='no_loss', spectral_model='no_loss', temperature_model=temp_model_function[dc_model]) From 57a0ab8505862b8ca7c08dd5146fd3024b31a780 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Thu, 17 Dec 2020 08:19:05 -0700 Subject: [PATCH 179/236] Add docstrings to helper functions --- pvlib/modelchain.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 1175035064..69427b317b 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1770,6 +1770,8 @@ def _copy(data): def _all_same_index(data): + """Raise a ValueError if all DataFrames in `data` do not have the + same index.""" indexes = map(lambda df: df.index, data) next(indexes, None) for index in indexes: @@ -1778,6 +1780,8 @@ def _all_same_index(data): def _common_keys(dicts): + """Return the intersection of the set of keys for each dictionary + in `dicts`""" if isinstance(dicts, tuple): return set.intersection(*map(set, dicts)) return set(dicts) From 3530fd6f4e460b437553c3a880a84da17293e055 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Thu, 17 Dec 2020 08:48:43 -0700 Subject: [PATCH 180/236] Fix type annotations in ModelChainResults Some fields had the wrong type (solar_position, airmass, ac, dc) --- pvlib/modelchain.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 69427b317b..11b7a3bd23 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -268,9 +268,9 @@ class ModelChainResult: T = TypeVar('T') PerArray = Union[T, Tuple[T, ...]] # system-level information - solar_position: pd.DataFrame = field(default=None) - airmass: pd.DataFrame = field(default=None) - ac: pd.Series = field(default=None) + solar_position: Optional[pd.DataFrame] = field(default=None) + airmass: Optional[pd.DataFrame] = field(default=None) + ac: Optional[pd.Series] = field(default=None) # per DC array information tracking: Optional[pd.DataFrame] = field(default=None) total_irrad: Optional[PerArray[pd.DataFrame]] = field(default=None) @@ -279,7 +279,8 @@ class ModelChainResult: spectral_modifier: Optional[PerArray[pd.Series]] = field(default=None) cell_temperature: Optional[PerArray[pd.Series]] = field(default=None) effective_irradiance: Optional[PerArray[pd.Series]] = field(default=None) - dc: Optional[PerArray[pd.Series]] = field(default=None) + dc: Optional[PerArray[Union[pd.Series, pd.DataFrame]]] = \ + field(default=None) array_ac: Optional[PerArray[pd.Series]] = field(default=None) diode_params: Optional[PerArray[pd.DataFrame]] = field(default=None) From 2644d6a310c451c01a5588748e536f1aaeb60afa Mon Sep 17 00:00:00 2001 From: Will Vining Date: Thu, 17 Dec 2020 09:44:27 -0700 Subject: [PATCH 181/236] Add index information to error message in ModelChain._verify() Provide information to the caller about which input is missing a column. --- pvlib/modelchain.py | 9 +++++---- pvlib/tests/test_modelchain.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 11b7a3bd23..2ecdd69131 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1242,17 +1242,18 @@ def _verify_df(self, data, required): ------ ValueError if any of required are not in data.columns. """ - def _verify(data): + def _verify(data, index=None): if not set(required) <= set(data.columns): + tuple_txt = "" if index is None else f"in element {index} " raise ValueError( "Incomplete input data. Data needs to contain " - f"{required}. Detected data contains: " + f"{required}. Detected data {tuple_txt}contains: " f"{list(data.columns)}") if not isinstance(data, tuple): _verify(data) else: - for array_data in data: - _verify(array_data) + for (i, array_data) in enumerate(data): + _verify(array_data, i) def _assign_weather(self, data): def _build_weather(data): diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 6cc1508571..5ca6e52d5a 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -704,7 +704,8 @@ def test_prepare_poa_arrays_missing_column( poa = pd.concat([weather, total_irrad], axis=1) with pytest.raises(ValueError, match=r"Incomplete input data\. " r"Data needs to contain .*\. " - r"Detected data contains: .*"): + r"Detected data in element 1 " + r"contains: .*"): mc.prepare_inputs_from_poa((poa, poa.drop(columns='poa_global'))) From 89053bcba6f01b78e927585b4ad224d9b29c5ffe Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 17 Dec 2020 09:48:38 -0700 Subject: [PATCH 182/236] fix copy, add input_type to wrong length tests --- pvlib/tests/test_modelchain.py | 78 ++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 9a791f4fa7..4ee61ef610 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -412,25 +412,43 @@ def test_run_model_from_irradiance_arrays_no_loss( mc_both.results.dc[1], mc_two.results.dc ) - # repeat test with tuple/list of weather - mc_both_results = mc_both.results.copy() - mc_both.run_model((irradiance, irradiance)) - assert_frame_equal( - mc_both.results.dc[0], - mc_both_results.dc[0] + + +@pytest.mark.parametrize("input_type", [tuple, list]) +def test_run_model_from_irradiance_arrays_no_loss_input_type( + multi_array_sapm_dc_snl_ac_system, location, input_type): + mc_both = ModelChain( + multi_array_sapm_dc_snl_ac_system['two_array_system'], + location, + aoi_model='no_loss', + spectral_model='no_loss', + losses_model='no_loss' ) - assert_frame_equal( - mc_both.results.dc[1], - mc_both_results.dc[1] + mc_one = ModelChain( + multi_array_sapm_dc_snl_ac_system['array_one_system'], + location, + aoi_model='no_loss', + spectral_model='no_loss', + losses_model='no_loss' + ) + mc_two = ModelChain( + multi_array_sapm_dc_snl_ac_system['array_two_system'], + location, + aoi_model='no_loss', + spectral_model='no_loss', + losses_model='no_loss' ) - mc_both.run_model([irradiance, irradiance]) + times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') + irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, + index=times) + mc_one.run_model(irradiance) + mc_two.run_model(irradiance) + mc_both.run_model(input_type((irradiance, irradiance))) assert_frame_equal( - mc_both.results.dc[0], - mc_both_results.dc[0] + mc_both.results.dc[0], mc_one.results.dc ) assert_frame_equal( - mc_both.results.dc[1], - mc_both_results.dc[1] + mc_both.results.dc[1], mc_two.results.dc ) @@ -483,18 +501,19 @@ def test_prepare_inputs_arrays_one_missing_irradiance( mc.prepare_inputs((weather_incomplete, weather)) +@pytest.mark.parametrize("input_type", [tuple, list]) def test_prepare_inputs_weather_wrong_length( - sapm_dc_snl_ac_system_Array, location): + sapm_dc_snl_ac_system_Array, location, input_type): mc = ModelChain(sapm_dc_snl_ac_system_Array, location) weather = pd.DataFrame({'ghi': [1], 'dhi': [1], 'dni': [1]}) with pytest.raises(ValueError, match="Input must be same length as number of Arrays " r"in system\. Expected 2, got 1\."): - mc.prepare_inputs((weather,)) + mc.prepare_inputs(input_type((weather,))) with pytest.raises(ValueError, match="Input must be same length as number of Arrays " r"in system\. Expected 2, got 3\."): - mc.prepare_inputs((weather, weather, weather)) + mc.prepare_inputs(input_type((weather, weather, weather))) def test_ModelChain_times_error_arrays(sapm_dc_snl_ac_system_Array, location): @@ -718,8 +737,10 @@ def test_prepare_inputs_from_poa_multi_data( assert len(mc.results.total_irrad) == num_arrays +@pytest.mark.parametrize("input_type", [tuple, list]) def test_prepare_inputs_from_poa_wrong_number_arrays( - sapm_dc_snl_ac_system_Array, location, total_irrad, weather): + sapm_dc_snl_ac_system_Array, location, total_irrad, weather, + input_type): len_error = r"Input must be same length as number of Arrays in system\. " \ r"Expected 2, got [0-9]+\." type_error = r"Input must be a tuple of length 2, got .*\." @@ -728,9 +749,9 @@ def test_prepare_inputs_from_poa_wrong_number_arrays( with pytest.raises(TypeError, match=type_error): mc.prepare_inputs_from_poa(poa) with pytest.raises(ValueError, match=len_error): - mc.prepare_inputs_from_poa((poa,)) + mc.prepare_inputs_from_poa(input_type((poa,))) with pytest.raises(ValueError, match=len_error): - mc.prepare_inputs_from_poa((poa, poa, poa)) + mc.prepare_inputs_from_poa(input_type((poa, poa, poa))) def test_prepare_inputs_from_poa_arrays_different_indices( @@ -893,8 +914,10 @@ def test_run_model_from_effective_irradiance(sapm_dc_snl_ac_system, location, assert_series_equal(ac, expected) +@pytest.mark.parametrize("input_type", [tuple, list]) def test_run_model_from_effective_irradiance_arrays_error( - sapm_dc_snl_ac_system_Array, location, weather, total_irrad): + sapm_dc_snl_ac_system_Array, location, weather, total_irrad, + input_type): data = weather.copy() data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad data['effetive_irradiance'] = data['poa_global'] @@ -905,9 +928,9 @@ def test_run_model_from_effective_irradiance_arrays_error( with pytest.raises(TypeError, match=type_error): mc.run_model_from_effective_irradiance(data) with pytest.raises(ValueError, match=len_error): - mc.run_model_from_effective_irradiance((data,)) + mc.run_model_from_effective_irradiance(input_type((data,))) with pytest.raises(ValueError, match=len_error): - mc.run_model_from_effective_irradiance((data, data, data)) + mc.run_model_from_effective_irradiance(input_type((data, data, data))) with pytest.raises(ValueError, match=r"Input DataFrames must have same index\."): mc.run_model_from_effective_irradiance( @@ -932,7 +955,7 @@ def test_run_model_from_effective_irradiance_arrays( # test that unequal inputs create unequal results data_two = data.copy() data_two['effective_irradiance'] = data['poa_global'] * 0.5 - mc.run_model_from_effective_irradiance(input_type(data, data_two)) + mc.run_model_from_effective_irradiance(input_type((data, data_two))) assert (mc.results.dc[0] != mc.results.dc[1]).all().all() @@ -1627,8 +1650,9 @@ def test_complete_irradiance_arrays( assert 'dhi' in mc.weather[1].columns +@pytest.mark.parametrize("input_type", [tuple, list]) def test_complete_irradiance_arrays_wrong_length( - sapm_dc_snl_ac_system_same_arrays, location): + sapm_dc_snl_ac_system_same_arrays, location, input_type): mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) times = pd.date_range(start='2020-01-01 0700-0700', periods=2, freq='H') weather = pd.DataFrame({'dni': [2, 3], @@ -1637,9 +1661,9 @@ def test_complete_irradiance_arrays_wrong_length( error_str = "Input must be same length as number " \ r"of Arrays in system\. Expected 2, got [0-9]+\." with pytest.raises(ValueError, match=error_str): - mc.complete_irradiance((weather,)) + mc.complete_irradiance(input_type((weather,))) with pytest.raises(ValueError, match=error_str): - mc.complete_irradiance((weather, weather, weather)) + mc.complete_irradiance(input_type((weather, weather, weather))) def test_unknown_attribute(sapm_dc_snl_ac_system, location): From 1cb9e86d88517a0f3f429ae97a4d55ce0075220d Mon Sep 17 00:00:00 2001 From: Will Vining Date: Thu, 17 Dec 2020 10:13:20 -0700 Subject: [PATCH 183/236] Improve error message for inconsistent temperature model params --- pvlib/modelchain.py | 7 ++++--- pvlib/tests/test_modelchain.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 2ecdd69131..4aa1191199 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -975,7 +975,8 @@ def temperature_model(self, model): f'Temperature model {self._temperature_model.__name__} is ' f'inconsistent with PVSystem temperature model ' f'parameters. All Arrays in system.arrays must have ' - f'consistent parameters. ' + f'consistent parameters. Common temperature model ' + f'parameters: ' f'{_common_keys(self.system.temperature_model_parameters)}' ) else: @@ -999,8 +1000,8 @@ def infer_temperature_model(self): raise ValueError(f'could not infer temperature model from ' f'system.temperature_model_parameters. Check ' f'that all Arrays in system.arrays have ' - f'parameters for the same model (or models) ' - f'{params}.') + f'parameters for the same temperature model. ' + f'Common temperature model parameters: {params}.') def _set_celltemp(self, model): """Set self.results.cell_temp using the given cell temperature model. diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 5ca6e52d5a..943ffd0f85 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1593,7 +1593,8 @@ def test_inconsistent_array_params(location): temperature_error = "could not infer temperature model from " \ r"system\.temperature_model_parameters\. Check " \ r"that all Arrays in system\.arrays have " \ - r"parameters for the same model \(or models\) .*" + r"parameters for the same temperature model\. " \ + r"Common temperature model parameters: .*" different_module_system = pvsystem.PVSystem( arrays=[ pvsystem.Array( From a8090510c40dc7a58e26239d7e8a37f368e00e26 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Thu, 17 Dec 2020 10:14:50 -0700 Subject: [PATCH 184/236] Correct attribute name in ModelChain._set_celltemp docstring self.results.cell_temp -> self.results.cell_temperature --- pvlib/modelchain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 4aa1191199..c395965aa3 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1004,7 +1004,8 @@ def infer_temperature_model(self): f'Common temperature model parameters: {params}.') def _set_celltemp(self, model): - """Set self.results.cell_temp using the given cell temperature model. + """Set self.results.cell_temperature using the given cell + temperature model. Parameters ---------- From 956df5a22fa5393d08d0f2bcd0eaf9323b1233b5 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Thu, 17 Dec 2020 10:40:17 -0700 Subject: [PATCH 185/236] Remove unused ModelChainResult.array_ac field --- pvlib/modelchain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index c395965aa3..7010bfd375 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -281,7 +281,6 @@ class ModelChainResult: effective_irradiance: Optional[PerArray[pd.Series]] = field(default=None) dc: Optional[PerArray[Union[pd.Series, pd.DataFrame]]] = \ field(default=None) - array_ac: Optional[PerArray[pd.Series]] = field(default=None) diode_params: Optional[PerArray[pd.DataFrame]] = field(default=None) From 40aa2da3170e6584aa6491a4eba6350afd8a08a4 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 17 Dec 2020 12:24:24 -0700 Subject: [PATCH 186/236] add coerce to prepare_inputs functions --- pvlib/modelchain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index adc06f103f..7bb554ed9a 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1336,6 +1336,7 @@ def prepare_inputs(self, weather): -------- ModelChain.complete_irradiance """ + weather = _to_tuple(weather) self._check_multiple_input(weather, strict=False) self._verify_df(weather, required=['ghi', 'dni', 'dhi']) self._assign_weather(weather) @@ -1440,6 +1441,7 @@ def prepare_inputs_from_poa(self, data): -------- pvlib.modelchain.ModelChain.prepare_inputs """ + data = _to_tuple(data) self._check_multiple_input(data) self._assign_weather(data) @@ -1601,7 +1603,6 @@ def run_model(self, weather): pvlib.modelchain.ModelChain.run_model_from_poa pvlib.modelchain.ModelChain.run_model_from_effective_irradiance """ - weather = _to_tuple(weather) self.prepare_inputs(weather) self.aoi_model() self.spectral_model() From f4f09c7b3c5fab13ec65e0bf006f9953dea0fadb Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 17 Dec 2020 12:37:22 -0700 Subject: [PATCH 187/236] add _to_tuple back to run_model --- pvlib/modelchain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 7bb554ed9a..ae53fd4bf8 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1603,6 +1603,7 @@ def run_model(self, weather): pvlib.modelchain.ModelChain.run_model_from_poa pvlib.modelchain.ModelChain.run_model_from_effective_irradiance """ + weather = _to_tuple(weather) self.prepare_inputs(weather) self.aoi_model() self.spectral_model() From e5914a91a1a550e98e0a92ab322ed8d6ef598526 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 17 Dec 2020 12:49:15 -0700 Subject: [PATCH 188/236] Coerce weather/data inputs to tuple in ModelChain entry points (#1) * coerce weather/data to tuple in run_model methods * add tuple/list type tests * add input_type to wrong length tests * add coerce to prepare_inputs functions Co-authored-by: Will Vining --- pvlib/modelchain.py | 98 +++++++++++++---------- pvlib/tests/test_modelchain.py | 138 ++++++++++++++++++++++++++------- 2 files changed, 169 insertions(+), 67 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 7010bfd375..0052d17d05 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1145,6 +1145,7 @@ def complete_irradiance(self, weather): >>> mc = ModelChain(my_system, my_location) # doctest: +SKIP >>> mc.run_model(my_weather) # doctest: +SKIP """ + weather = _to_tuple(weather) self._check_multiple_input(weather) # Don't use ModelChain._assign_weather() here because it adds # temperature and wind-speed columns which we do not need here. @@ -1306,26 +1307,25 @@ def prepare_inputs(self, weather): Parameters ---------- - weather : DataFrame or tuple of DataFrame - Column names must be ``'dni'``, ``'ghi'``, ``'dhi'``, - ``'wind_speed'``, ``'temp_air'``. All irradiance components - are required. Air temperature of 20 C and wind speed - of 0 m/s will be added to the DataFrame if not provided. + weather : DataFrame, or tuple or list of DataFrame + Required column names include ``'dni'``, ``'ghi'``, ``'dhi'``. + Optional column names are ``'wind_speed'``, ``'temp_air'``; if not + provided, air temperature of 20 C and wind speed + of 0 m/s will be added to the DataFrame. - If `weather` is a tuple each DataFrame it contains must have - the same index and it must be the same length as the number - of Arrays in the system. + If `weather` is a tuple or list, it must be of the same length and + order as the Arrays of the ModelChain's PVSystem. Raises ------ ValueError - If the `weather` DataFrame(s) are missing an irradiance component. + If any `weather` DataFrame(s) is missing an irradiance component. ValueError - If `weather` is a tuple and the DataFrames it contains have + If `weather` is a tuple or list and the DataFrames it contains have different indices. ValueError - If `weather` is a tuple with a different length than the number - of Arrays in the system. + If `weather` is a tuple or list with a different length than the + number of Arrays in the system. Notes ----- @@ -1336,6 +1336,7 @@ def prepare_inputs(self, weather): -------- ModelChain.complete_irradiance """ + weather = _to_tuple(weather) self._check_multiple_input(weather, strict=False) self._verify_df(weather, required=['ghi', 'dni', 'dhi']) self._assign_weather(weather) @@ -1422,8 +1423,8 @@ def prepare_inputs_from_poa(self, data): ``'wind_speed'`` are not provided, air temperature of 20 C and wind speed of 0 m/s are assumed. - If there are multiple Arrays in the system then `data` must be - a tuple with the same length as the number of Arrays. + If list or tuple, must be of the same length and order as the + Arrays of the ModelChain's PVSystem. Raises ------ @@ -1440,6 +1441,7 @@ def prepare_inputs_from_poa(self, data): -------- pvlib.modelchain.ModelChain.prepare_inputs """ + data = _to_tuple(data) self._check_multiple_input(data) self._assign_weather(data) @@ -1565,7 +1567,7 @@ def run_model(self, weather): Parameters ---------- - weather : DataFrame or tuple of DataFrame + weather : DataFrame, or list or tuple of DataFrame Irradiance column names must include ``'dni'``, ``'ghi'``, and ``'dhi'``. If optional columns ``'temp_air'`` and ``'wind_speed'`` are not provided, air temperature of 20 C and wind speed of 0 m/s @@ -1574,17 +1576,21 @@ def run_model(self, weather): of `temperature_model`. If optional column `module_temperature` is provided, `temperature_model` must be ``'sapm'``. - If `weather` is a tuple it must have the same length as the number - of Arrays in the system being modeled. Each element should specify - the POA (and other input) to the corresponding Array in the system - being modeled. For example, ``weather[0]`` contains weather data - for ``system.arrays[0]``. Each element must satisfy the - requirements described above. + If list or tuple, must be of the same length and order as the + Arrays of the ModelChain's PVSystem. Returns ------- self + Raises + ------ + ValueError + If the number of DataFrames in `data` is different than the number + of Arrays in the PVSystem. + ValueError + If the DataFrames in `data` have different indexes. + Notes ----- Assigns attributes: ``solar_position``, ``airmass``, ``weather``, @@ -1597,6 +1603,7 @@ def run_model(self, weather): pvlib.modelchain.ModelChain.run_model_from_poa pvlib.modelchain.ModelChain.run_model_from_effective_irradiance """ + weather = _to_tuple(weather) self.prepare_inputs(weather) self.aoi_model() self.spectral_model() @@ -1616,7 +1623,7 @@ def run_model_from_poa(self, data): Parameters ---------- - data : DataFrame or tuple of DataFrame + data : DataFrame, or list or tuple of DataFrame Required column names include ``'poa_global'``, ``'poa_direct'`` and ``'poa_diffuse'``. If optional columns ``'temp_air'`` and ``'wind_speed'`` are not provided, air @@ -1626,17 +1633,23 @@ def run_model_from_poa(self, data): ``'module_temperature'`` is provided, `temperature_model` must be ``'sapm'``. - If `data` is a tuple it must have the same length as the number - of Arrays in the system being modeled. Each element should specify - the POA (and other input) to the corresponding Array in the system - being modeled. For example, ``data[0]`` contains POA irradiance - for ``system.arrays[0]``. Each element must satisfy the - requirements described above. + If the ModelChain's PVSystem has multiple arrays, `data` must be a + list or tuple with the same length and order as the PVsystem's + Arrays. Each element of `data` provides the irradiance and weather + for the corresponding array. Returns ------- self + Raises + ------ + ValueError + If the number of DataFrames in `data` is different than the number + of Arrays in the PVSystem. + ValueError + If the DataFrames in `data` have different indexes. + Notes ----- Assigns attributes: ``solar_position``, ``airmass``, ``weather``, @@ -1649,7 +1662,7 @@ def run_model_from_poa(self, data): pvlib.modelchain.ModelChain.run_model pvlib.modelchain.ModelChain.run_model_from_effective_irradiance """ - + data = _to_tuple(data) self.prepare_inputs_from_poa(data) self.aoi_model() @@ -1697,20 +1710,17 @@ def run_model_from_effective_irradiance(self, data=None): Parameters ---------- - data : DataFrame or tuple of DataFrame, default None + data : DataFrame, or list or tuple of DataFrame Required column is ``'effective_irradiance'``. If optional column ``'cell_temperature'`` is provided, these values are used instead of `temperature_model`. If optional column ``'module_temperature'`` is provided, `temperature_model` must be ``'sapm'``. - If the system has multiple Arrays, `data` must be a tuple with - the same length as the number of Arrays in the system where - each element provides the effective irradiance and weather - for the corresponding Array. Note that if any of the DataFrames - in `data` are missing a ``'cell_temperature'`` column, you must - provide a ``'poa_global'`` column in *every* DataFrame (not just - the one(s) without ``'cell_temperature'``). + If the ModelChain's PVSystem has multiple arrays, `data` must be a + list or tuple with the same length and order as the PVsystem's + Arrays. Each element of `data` provides the irradiance and weather + for the corresponding array. Returns ------- @@ -1719,11 +1729,10 @@ def run_model_from_effective_irradiance(self, data=None): Raises ------ ValueError - If the number of Arrays is different than the number of data - frames passed in `data` + If the number of DataFrames in `data` is different than the number + of Arrays in the PVSystem. ValueError - If `data` is a tuple and the DataFrames it contains have - different indices. + If the DataFrames in `data` have different indexes. Notes ----- @@ -1736,6 +1745,7 @@ def run_model_from_effective_irradiance(self, data=None): pvlib.modelchain.ModelChain.run_model pvlib.modelchain.ModelChain.run_model_from_poa """ + data = _to_tuple(data) self._check_multiple_input(data) self._assign_weather(data) self._assign_total_irrad(data) @@ -1800,3 +1810,9 @@ def _tuple_from_dfs(dfs, name): return tuple(df[name] for df in dfs) else: return dfs[name] + + +def _to_tuple(x): + if not isinstance(x, (tuple, list)): + return x + return tuple(x) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 943ffd0f85..d0d48d9c1c 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -328,6 +328,7 @@ def test_orientation_strategy(strategy, expected, sapm_dc_snl_ac_system, assert sapm_dc_snl_ac_system.surface_tilt == expected[0] assert sapm_dc_snl_ac_system.surface_azimuth == expected[1] + def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location) times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') @@ -413,6 +414,44 @@ def test_run_model_from_irradiance_arrays_no_loss( ) +@pytest.mark.parametrize("input_type", [tuple, list]) +def test_run_model_from_irradiance_arrays_no_loss_input_type( + multi_array_sapm_dc_snl_ac_system, location, input_type): + mc_both = ModelChain( + multi_array_sapm_dc_snl_ac_system['two_array_system'], + location, + aoi_model='no_loss', + spectral_model='no_loss', + losses_model='no_loss' + ) + mc_one = ModelChain( + multi_array_sapm_dc_snl_ac_system['array_one_system'], + location, + aoi_model='no_loss', + spectral_model='no_loss', + losses_model='no_loss' + ) + mc_two = ModelChain( + multi_array_sapm_dc_snl_ac_system['array_two_system'], + location, + aoi_model='no_loss', + spectral_model='no_loss', + losses_model='no_loss' + ) + times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') + irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, + index=times) + mc_one.run_model(irradiance) + mc_two.run_model(irradiance) + mc_both.run_model(input_type((irradiance, irradiance))) + assert_frame_equal( + mc_both.results.dc[0], mc_one.results.dc + ) + assert_frame_equal( + mc_both.results.dc[1], mc_two.results.dc + ) + + @pytest.mark.parametrize('inverter', ['adr', 'pvwatts']) def test_ModelChain_invalid_inverter_params_arrays( inverter, sapm_dc_snl_ac_system_same_arrays, @@ -426,6 +465,16 @@ def test_ModelChain_invalid_inverter_params_arrays( ModelChain(sapm_dc_snl_ac_system_same_arrays, location) +@pytest.mark.parametrize("input_type", [tuple, list]) +def test_prepare_inputs_multi_weather( + sapm_dc_snl_ac_system_Array, location, input_type): + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + weather = pd.DataFrame({'ghi': [1], 'dhi': [1], 'dni': [1]}) + mc.prepare_inputs(input_type((weather, weather))) + num_arrays = sapm_dc_snl_ac_system_Array.num_arrays + assert len(mc.results.total_irrad) == num_arrays + + def test_prepare_inputs_no_irradiance(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location) weather = pd.DataFrame() @@ -452,18 +501,19 @@ def test_prepare_inputs_arrays_one_missing_irradiance( mc.prepare_inputs((weather_incomplete, weather)) +@pytest.mark.parametrize("input_type", [tuple, list]) def test_prepare_inputs_weather_wrong_length( - sapm_dc_snl_ac_system_Array, location): + sapm_dc_snl_ac_system_Array, location, input_type): mc = ModelChain(sapm_dc_snl_ac_system_Array, location) weather = pd.DataFrame({'ghi': [1], 'dhi': [1], 'dni': [1]}) with pytest.raises(ValueError, match="Input must be same length as number of Arrays " r"in system\. Expected 2, got 1\."): - mc.prepare_inputs((weather,)) + mc.prepare_inputs(input_type((weather,))) with pytest.raises(ValueError, match="Input must be same length as number of Arrays " r"in system\. Expected 2, got 3\."): - mc.prepare_inputs((weather, weather, weather)) + mc.prepare_inputs(input_type((weather, weather, weather))) def test_ModelChain_times_error_arrays(sapm_dc_snl_ac_system_Array, location): @@ -517,7 +567,9 @@ def test_prepare_inputs_missing_irrad_component( mc.prepare_inputs(weather) -def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays, location): +@pytest.mark.parametrize("input_type", [tuple, list]) +def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays, location, + input_type): mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) times = pd.date_range('20200101 1200-0700', periods=2, freq='2H') weather_one = pd.DataFrame({'dni': [900, 800], @@ -528,7 +580,7 @@ def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays, location): 'ghi': [300, 200], 'dhi': [75, 65]}, index=times) - mc.run_model((weather_one, weather_two)) + mc.run_model(input_type((weather_one, weather_two))) assert (mc.results.dc[0] != mc.results.dc[1]).all().all() assert not mc.results.ac.empty @@ -674,8 +726,21 @@ def test_prepare_inputs_from_poa(sapm_dc_snl_ac_system, location, assert_frame_equal(mc.results.total_irrad, total_irrad) -def test_prepare_poa_wrong_number_arrays( - sapm_dc_snl_ac_system_Array, location, total_irrad, weather): +@pytest.mark.parametrize("input_type", [tuple, list]) +def test_prepare_inputs_from_poa_multi_data( + sapm_dc_snl_ac_system_Array, location, total_irrad, weather, + input_type): + mc = ModelChain(sapm_dc_snl_ac_system_Array, location) + poa = pd.concat([weather, total_irrad], axis=1) + mc.prepare_inputs_from_poa(input_type((poa, poa))) + num_arrays = sapm_dc_snl_ac_system_Array.num_arrays + assert len(mc.results.total_irrad) == num_arrays + + +@pytest.mark.parametrize("input_type", [tuple, list]) +def test_prepare_inputs_from_poa_wrong_number_arrays( + sapm_dc_snl_ac_system_Array, location, total_irrad, weather, + input_type): len_error = r"Input must be same length as number of Arrays in system\. " \ r"Expected 2, got [0-9]+\." type_error = r"Input must be a tuple of length 2, got .*\." @@ -684,12 +749,12 @@ def test_prepare_poa_wrong_number_arrays( with pytest.raises(TypeError, match=type_error): mc.prepare_inputs_from_poa(poa) with pytest.raises(ValueError, match=len_error): - mc.prepare_inputs_from_poa((poa,)) + mc.prepare_inputs_from_poa(input_type((poa,))) with pytest.raises(ValueError, match=len_error): - mc.prepare_inputs_from_poa((poa, poa, poa)) + mc.prepare_inputs_from_poa(input_type((poa, poa, poa))) -def test_prepare_poa_arrays_different_indices( +def test_prepare_inputs_from_poa_arrays_different_indices( sapm_dc_snl_ac_system_Array, location, total_irrad, weather): error_str = r"Input DataFrames must have same index\." mc = ModelChain(sapm_dc_snl_ac_system_Array, location) @@ -698,7 +763,7 @@ def test_prepare_poa_arrays_different_indices( mc.prepare_inputs_from_poa((poa, poa.shift(periods=1, freq='6H'))) -def test_prepare_poa_arrays_missing_column( +def test_prepare_inputs_from_poa_arrays_missing_column( sapm_dc_snl_ac_system_Array, location, weather, total_irrad): mc = ModelChain(sapm_dc_snl_ac_system_Array, location) poa = pd.concat([weather, total_irrad], axis=1) @@ -803,6 +868,20 @@ def test_run_model_from_poa(sapm_dc_snl_ac_system, location, total_irrad): assert_series_equal(ac, expected) +@pytest.mark.parametrize("input_type", [tuple, list]) +def test_run_model_from_poa_arrays(sapm_dc_snl_ac_system_Array, location, + weather, total_irrad, input_type): + data = weather.copy() + data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad + mc = ModelChain(sapm_dc_snl_ac_system_Array, location, aoi_model='no_loss', + spectral_model='no_loss') + mc.run_model_from_poa(input_type((data, data))) + # arrays have different orientation, but should give same dc power + # because we are the same passing POA irradiance and air + # temperature. + assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) + + def test_run_model_from_poa_tracking(sapm_dc_snl_ac_system, location, total_irrad): system = SingleAxisTracker( @@ -836,8 +915,10 @@ def test_run_model_from_effective_irradiance(sapm_dc_snl_ac_system, location, assert_series_equal(ac, expected) +@pytest.mark.parametrize("input_type", [tuple, list]) def test_run_model_from_effective_irradiance_arrays_error( - sapm_dc_snl_ac_system_Array, location, weather, total_irrad): + sapm_dc_snl_ac_system_Array, location, weather, total_irrad, + input_type): data = weather.copy() data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad data['effetive_irradiance'] = data['poa_global'] @@ -848,9 +929,9 @@ def test_run_model_from_effective_irradiance_arrays_error( with pytest.raises(TypeError, match=type_error): mc.run_model_from_effective_irradiance(data) with pytest.raises(ValueError, match=len_error): - mc.run_model_from_effective_irradiance((data,)) + mc.run_model_from_effective_irradiance(input_type((data,))) with pytest.raises(ValueError, match=len_error): - mc.run_model_from_effective_irradiance((data, data, data)) + mc.run_model_from_effective_irradiance(input_type((data, data, data))) with pytest.raises(ValueError, match=r"Input DataFrames must have same index\."): mc.run_model_from_effective_irradiance( @@ -858,21 +939,24 @@ def test_run_model_from_effective_irradiance_arrays_error( ) +@pytest.mark.parametrize("input_type", [tuple, list]) def test_run_model_from_effective_irradiance_arrays( - sapm_dc_snl_ac_system_Array, location, weather, total_irrad): + sapm_dc_snl_ac_system_Array, location, weather, total_irrad, + input_type): data = weather.copy() data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad data['effective_irradiance'] = data['poa_global'] data['cell_temperature'] = 40 mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - mc.run_model_from_effective_irradiance((data, data)) + mc.run_model_from_effective_irradiance(input_type((data, data))) # arrays have different orientation, but should give same dc power # because we are the same passing effective irradiance and cell # temperature. assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) + # test that unequal inputs create unequal results data_two = data.copy() data_two['effective_irradiance'] = data['poa_global'] * 0.5 - mc.run_model_from_effective_irradiance((data, data_two)) + mc.run_model_from_effective_irradiance(input_type((data, data_two))) assert (mc.results.dc[0] != mc.results.dc[1]).all().all() @@ -986,7 +1070,6 @@ def test_singlediode_dc_arrays(location, dc_model, assert isinstance(dc, (pd.Series, pd.DataFrame)) - @pytest.mark.parametrize('dc_model', ['sapm', 'cec', 'cec_native']) def test_infer_spectral_model(location, sapm_dc_snl_ac_system, cec_dc_snl_ac_system, @@ -1531,9 +1614,10 @@ def test_complete_irradiance(sapm_dc_snl_ac_system, location): @pytest.mark.filterwarnings("ignore:This function is not safe at the moment") +@pytest.mark.parametrize("input_type", [tuple, list]) @requires_tables def test_complete_irradiance_arrays( - sapm_dc_snl_ac_system_same_arrays, location): + sapm_dc_snl_ac_system_same_arrays, location, input_type): """ModelChain.complete_irradiance can accept a tuple of weather DataFrames.""" times = pd.date_range(start='2020-01-01 0700-0700', periods=2, freq='H') @@ -1543,8 +1627,8 @@ def test_complete_irradiance_arrays( mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) with pytest.raises(ValueError, match=r"Input DataFrames must have same index\."): - mc.complete_irradiance((weather, weather[1:])) - mc.complete_irradiance((weather, weather)) + mc.complete_irradiance(input_type((weather, weather[1:]))) + mc.complete_irradiance(input_type((weather, weather))) for mc_weather in mc.weather: assert_series_equal(mc_weather['dni'], pd.Series([2, 3], index=times, name='dni')) @@ -1553,10 +1637,11 @@ def test_complete_irradiance_arrays( assert_series_equal(mc_weather['ghi'], pd.Series([9, 5], index=times, name='ghi')) mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) - mc.complete_irradiance((weather[['ghi', 'dhi']], weather[['dhi', 'dni']])) + mc.complete_irradiance(input_type((weather[['ghi', 'dhi']], + weather[['dhi', 'dni']]))) assert 'dni' in mc.weather[0].columns assert 'ghi' in mc.weather[1].columns - mc.complete_irradiance((weather, weather[['ghi', 'dni']])) + mc.complete_irradiance(input_type((weather, weather[['ghi', 'dni']]))) assert_series_equal(mc.weather[0]['dhi'], pd.Series([4, 6], index=times, name='dhi')) assert_series_equal(mc.weather[0]['ghi'], @@ -1566,8 +1651,9 @@ def test_complete_irradiance_arrays( assert 'dhi' in mc.weather[1].columns +@pytest.mark.parametrize("input_type", [tuple, list]) def test_complete_irradiance_arrays_wrong_length( - sapm_dc_snl_ac_system_same_arrays, location): + sapm_dc_snl_ac_system_same_arrays, location, input_type): mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) times = pd.date_range(start='2020-01-01 0700-0700', periods=2, freq='H') weather = pd.DataFrame({'dni': [2, 3], @@ -1576,9 +1662,9 @@ def test_complete_irradiance_arrays_wrong_length( error_str = "Input must be same length as number " \ r"of Arrays in system\. Expected 2, got [0-9]+\." with pytest.raises(ValueError, match=error_str): - mc.complete_irradiance((weather,)) + mc.complete_irradiance(input_type((weather,))) with pytest.raises(ValueError, match=error_str): - mc.complete_irradiance((weather, weather, weather)) + mc.complete_irradiance(input_type((weather, weather, weather))) def test_unknown_attribute(sapm_dc_snl_ac_system, location): From c28313417f509be90365ed00f368b0be8d1bb869 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Thu, 17 Dec 2020 14:14:04 -0700 Subject: [PATCH 189/236] Add DatetimeIndex for test_prepare_inputs_multi_weather() --- pvlib/tests/test_modelchain.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index d0d48d9c1c..a05d6bdf26 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -468,8 +468,11 @@ def test_ModelChain_invalid_inverter_params_arrays( @pytest.mark.parametrize("input_type", [tuple, list]) def test_prepare_inputs_multi_weather( sapm_dc_snl_ac_system_Array, location, input_type): + times = pd.date_range(start='20160101 1200-0700', + end='20160101 1800-0700', freq='6H') mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - weather = pd.DataFrame({'ghi': [1], 'dhi': [1], 'dni': [1]}) + weather = pd.DataFrame({'ghi': 1, 'dhi': 1, 'dni': 1}, + index=times) mc.prepare_inputs(input_type((weather, weather))) num_arrays = sapm_dc_snl_ac_system_Array.num_arrays assert len(mc.results.total_irrad) == num_arrays From a3c374ab895eeb08763afd69b1f6b62e7e366464 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Thu, 17 Dec 2020 14:49:40 -0700 Subject: [PATCH 190/236] Add whatsnew entries for 0.9.0 List of deprecated ModelChain attributes and brief summaries of the main API enhancements. --- docs/sphinx/source/whatsnew/v0.9.0.rst | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/sphinx/source/whatsnew/v0.9.0.rst diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst new file mode 100644 index 0000000000..fa413dee57 --- /dev/null +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -0,0 +1,63 @@ +.. _whatsnew_0810: + +v0.9.0 (MONTH DAY YEAR) +----------------------- + +Breaking changes +~~~~~~~~~~~~~~~~ + + +Deprecations +~~~~~~~~~~~~ +* The following ``ModelChain`` attributes are deprecated. They have been moved + to the :py:class:`~pvlib.modelchain.ModelChainResult` class that is + accessible via ``ModelChain.results`` + * ``ModelChain.ac`` + * ``ModelChain.airmass`` + * ``ModelChain.aoi`` + * ``ModelChain.aoi_modifier`` + * ``ModelChain.cell_temperature`` + * ``ModelChain.dc`` + * ``ModelChain.diode_params`` + * ``ModelChain.effective_irradiance`` + * ``ModelChain.spectral_modifier`` + * ``ModelChain.total_irrad`` + * ``ModelChain.tracking`` + +Enhancements +~~~~~~~~~~~~ +* Added :py:class:`~pvlib.pvsystem.Array` class to represent an array of + modules independently from a :py:class:`~pvlib.pvsystem.PVSystem`. + (:pull:`1076`, :issue:`1067`) +* Support added for modeling PV systems with multiple arrays in + :py:class:`~pvlib.pvsystem.PVSystem`. Updates the ``PVSystem`` API + to operate on and return tuples where each element of the tuple corresponds + to the input or output for a specific ``Array``. (:pull:`1076`, + :issue:`1067`) +* Support for systems with multiple ``Arrays`` added to + :py:class:`~pvlib.modelchain.ModelChain`. This includes substantial API + enhancements for accepting different weather input for each ``Array`` in the + system. (:pull:`1076`, :issue:`1067`) +* Support for :py:func:`~pvlib.inverter.sandia_multi` added to + :py:class:`~pvlib.pvsystem.PVSystem` and + :py:class:`~pvlib.modelchain.ModelChain` (as ``ac_model='sandia_multi'``). + (:pull:`1076`, :issue:`1067`) + +Bug fixes +~~~~~~~~~ + +Testing +~~~~~~~ + +Documentation +~~~~~~~~~~~~~ + +Requirements +~~~~~~~~~~~~ + + +Contributors +~~~~~~~~~~~~ +* Will Holmgren (:ghuser:`wholmgren`) +* Cliff Hansen (:ghuser:`cwhanse`) +* Will Vining (:ghuser:`wfvining`) From f04634792b6bb4fdefa9a837018b99b1b00ef159 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 18 Dec 2020 09:53:58 -0700 Subject: [PATCH 191/236] edits to pvsystem.rst --- docs/sphinx/source/pvsystem.rst | 103 +++++++++++++++++--------------- 1 file changed, 56 insertions(+), 47 deletions(-) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 719af6917b..a555c22dcf 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -12,24 +12,23 @@ PVSystem The :py:class:`~pvlib.pvsystem.PVSystem` represents one inverter and the PV modules that supply DC power to the inverter. A PV system may be on fixed -mounting or single axis trackers. The :py:class`~pvlib.pvsystem.PVSystem` +mounting or single axis trackers. The :py:class:`~pvlib.pvsystem.PVSystem` is supported by the :py:class:`~pvlib.pvsystem.Array` which represents the -PV modules in the :py:class`~pvlib.pvsystem.PVSystem`. An instance of +PV modules in the :py:class:`~pvlib.pvsystem.PVSystem`. An instance of :py:class:`~pvlib.pvsystem.PVSystem` has a single inverter, but can have multiple instances of :py:class:`~pvlib.pvsystem.Array`. Arrays can have different tilt, orientation, and number or type of modules. The :py:class:`~pvlib.pvsystem.PVSystem` class methods wrap many of the -functions in the :py:mod:`~pvlib.pvsystem` module. Similarly, the +functions in the :py:mod:`~pvlib.pvsystem` module. Similarly, :py:class:`~pvlib.pvsystem.Array` wraps several functions with its class -methods. Methods that wrap functions have similar names as the wrapped function. +methods. Methods that wrap functions have similar names as the wrapped functions. This practice simplifies the API for :py:class:`~pvlib.pvsystem.PVSystem` and :py:class:`~pvlib.pvsystem.Array` methods by eliminating the need to specify arguments that are stored as attributes of these classes, such as -module and inverter properties when calling PVSystem methods. Using -:py:class:`~pvlib.pvsystem.PVSystem` is not better or worse than using the -functions it wraps -- it is simply an alternative way of organizing -your data and calculations. +module and inverter properties. Using :py:class:`~pvlib.pvsystem.PVSystem` +is not better or worse than using the functions it wraps -- it is an +alternative way of organizing your data and calculations. This guide aims to build understanding of the PVSystem class. It assumes basic familiarity with object-oriented code in Python, but most @@ -63,9 +62,8 @@ that describe a PV system's inverter is stored in system = pvsystem.PVSystem(inverter_parameters=inverter_parameters) print(system.inverter_parameters) -The parameters that describe a PV system's modules can be provided to -`PVSystem.module_parameters` (in the case of a single array) or by providing -a list of instances of the :py:class:`~pvlib.pvsystem.Array`: +In the case of a PV system with a single array, the parameters that describe the +system's modules can be provided directly to `PVSystem.module_parameters`: .. ipython:: python @@ -74,25 +72,35 @@ a list of instances of the :py:class:`~pvlib.pvsystem.Array`: inverter_parameters=inverter_parameters) print(system.module_parameters) +In the case of a PV system with several arrays, the module parameters are +provided for each array, and the arrays are provided to +:py:class:`~pvlib.pvsystem.PVSystem` as a tuple or list of instances of +:py:class:`~pvlib.pvsystem.Array`: + .. ipython:: python module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} - array = pvsystem.Array(module_parameters=module_parameters) - system = pvsystem.PVSystem(arrays=[array], + array_one = pvsystem.Array(module_parameters=module_parameters) + array_two = pvsystem.Array(module_parameters=module_parameters) + system = pvsystem.PVSystem(arrays=[array_one, array_two], inverter_parameters=inverter_parameters) print(system.module_parameters) print(system.inverter_parameters) +Note that in the case of a PV system with multiple arrays, the +`module_parameters` attribute contains a tuple with the `module_parameters` +for each array. + Extrinsic data is passed to a PVSystem instance as method arguments. For example, the :py:meth:`~pvlib.pvsystem.PVSystem.pvwatts_dc` method accepts extrinsic data irradiance and temperature. .. ipython:: python - pdc = system.pvwatts_dc(1000, 30) + pdc = system.pvwatts_dc(g_poa_effective=1000, temp_cell=30) print(pdc) -Methods attached to a PVSystem object wrap corresponding functions in +Methods attached to a PVSystem object wrap the corresponding functions in :py:mod:`~pvlib.pvsystem`. The methods simplify the argument list by using data stored in the PVSystem attributes. Compare the :py:meth:`~pvlib.pvsystem.PVSystem.pvwatts_dc` method signature to the @@ -144,11 +152,13 @@ The `surface_tilt` and `surface_azimuth` attributes are used in PVSystem (or Array) methods such as :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` or :py:meth:`~pvlib.pvsystem.Array.get_aoi`. The angle of incidence (AOI) (AOI) calculations require `surface_tilt`, `surface_azimuth` and also -the extrinsic sun position. The :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` method -uses the `surface_tilt` and `surface_azimuth` attributes from its PVSystem -object, and so requires only `solar_zenith` and `solar_azimuth` as -arguments. The :py:meth:`~pvlib.pvsystem.Array.get_aoi` operates in a similar -manner. +the extrinsic sun position. The `PVSystem` method :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` +uses the `surface_tilt` and `surface_azimuth` attributes from the +:py:class:`pvlib.pvsystem.PVSystem` instance, and so requires only `solar_zenith` +and `solar_azimuth` as arguments. The `Array` method :py:meth:`~pvlib.pvsystem.Array.get_aoi` +operates in a similar manner. These two methods differ only in scope: the +`Array` method operates only on the `Array` instance, whereas the `PVSystem` +method operates on all `Array` instances. .. ipython:: python @@ -171,11 +181,11 @@ manner. aoi = system_multiarray.get_aoi(solar_zenith=30, solar_azimuth=180) print(aoi) -Note that when the PV system includes more than one array, the output of -:py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` is a *tuple* with the order of the -elements corresponding to the order of the arrays. If the AOI is desired for -a specific array, :py:meth:`~pvlib.pvsystem.Array.get_aoi` returns the AOI for -the array represented by the method's object. +Note that when the PV system includes more than one array, the output of the +`PVSystem` method :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` is a *tuple* with +the order of the elements corresponding to the order of the arrays. If the AOI +is desired for a specific array, the `Array` method :py:meth:`~pvlib.pvsystem.Array.get_aoi` +returns the AOI for the specific array. .. ipython:: python @@ -184,32 +194,26 @@ the array represented by the method's object. `module_parameters` and `inverter_parameters` contain the data necessary for computing DC and AC power using one of the available -PVSystem methods. These attributes are typically specified using data from -the :py:func:`~pvlib.pvsystem.retrieve_sam` function: +PVSystem methods. Values for these attributes can be obtained from databases +included with pvlib python by using the :py:func:`~pvlib.pvsystem.retrieve_sam` function: .. ipython:: python + # Load the database of CEC module model parameters + modules = pvsystem.retrieve_sam('cecmod') # retrieve_sam returns a dict. the dict keys are module names, # and the values are model parameters for that module - modules = pvsystem.retrieve_sam('cecmod') module_parameters = modules['Canadian_Solar_Inc__CS5P_220M'] + # Load the database of CEC inverter model parameters inverters = pvsystem.retrieve_sam('cecinverter') inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_'] system_one_array = pvsystem.PVSystem(module_parameters=module_parameters, inverter_parameters=inverter_parameters) The module and/or inverter parameters can also be specified manually. -This is useful for specifying modules and inverters that are not -included in the supplied databases. It is also useful for specifying -systems for use with the PVWatts models, as demonstrated in -:ref:`designphilosophy`. - -The `losses_parameters` attribute contains data that may be used with -methods that calculate system losses. At present, these methods include -only :py:meth:`PVSystem.pvwatts_losses -` and -:py:func:`pvsystem.pvwatts_losses `, but -we hope to add more related functions and methods in the future. +This is useful for modules or inverters that are not +included in the supplied databases, or when using the PVWatts model, +as demonstrated in :ref:`designphilosophy`. The attributes `modules_per_string` and `strings_per_inverter` are used in the :py:meth:`~pvlib.pvsystem.PVSystem.scale_voltage_current_power` @@ -227,6 +231,12 @@ arranged into 5 strings of 7 modules each. data_scaled = system.scale_voltage_current_power(data) print(data_scaled) +The `losses_parameters` attribute contains data that may be used with +methods that calculate system losses. At present, these methods include +only :py:meth:`PVSystem.pvwatts_losses +` and +:py:func:`pvsystem.pvwatts_losses `, but +we hope to add more related functions and methods in the future. .. _multiarray: @@ -236,7 +246,7 @@ PVSystem with multiple Arrays It is possible to model a system with multiple arrays by passing a list of :py:class:`~pvlib.pvsystem.Array` to the :py:class:`~pvlib.pvsystem.PVSystem` constructor. The :py:class:`~pvlib.pvsystem.Array` class includes those -:py:class:`~pvlib.pvsystem.PVSystem` attributes that may differ from array +:py:class:`~pvlib.pvsystem.PVSystem` attributes that may vary from array to array. These attributes include `surface_tilt`, `surface_azimuth`, `module_parameters`, `temperature_model_parameters`, `modules_per_string`, `strings_per_inverter`, `albedo`, `surface_type`, `module_type`, and @@ -249,12 +259,12 @@ to array. These attributes include `surface_tilt`, `surface_azimuth`, system = pvsystem.PVSystem(arrays=[array_one, array_two]) system.num_arrays -When instantiating a :py:class:`~pvlib.pvsystem.PVSystem` with a list -of :py:class:`~pvlib.pvsystem.Array`, each parameter must be specified individually -for each array when the :py:class:`~pvlib.pvsystem.PVSystem` instances are constructed. +When instantiating a :py:class:`~pvlib.pvsystem.PVSystem` with a tuple or list +of :py:class:`~pvlib.pvsystem.Array`, each array parameter must be specified individually +when each instance of :py:class:`~pvlib.pvsystem.Array` is constructed. For example, if all arrays are at the same tilt you must specify that tilt for -every array. When using a list of :py:class:`~pvlib.pvsystem.Array` you shouldn't -also pass any attributes for Arrays to the `PVSystem` attributes; these values +every array. When using :py:class:`~pvlib.pvsystem.Array` you shouldn't +also pass any array attributes to the `PVSystem` attributes; these values are ignored. The output of `PVSystem` methods and attributes changes when the system has @@ -268,8 +278,7 @@ constructed above: system.surface_tilt system.surface_azimuth -Similarly, other `PVSystem` methods expect tuples as input and return tuples -for values that differ among arrays. +Similarly, other `PVSystem` methods expect tuples as input and return tuples: .. ipython:: python From 6e14654f87136f25d16dbb8ca5e1405dfb71f3f1 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 18 Dec 2020 10:30:15 -0700 Subject: [PATCH 192/236] fix ipython in pvsystem.rst --- docs/sphinx/source/pvsystem.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index a555c22dcf..7fb658bd21 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -82,10 +82,10 @@ provided for each array, and the arrays are provided to module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} array_one = pvsystem.Array(module_parameters=module_parameters) array_two = pvsystem.Array(module_parameters=module_parameters) - system = pvsystem.PVSystem(arrays=[array_one, array_two], - inverter_parameters=inverter_parameters) - print(system.module_parameters) - print(system.inverter_parameters) + system_two_arrays = pvsystem.PVSystem(arrays=[array_one, array_two], + inverter_parameters=inverter_parameters) + print(system_two_arrays.module_parameters) + print(system_two_arrays.inverter_parameters) Note that in the case of a PV system with multiple arrays, the `module_parameters` attribute contains a tuple with the `module_parameters` @@ -210,6 +210,7 @@ included with pvlib python by using the :py:func:`~pvlib.pvsystem.retrieve_sam` system_one_array = pvsystem.PVSystem(module_parameters=module_parameters, inverter_parameters=inverter_parameters) + The module and/or inverter parameters can also be specified manually. This is useful for modules or inverters that are not included in the supplied databases, or when using the PVWatts model, From 9e09c80840ccfbf442a0be448c021f126635e616 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 18 Dec 2020 10:43:20 -0700 Subject: [PATCH 193/236] edits to modelchain.py docstrings --- pvlib/modelchain.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 0052d17d05..c150c474a8 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1104,7 +1104,7 @@ def complete_irradiance(self, weather): Parameters ---------- - weather : DataFrame or tuple of DataFrame + weather : DataFrame, or tuple or list of DataFrame Column names must be ``'dni'``, ``'ghi'``, ``'dhi'``, ``'wind_speed'``, ``'temp_air'``. All irradiance components are required. Air temperature of 20 C and wind speed @@ -1415,7 +1415,7 @@ def prepare_inputs_from_poa(self, data): Parameters ---------- - data : DataFrame or tuple of DataFrame + data : DataFrame, or tuple or list of DataFrame Contains plane-of-array irradiance data. Required column names include ``'poa_global'``, ``'poa_direct'`` and ``'poa_diffuse'``. Columns with weather-related data are ssigned to the @@ -1567,7 +1567,7 @@ def run_model(self, weather): Parameters ---------- - weather : DataFrame, or list or tuple of DataFrame + weather : DataFrame, or tuple or list of DataFrame Irradiance column names must include ``'dni'``, ``'ghi'``, and ``'dhi'``. If optional columns ``'temp_air'`` and ``'wind_speed'`` are not provided, air temperature of 20 C and wind speed of 0 m/s @@ -1623,7 +1623,7 @@ def run_model_from_poa(self, data): Parameters ---------- - data : DataFrame, or list or tuple of DataFrame + data : DataFrame, or tuple or list of DataFrame Required column names include ``'poa_global'``, ``'poa_direct'`` and ``'poa_diffuse'``. If optional columns ``'temp_air'`` and ``'wind_speed'`` are not provided, air @@ -1679,7 +1679,7 @@ def _run_from_effective_irrad(self, data=None): Parameters ---------- - data : DataFrame, default None + data : DataFrame, or tuple of DataFrame, default None If optional column ``'cell_temperature'`` is provided, these values are used instead of `temperature_model`. If optional column `module_temperature` is provided, `temperature_model` must be From 45346337cf419ea8ed4fb648e623a119f79b0d4c Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 21 Dec 2020 09:38:17 -0700 Subject: [PATCH 194/236] minor edits to whatsnew --- docs/sphinx/source/whatsnew/v0.9.0.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index fa413dee57..1dc84484b3 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -26,10 +26,13 @@ Deprecations Enhancements ~~~~~~~~~~~~ +* In :py:class:`~pvlib.modelchain.ModelChain`, attributes which contain + output of models are now collected into ``ModelChain.results``. + (:pull:`1076`, :issue:`1067`) * Added :py:class:`~pvlib.pvsystem.Array` class to represent an array of - modules independently from a :py:class:`~pvlib.pvsystem.PVSystem`. + modules separately from a :py:class:`~pvlib.pvsystem.PVSystem`. (:pull:`1076`, :issue:`1067`) -* Support added for modeling PV systems with multiple arrays in +* Added capability for modeling a PV system with multiple arrays in :py:class:`~pvlib.pvsystem.PVSystem`. Updates the ``PVSystem`` API to operate on and return tuples where each element of the tuple corresponds to the input or output for a specific ``Array``. (:pull:`1076`, From 42dde2cbe49a3cb57edb00eb81fdc20c7cf3b66a Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 21 Dec 2020 13:53:56 -0700 Subject: [PATCH 195/236] pvsystem.rst edits --- docs/sphinx/source/pvsystem.rst | 109 ++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 7fb658bd21..6f4c07ac4c 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -62,6 +62,7 @@ that describe a PV system's inverter is stored in system = pvsystem.PVSystem(inverter_parameters=inverter_parameters) print(system.inverter_parameters) + In the case of a PV system with a single array, the parameters that describe the system's modules can be provided directly to `PVSystem.module_parameters`: @@ -72,6 +73,7 @@ system's modules can be provided directly to `PVSystem.module_parameters`: inverter_parameters=inverter_parameters) print(system.module_parameters) + In the case of a PV system with several arrays, the module parameters are provided for each array, and the arrays are provided to :py:class:`~pvlib.pvsystem.PVSystem` as a tuple or list of instances of @@ -88,10 +90,10 @@ provided for each array, and the arrays are provided to print(system_two_arrays.inverter_parameters) Note that in the case of a PV system with multiple arrays, the -`module_parameters` attribute contains a tuple with the `module_parameters` -for each array. +:py:class:`~pvlib.pvsystem.PVSystem` attribute `module_parameters` contains +a tuple with the `module_parameters` for each array. -Extrinsic data is passed to a PVSystem instance as method arguments. For example, +Extrinsic data is passed to the arguments of PVSystem methods. For example, the :py:meth:`~pvlib.pvsystem.PVSystem.pvwatts_dc` method accepts extrinsic data irradiance and temperature. @@ -141,12 +143,35 @@ Please see the :py:class:`~pvlib.pvsystem.PVSystem` and :py:class:`~pvlib.pvsystem.Array` class documentation for a comprehensive list of attributes. + +Tilt and azimuth +^^^^^^^^^^^^^^^^ + The first parameters which describe the DC part of a PV system are the tilt and azimuth of the modules. In the case of a PV system with a single array, these parameters can be specified using the `PVSystem.surface_tilt` and -`PVSystem.surface_azimuth` attributes. In the case of a PV system with -several arrays, the parameters are specified for each array using -the attributes `Array.surface_tilt` and `Array.surface_azimuth`. +`PVSystem.surface_azimuth` attributes. + +.. ipython:: python + + # single south-facing array at 20 deg tilt + system_one_array = pvsystem.PVSystem(surface_tilt=20, surface_azimuth=180) + print(system_one_array.surface_tilt, system_one_array.surface_azimuth) + + +In the case of a PV system with several arrays, the parameters are specified +for each array using the attributes `Array.surface_tilt` and `Array.surface_azimuth`. + +.. ipython:: python + + array_one = pvsystem.Array(surface_tilt=30, surface_azimuth=90) + print(array_one.surface_tilt, array_one.surface_azimuth) + array_two = pvsystem.Array(surface_tilt=30, surface_azimuth=220) + system = pvsystem.PVSystem(arrays=[array_one, array_two]) + system.num_arrays + system.surface_tilt + system.surface_azimuth + The `surface_tilt` and `surface_azimuth` attributes are used in PVSystem (or Array) methods such as :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` or @@ -155,10 +180,7 @@ The `surface_tilt` and `surface_azimuth` attributes are used in PVSystem the extrinsic sun position. The `PVSystem` method :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` uses the `surface_tilt` and `surface_azimuth` attributes from the :py:class:`pvlib.pvsystem.PVSystem` instance, and so requires only `solar_zenith` -and `solar_azimuth` as arguments. The `Array` method :py:meth:`~pvlib.pvsystem.Array.get_aoi` -operates in a similar manner. These two methods differ only in scope: the -`Array` method operates only on the `Array` instance, whereas the `PVSystem` -method operates on all `Array` instances. +and `solar_azimuth` as arguments. .. ipython:: python @@ -170,10 +192,24 @@ method operates on all `Array` instances. aoi = system_one_array.get_aoi(solar_zenith=30, solar_azimuth=180) print(aoi) + +The `Array` method :py:meth:`~pvlib.pvsystem.Array.get_aoi` +operates in a similar manner. + .. ipython:: python # two arrays each at 30 deg tilt with different facing array_one = pvsystem.Array(surface_tilt=30, surface_azimuth=90) + array_one_aoi = array_one.get_aoi(solar_zenith=30, solar_azimuth=180) + print(aoi) + + +The `PVSystem` method :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` +operates on all `Array` instances in the `PVSystem`, whereas the the +`Array` method operates only on its `Array` instance. + +.. ipython:: python + array_two = pvsystem.Array(surface_tilt=30, surface_azimuth=220) system_multiarray = pvsystem.PVSystem(arrays=[array_one, array_two]) print(system_multiarray.num_arrays) @@ -181,16 +217,23 @@ method operates on all `Array` instances. aoi = system_multiarray.get_aoi(solar_zenith=30, solar_azimuth=180) print(aoi) -Note that when the PV system includes more than one array, the output of the + +As a reminder, when the PV system includes more than one array, the output of the `PVSystem` method :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` is a *tuple* with -the order of the elements corresponding to the order of the arrays. If the AOI -is desired for a specific array, the `Array` method :py:meth:`~pvlib.pvsystem.Array.get_aoi` -returns the AOI for the specific array. +the order of the elements corresponding to the order of the arrays. + +Other `PVSystem` and `Array` methods operate in a similar manner. When a `PVSystem` +method needs input for each array, the input is provided in a tuple: .. ipython:: python - aoi = array_one.get_aoi(solar_zenith=30, solar_azimuth=180) + aoi = system.get_aoi(solar_zenith=30, solar_azimuth=180) print(aoi) + system_multiarray.get_iam(aoi) + + +Module and inverter parameters +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `module_parameters` and `inverter_parameters` contain the data necessary for computing DC and AC power using one of the available @@ -216,6 +259,10 @@ This is useful for modules or inverters that are not included in the supplied databases, or when using the PVWatts model, as demonstrated in :ref:`designphilosophy`. + +Module strings +^^^^^^^^^^^^^^ + The attributes `modules_per_string` and `strings_per_inverter` are used in the :py:meth:`~pvlib.pvsystem.PVSystem.scale_voltage_current_power` method. Some DC power models in :py:class:`~pvlib.modelchain.ModelChain` @@ -232,6 +279,10 @@ arranged into 5 strings of 7 modules each. data_scaled = system.scale_voltage_current_power(data) print(data_scaled) + +Losses +^^^^^^ + The `losses_parameters` attribute contains data that may be used with methods that calculate system losses. At present, these methods include only :py:meth:`PVSystem.pvwatts_losses @@ -239,12 +290,13 @@ only :py:meth:`PVSystem.pvwatts_losses :py:func:`pvsystem.pvwatts_losses `, but we hope to add more related functions and methods in the future. + .. _multiarray: PVSystem with multiple Arrays ----------------------------- -It is possible to model a system with multiple arrays by passing a list of +A system with multiple arrays is specified by passing a list of :py:class:`~pvlib.pvsystem.Array` to the :py:class:`~pvlib.pvsystem.PVSystem` constructor. The :py:class:`~pvlib.pvsystem.Array` class includes those :py:class:`~pvlib.pvsystem.PVSystem` attributes that may vary from array @@ -253,13 +305,6 @@ to array. These attributes include `surface_tilt`, `surface_azimuth`, `strings_per_inverter`, `albedo`, `surface_type`, `module_type`, and `racking_model`. -.. ipython:: python - - array_one = pvsystem.Array(surface_tilt=30, surface_azimuth=90) - array_two = pvsystem.Array(surface_tilt=30, surface_azimuth=220) - system = pvsystem.PVSystem(arrays=[array_one, array_two]) - system.num_arrays - When instantiating a :py:class:`~pvlib.pvsystem.PVSystem` with a tuple or list of :py:class:`~pvlib.pvsystem.Array`, each array parameter must be specified individually when each instance of :py:class:`~pvlib.pvsystem.Array` is constructed. @@ -268,24 +313,6 @@ every array. When using :py:class:`~pvlib.pvsystem.Array` you shouldn't also pass any array attributes to the `PVSystem` attributes; these values are ignored. -The output of `PVSystem` methods and attributes changes when the system has -multiple arrays. Accessing any of Array attributes on the PVSystem object returns -return a tuple with the value of the attribute for each array, in -the same order as the `PVSystem.arrays` parameter. For example, using the system -constructed above: - -.. ipython:: python - - system.surface_tilt - system.surface_azimuth - -Similarly, other `PVSystem` methods expect tuples as input and return tuples: - -.. ipython:: python - - aoi = system.get_aoi(solar_zenith=30, solar_azimuth=180) - print(aoi) - system.get_iam(aoi) .. _sat: From b23be5efbe57824b1357946ea7b8bb3ea80e1f58 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 21 Dec 2020 14:31:44 -0700 Subject: [PATCH 196/236] Allow Series input to modelchain._common_keys() Return the intersection of the indices of the series. This supports the use case shown in introtutorial.rst where module parameters are a column selected from the a data frame. --- pvlib/modelchain.py | 6 +++-- pvlib/tests/test_modelchain.py | 45 ++++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index c150c474a8..ef3cbee01c 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1795,9 +1795,11 @@ def _all_same_index(data): def _common_keys(dicts): """Return the intersection of the set of keys for each dictionary in `dicts`""" + def _keys(x): + return set(x.keys()) if isinstance(dicts, tuple): - return set.intersection(*map(set, dicts)) - return set(dicts) + return set.intersection(*map(_keys, dicts)) + return _keys(dicts) def _tuple_from_dfs(dfs, name): diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index a05d6bdf26..4e09f77f84 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1204,9 +1204,11 @@ def test_ac_model_not_a_model(pvwatts_dc_pvwatts_ac_system, location, weather): def test_infer_ac_model_invalid_params(location): + # only the keys are relevant here, using arbitrary values + module_parameters = {'pdc0': 1, 'gamma_pdc': 1} system = pvsystem.PVSystem( arrays=[pvsystem.Array( - module_parameters=pvsystem._DC_MODEL_PARAMS['pvwatts'] + module_parameters=module_parameters )], inverter_parameters={'foo': 1, 'bar': 2} ) @@ -1676,7 +1678,9 @@ def test_unknown_attribute(sapm_dc_snl_ac_system, location): mc.unknown_attribute -def test_inconsistent_array_params(location): +def test_inconsistent_array_params(location, + sapm_module_params, + cec_module_params): module_error = ".* selected for the DC model but one or more Arrays are " \ "missing one or more required parameters" temperature_error = "could not infer temperature model from " \ @@ -1687,28 +1691,28 @@ def test_inconsistent_array_params(location): different_module_system = pvsystem.PVSystem( arrays=[ pvsystem.Array( - module_parameters=pvsystem._DC_MODEL_PARAMS['sapm']), + module_parameters=sapm_module_params), pvsystem.Array( - module_parameters=pvsystem._DC_MODEL_PARAMS['cec']), + module_parameters=cec_module_params), pvsystem.Array( - module_parameters=pvsystem._DC_MODEL_PARAMS['cec'])] + module_parameters=cec_module_params)] ) with pytest.raises(ValueError, match=module_error): ModelChain(different_module_system, location, dc_model='cec') different_temp_system = pvsystem.PVSystem( arrays=[ pvsystem.Array( - module_parameters=pvsystem._DC_MODEL_PARAMS['cec'], + module_parameters=cec_module_params, temperature_model_parameters={'a': 1, 'b': 1, 'deltaT': 1}), pvsystem.Array( - module_parameters=pvsystem._DC_MODEL_PARAMS['cec'], + module_parameters=cec_module_params, temperature_model_parameters={'a': 2, 'b': 2, 'deltaT': 2}), pvsystem.Array( - module_parameters=pvsystem._DC_MODEL_PARAMS['cec'], + module_parameters=cec_module_params, temperature_model_parameters={'b': 3, 'deltaT': 3})] ) with pytest.raises(ValueError, match=temperature_error): @@ -1716,3 +1720,28 @@ def test_inconsistent_array_params(location): ac_model='sandia_multi', aoi_model='no_loss', spectral_model='no_loss', temperature_model='sapm') + + +def test_modelchain__common_keys(): + dictionary = {'a': 1, 'b': 1} + series = pd.Series(dictionary) + assert {'a', 'b'} == modelchain._common_keys( + {'a': 1, 'b': 1} + ) + assert {'a', 'b'} == modelchain._common_keys( + pd.Series({'a': 1, 'b': 1}) + ) + assert {'a', 'b'} == modelchain._common_keys( + (dictionary, series) + ) + no_a = dictionary.copy() + del no_a['a'] + assert {'b'} == modelchain._common_keys( + (dictionary, no_a) + ) + assert {'b'} == modelchain._common_keys( + (series, pd.Series(no_a)) + ) + assert {'b'} == modelchain._common_keys( + (series, no_a) + ) From 1673ca08def16c0e6a2ffc5e9acb41cd24bcc94c Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 07:45:18 -0700 Subject: [PATCH 197/236] Update whatsnew v0.9.0 label --- docs/sphinx/source/whatsnew/v0.9.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 1dc84484b3..375e561cd6 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -1,4 +1,4 @@ -.. _whatsnew_0810: +.. _whatsnew_0900: v0.9.0 (MONTH DAY YEAR) ----------------------- From a277b0a0222890140c65dc40875d2ebe54b275d8 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 07:47:10 -0700 Subject: [PATCH 198/236] Note dataclasses requirement in whatsnew/v0.9.0.rst --- docs/sphinx/source/whatsnew/v0.9.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 375e561cd6..7f7ee44c6d 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -57,7 +57,7 @@ Documentation Requirements ~~~~~~~~~~~~ - +* ``dataclasses`` is required for python 3.6 Contributors ~~~~~~~~~~~~ From 1d823cf505f238fa71e41c70dbf9241ba72b385e Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 11:25:13 -0700 Subject: [PATCH 199/236] Fix indentation in ModelChain.martin_ruiz_aoi_loss() --- pvlib/modelchain.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index ef3cbee01c..0ca748c592 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -878,8 +878,9 @@ def sapm_aoi_loss(self): return self def martin_ruiz_aoi_loss(self): - self.results.aoi_modifier = self.system.get_iam(self.results.aoi, - iam_model='martin_ruiz') + self.results.aoi_modifier = self.system.get_iam( + self.results.aoi, + iam_model='martin_ruiz') return self def no_aoi_loss(self): From e4a00aac2fdfc403e8108d19c37ec2c899a04763 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 11:47:45 -0700 Subject: [PATCH 200/236] Clean up Array.get_iam() docstring Change PVSystem.module_parameters to Array.module_parameters. Fix indentation of Raises section --- pvlib/pvsystem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 81693b83f4..faf28ab024 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1331,7 +1331,7 @@ def get_iam(self, aoi, iam_model='physical'): ``iam_model``. Parameters for the selected IAM model are expected to be in - ``PVSystem.module_parameters``. Default parameters are available for + ``Array.module_parameters``. Default parameters are available for the 'physical', 'ashrae' and 'martin_ruiz' models. Parameters @@ -1350,7 +1350,8 @@ def get_iam(self, aoi, iam_model='physical'): Raises ------ - ValueError if `iam_model` is not a valid model name. + ValueError + if `iam_model` is not a valid model name. """ model = iam_model.lower() if model in ['ashrae', 'physical', 'martin_ruiz']: From 0ee4a87dee82dd22bb482ab3060340be42194a28 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 11:48:35 -0700 Subject: [PATCH 201/236] Change PVSystem to Array in Array.get_iam() error message --- pvlib/pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index faf28ab024..c794930348 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1363,7 +1363,7 @@ def get_iam(self, aoi, iam_model='physical'): return iam.sapm(aoi, self.module_parameters) elif model == 'interp': raise ValueError(model + ' is not implemented as an IAM model' - 'option for PVSystem') + 'option for Array') else: raise ValueError(model + ' is not a valid IAM model') From dc6b0ece44ef67aed43dbe4de060cab663a7307a Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 11:58:06 -0700 Subject: [PATCH 202/236] Update links to other pvlib functions in Array.get_irradiance() Links in docstring were broken. Use full name to fix. --- pvlib/pvsystem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index c794930348..b63667e2d8 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1276,7 +1276,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, """ Get plane of array irradiance components. - Uses the :py:func:`irradiance.get_total_irradiance` function to + Uses the :py:func:`pvlib.irradiance.get_total_irradiance` function to calculate the plane of array irradiance components for a surface defined by ``self.surface_tilt`` and ``self.surface_azimuth`` with albedo ``self.albedo``. @@ -1301,7 +1301,8 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, Irradiance model. kwargs - Extra parameters passed to :func:`irradiance.get_total_irradiance`. + Extra parameters passed to + :py:func:`pvlib.irradiance.get_total_irradiance`. Returns ------- From 111ff32732592523342dd0d409478117d4cb1de6 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 12:01:21 -0700 Subject: [PATCH 203/236] Compare function objects in ModelChain._get_cell_temperature() Rather than using the name of the function we can compare directly to the ModelChain.sapm_temp() bound method to check whether the sapm temperature model is in use. --- pvlib/modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 0ca748c592..a88dd2ee06 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1476,7 +1476,7 @@ def _get_cell_temperature(self, data, # If module_temperature is in input data we can use the SAPM cell # temperature model. if (('module_temperature' in data) and - (self.temperature_model.__name__ == 'sapm_temp')): + (self.temperature_model is self.sapm_temp)): # use SAPM cell temperature model only return pvlib.temperature.sapm_cell_from_module( module_temperature=data['module_temperature'], From 5264866a1e487c2f7ba294154e297876480b7745 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 4 Jan 2021 12:47:11 -0700 Subject: [PATCH 204/236] merge master, edits to pvsystem.rst from review --- docs/sphinx/source/pvsystem.rst | 126 ++++++++++++++++---------------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 6f4c07ac4c..c68aad827a 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -16,8 +16,10 @@ mounting or single axis trackers. The :py:class:`~pvlib.pvsystem.PVSystem` is supported by the :py:class:`~pvlib.pvsystem.Array` which represents the PV modules in the :py:class:`~pvlib.pvsystem.PVSystem`. An instance of :py:class:`~pvlib.pvsystem.PVSystem` has a single inverter, but can have -multiple instances of :py:class:`~pvlib.pvsystem.Array`. Arrays can have -different tilt, orientation, and number or type of modules. +multiple instances of :py:class:`~pvlib.pvsystem.Array`. An instance of the +Array class represents a group of modules with the same orientation and +module type. Different instances of Array can have different tilt, orientation, +and number or type of modules. The :py:class:`~pvlib.pvsystem.PVSystem` class methods wrap many of the functions in the :py:mod:`~pvlib.pvsystem` module. Similarly, @@ -63,36 +65,6 @@ that describe a PV system's inverter is stored in print(system.inverter_parameters) -In the case of a PV system with a single array, the parameters that describe the -system's modules can be provided directly to `PVSystem.module_parameters`: - -.. ipython:: python - - module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} - system = pvsystem.PVSystem(module_parameters=module_parameters, - inverter_parameters=inverter_parameters) - print(system.module_parameters) - - -In the case of a PV system with several arrays, the module parameters are -provided for each array, and the arrays are provided to -:py:class:`~pvlib.pvsystem.PVSystem` as a tuple or list of instances of -:py:class:`~pvlib.pvsystem.Array`: - -.. ipython:: python - - module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} - array_one = pvsystem.Array(module_parameters=module_parameters) - array_two = pvsystem.Array(module_parameters=module_parameters) - system_two_arrays = pvsystem.PVSystem(arrays=[array_one, array_two], - inverter_parameters=inverter_parameters) - print(system_two_arrays.module_parameters) - print(system_two_arrays.inverter_parameters) - -Note that in the case of a PV system with multiple arrays, the -:py:class:`~pvlib.pvsystem.PVSystem` attribute `module_parameters` contains -a tuple with the `module_parameters` for each array. - Extrinsic data is passed to the arguments of PVSystem methods. For example, the :py:meth:`~pvlib.pvsystem.PVSystem.pvwatts_dc` method accepts extrinsic data irradiance and temperature. @@ -133,6 +105,62 @@ Multiple methods may pull data from the same attribute. For example, the as well as the incidence angle modifier methods. +.. _multiarray: + +PVSystem and Arrays +------------------- + +The PVSystem class can represent a PV system with a single array of modules, +or with multiple arrays. For a PV system with a single array, the parameters +that describe the array can be provided directly to the PVSystem instand. +For example, the parameters that describe the array's modules are can be +passed to `PVSystem.module_parameters`: + +.. ipython:: python + + module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} + system = pvsystem.PVSystem(module_parameters=module_parameters, + inverter_parameters=inverter_parameters) + print(system.module_parameters) + + +A system with multiple arrays is specified by passing a list of +:py:class:`~pvlib.pvsystem.Array` to the :py:class:`~pvlib.pvsystem.PVSystem` +constructor. For a PV system with several arrays, the module parameters are +provided for each array, and the arrays are provided to +:py:class:`~pvlib.pvsystem.PVSystem` as a tuple or list of instances of +:py:class:`~pvlib.pvsystem.Array`: + +.. ipython:: python + + module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} + array_one = pvsystem.Array(module_parameters=module_parameters) + array_two = pvsystem.Array(module_parameters=module_parameters) + system_two_arrays = pvsystem.PVSystem(arrays=[array_one, array_two], + inverter_parameters=inverter_parameters) + print(system_two_arrays.module_parameters) + print(system_two_arrays.inverter_parameters) + +Note that in the case of a PV system with multiple arrays, the +:py:class:`~pvlib.pvsystem.PVSystem` attribute `module_parameters` contains +a tuple with the `module_parameters` for each array. + +The :py:class:`~pvlib.pvsystem.Array` class includes those +:py:class:`~pvlib.pvsystem.PVSystem` attributes that may vary from array +to array. These attributes include `surface_tilt`, `surface_azimuth`, +`module_parameters`, `temperature_model_parameters`, `modules_per_string`, +`strings_per_inverter`, `albedo`, `surface_type`, `module_type`, and +`racking_model`. + +When instantiating a :py:class:`~pvlib.pvsystem.PVSystem` with a tuple or list +of :py:class:`~pvlib.pvsystem.Array`, each array parameter must be specified for +each instance of :py:class:`~pvlib.pvsystem.Array`. For example, if all arrays +are at the same tilt you must still specify the tilt value for +each array. When using :py:class:`~pvlib.pvsystem.Array` you shouldn't +also pass any array attributes to the `PVSystem` attributes; when Array instances +are provided to PVSystem, the PVSystem attributes are ignored. + + .. _pvsystemattributes: PVSystem attributes @@ -176,8 +204,8 @@ for each array using the attributes `Array.surface_tilt` and `Array.surface_azim The `surface_tilt` and `surface_azimuth` attributes are used in PVSystem (or Array) methods such as :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` or :py:meth:`~pvlib.pvsystem.Array.get_aoi`. The angle of incidence (AOI) -(AOI) calculations require `surface_tilt`, `surface_azimuth` and also -the extrinsic sun position. The `PVSystem` method :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` +calculations require `surface_tilt`, `surface_azimuth` and the extrinsic +sun position. The `PVSystem` method :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` uses the `surface_tilt` and `surface_azimuth` attributes from the :py:class:`pvlib.pvsystem.PVSystem` instance, and so requires only `solar_zenith` and `solar_azimuth` as arguments. @@ -285,33 +313,9 @@ Losses The `losses_parameters` attribute contains data that may be used with methods that calculate system losses. At present, these methods include -only :py:meth:`PVSystem.pvwatts_losses -` and -:py:func:`pvsystem.pvwatts_losses `, but -we hope to add more related functions and methods in the future. - - -.. _multiarray: - -PVSystem with multiple Arrays ------------------------------ - -A system with multiple arrays is specified by passing a list of -:py:class:`~pvlib.pvsystem.Array` to the :py:class:`~pvlib.pvsystem.PVSystem` -constructor. The :py:class:`~pvlib.pvsystem.Array` class includes those -:py:class:`~pvlib.pvsystem.PVSystem` attributes that may vary from array -to array. These attributes include `surface_tilt`, `surface_azimuth`, -`module_parameters`, `temperature_model_parameters`, `modules_per_string`, -`strings_per_inverter`, `albedo`, `surface_type`, `module_type`, and -`racking_model`. - -When instantiating a :py:class:`~pvlib.pvsystem.PVSystem` with a tuple or list -of :py:class:`~pvlib.pvsystem.Array`, each array parameter must be specified individually -when each instance of :py:class:`~pvlib.pvsystem.Array` is constructed. -For example, if all arrays are at the same tilt you must specify that tilt for -every array. When using :py:class:`~pvlib.pvsystem.Array` you shouldn't -also pass any array attributes to the `PVSystem` attributes; these values -are ignored. +only :py:meth:`PVSystem.pvwatts_losses` and +:py:func:`pvsystem.pvwatts_losses`, but we hope to add more related functions +and methods in the future. .. _sat: From 4bd32ab5e1e354b58e2701b45c714802ea79930d Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 12:41:46 -0700 Subject: [PATCH 205/236] Reword multi-array SingleAxisTracker error message remove the word "currently" --- pvlib/tests/test_tracking.py | 2 +- pvlib/tracking.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 3e21487ee9..77c0f3a06e 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -313,7 +313,7 @@ def test_SingleAxisTracker_one_array_only(): ) assert system.module == 'foo' with pytest.raises(ValueError, - match="SingleAxisTracker does not currently support " + match="SingleAxisTracker does not support " r"multiple arrays\."): tracking.SingleAxisTracker( arrays=[pvsystem.Array(module='foo'), diff --git a/pvlib/tracking.py b/pvlib/tracking.py index d86381ad2e..fa435cceeb 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -81,7 +81,7 @@ def __init__(self, axis_tilt=0, axis_azimuth=0, max_angle=90, arrays = kwargs.get('arrays', []) if len(arrays) > 1: - raise ValueError("SingleAxisTracker does not currently support " + raise ValueError("SingleAxisTracker does not support " "multiple arrays.") elif len(arrays) == 1: surface_tilt = arrays[0].surface_tilt From 1dcda05b595156ad41d61d7f47694b96edaf39dd Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 12:42:51 -0700 Subject: [PATCH 206/236] Revert "Compare function objects in ModelChain._get_cell_temperature()" This reverts commit 111ff327. Using `is` here was a mistake, and does not work. --- pvlib/modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index a88dd2ee06..0ca748c592 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1476,7 +1476,7 @@ def _get_cell_temperature(self, data, # If module_temperature is in input data we can use the SAPM cell # temperature model. if (('module_temperature' in data) and - (self.temperature_model is self.sapm_temp)): + (self.temperature_model.__name__ == 'sapm_temp')): # use SAPM cell temperature model only return pvlib.temperature.sapm_cell_from_module( module_temperature=data['module_temperature'], From eef913a1c5486f099346c9692d125e09f5604cc7 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 13:06:48 -0700 Subject: [PATCH 207/236] Update return types of PVSystem methods Some methods now return tuples. --- pvlib/pvsystem.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index b63667e2d8..a5812f5b9a 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -294,7 +294,7 @@ def _infer_cell_type(self): @_unwrap_single_value def get_aoi(self, solar_zenith, solar_azimuth): - """Get the angle of incidence on the system. + """Get the angle of incidence on the Array(s) in the system. Parameters ---------- @@ -305,7 +305,7 @@ def get_aoi(self, solar_zenith, solar_azimuth): Returns ------- - aoi : Series + aoi : Series or tuple of Series The angle of incidence """ @@ -346,7 +346,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, Returns ------- - poa_irradiance : DataFrame + poa_irradiance : DataFrame or tuple of DataFrame Column names are: ``total, beam, sky, ground``. """ dni = self._validate_per_array(dni, system_wide=True) @@ -381,7 +381,7 @@ def get_iam(self, aoi, iam_model='physical'): 'martin_ruiz' and 'sapm'. Returns ------- - iam : numeric + iam : numeric or tuple of numeric The AOI modifier. Raises @@ -563,7 +563,8 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): Returns ------- - numeric, values in degrees C. + numeric or tuple of numeric + values in degrees C. """ poa_global = self._validate_per_array(poa_global) temp_air = self._validate_per_array(temp_air, system_wide=True) @@ -608,7 +609,7 @@ def sapm_spectral_loss(self, airmass_absolute): Returns ------- - F1 : numeric + F1 : numeric or tuple of numeric The SAPM spectral loss coefficient. """ return tuple( @@ -641,7 +642,7 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, Returns ------- - effective_irradiance : numeric + effective_irradiance : numeric or tuple of numeric The SAPM effective irradiance. [W/m2] """ poa_direct = self._validate_per_array(poa_direct) @@ -675,7 +676,8 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): Returns ------- - numeric, values in degrees C. + numeric or tuple of numeric + values in degrees C. """ poa_global = self._validate_per_array(poa_global) temp_air = self._validate_per_array(temp_air, system_wide=True) @@ -714,7 +716,8 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): Returns ------- - numeric, values in degrees C. + numeric or tuple of numeric + values in degrees C. """ poa_global = self._validate_per_array(poa_global) temp_air = self._validate_per_array(temp_air, system_wide=True) @@ -747,7 +750,7 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): Returns ------- - temperature_cell : pandas Series + temperature_cell : Series or tuple of Series The modeled cell temperature [C] Notes @@ -807,7 +810,7 @@ def first_solar_spectral_loss(self, pw, airmass_absolute): Returns ------- - modifier: array-like + modifier: array-like or tuple of array-like spectral mismatch factor (unitless) which can be multiplied with broadband irradiance reaching a module's cells to estimate effective irradiance, i.e., the irradiance that is converted to @@ -896,7 +899,7 @@ def scale_voltage_current_power(self, data): Returns ------- - scaled_data: DataFrame + scaled_data: DataFrame or tuple of DataFrame A scaled copy of the input data. """ data = self._validate_per_array(data) From 82cccd2e1745adaab338fcd4fcc973f8c9f0ddbf Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 13:15:35 -0700 Subject: [PATCH 208/236] Compare function objects in ModelChain._get_cell_temperature() Rather than relying on the name of the function we can compare function objects directly to determine if the sapm_temp model has been selected by the user. --- pvlib/modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 0ca748c592..ff4aa3a04e 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1476,7 +1476,7 @@ def _get_cell_temperature(self, data, # If module_temperature is in input data we can use the SAPM cell # temperature model. if (('module_temperature' in data) and - (self.temperature_model.__name__ == 'sapm_temp')): + (self.temperature_model == self.sapm_temp)): # use SAPM cell temperature model only return pvlib.temperature.sapm_cell_from_module( module_temperature=data['module_temperature'], From bff88d17fd8aa991ce51bc3da1329c874321d881 Mon Sep 17 00:00:00 2001 From: Aziz Date: Wed, 23 Dec 2020 19:37:16 +0100 Subject: [PATCH 209/236] Add project urls to setup.py for pypi page (#1119) * Update setup.py * Update setup.py * add project_urls * Update v0.8.1.rst * Update v0.8.1.rst --- docs/sphinx/source/whatsnew/v0.8.1.rst | 3 +++ setup.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.8.1.rst b/docs/sphinx/source/whatsnew/v0.8.1.rst index 0c1b588276..df2835226f 100644 --- a/docs/sphinx/source/whatsnew/v0.8.1.rst +++ b/docs/sphinx/source/whatsnew/v0.8.1.rst @@ -49,6 +49,7 @@ Documentation (:issue:`1055`, :pull:`1075`) * Add gallery example about backtracking on sloped terrain. (:pull:`1077`) * Add toggle button for code prompts to make copying code easier (:pull:`1096`) +* Add project urls to setup.py for pypi page (:pull:`1119`) Requirements ~~~~~~~~~~~~ @@ -62,3 +63,5 @@ Contributors * Cliff Hansen (:ghuser:`cwhanse`) * Will Vining (:ghuser:`wfvining`) * Michael Jurasovic (:ghuser:`jurasofish`) +* Aziz Ben Othman (:ghuser:`AzizCode92`) + diff --git a/setup.py b/setup.py index ab718c9d9c..a876f0e995 100755 --- a/setup.py +++ b/setup.py @@ -79,6 +79,12 @@ 'python_requires': '>=3.6' } +PROJECT_URLS = { + "Bug Tracker": "https://github.com/pvlib/pvlib-python/issues", + "Documentation": "https://pvlib-python.readthedocs.io/", + "Source Code": "https://github.com/pvlib/pvlib-python", +} + # set up pvlib packages to be installed and extensions to be compiled PACKAGES = ['pvlib'] @@ -115,5 +121,6 @@ maintainer_email=MAINTAINER_EMAIL, license=LICENSE, url=URL, + project_urls=PROJECT_URLS, classifiers=CLASSIFIERS, **setuptools_kwargs) From bbf01d6347fd07c8e64bc1affd3aea4da248d4c1 Mon Sep 17 00:00:00 2001 From: Kevin Anderson <57452607+kanderso-nrel@users.noreply.github.com> Date: Mon, 4 Jan 2021 09:08:54 -0700 Subject: [PATCH 210/236] fix pvgis test (#1121) --- docs/sphinx/source/whatsnew/v0.8.1.rst | 2 ++ pvlib/tests/iotools/test_pvgis.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.8.1.rst b/docs/sphinx/source/whatsnew/v0.8.1.rst index df2835226f..e765d200f2 100644 --- a/docs/sphinx/source/whatsnew/v0.8.1.rst +++ b/docs/sphinx/source/whatsnew/v0.8.1.rst @@ -42,6 +42,8 @@ Testing * Add airspeed velocity performance testing configuration and a few benchmarks. (:issue:`419`, :pull:`1049`, :pull:`1059`) * Add Python 3.9 CI configurations. (:issue:`1102`, :pull:`1112`) +* Update ``test_pvgis.py`` to be more flexible about the PVGIS copyright notice + (:pull:`1121`) Documentation ~~~~~~~~~~~~~ diff --git a/pvlib/tests/iotools/test_pvgis.py b/pvlib/tests/iotools/test_pvgis.py index 1957639939..0dce09328c 100644 --- a/pvlib/tests/iotools/test_pvgis.py +++ b/pvlib/tests/iotools/test_pvgis.py @@ -159,7 +159,8 @@ def _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected, for meta_value in meta: if not meta_value: continue - if meta_value == 'PVGIS (c) European Communities, 2001-2020': + # don't check end year because it changes every year + if meta_value[:-4] == 'PVGIS (c) European Communities, 2001-': continue assert meta_value in csv_meta From 7114e588eeb1141f59859c9e18daae889d39c320 Mon Sep 17 00:00:00 2001 From: Kevin Anderson <57452607+kanderso-nrel@users.noreply.github.com> Date: Mon, 4 Jan 2021 09:12:56 -0700 Subject: [PATCH 211/236] make clean: also delete generated, auto_examples, savefig (#1122) --- docs/sphinx/Makefile | 3 +++ docs/sphinx/make.bat | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/docs/sphinx/Makefile b/docs/sphinx/Makefile index aedab24b80..0c96b4ed23 100644 --- a/docs/sphinx/Makefile +++ b/docs/sphinx/Makefile @@ -48,6 +48,9 @@ help: clean: rm -rf $(BUILDDIR)/* + rm -rf source/generated + rm -rf source/auto_examples + rm -rf source/savefig html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/docs/sphinx/make.bat b/docs/sphinx/make.bat index 9534b01813..d12b216553 100644 --- a/docs/sphinx/make.bat +++ b/docs/sphinx/make.bat @@ -12,6 +12,17 @@ set BUILDDIR=build if "%1" == "" goto help + +if "%1" == "clean" ( + REM override the default `make clean` behavior of sphinx-build; + REM this lets us clean out the various build files in sphinx/source/ + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + rmdir /q /s %SOURCEDIR%\generated >nul 2>&1 + rmdir /q /s %SOURCEDIR%\auto_examples >nul 2>&1 + rmdir /q /s %SOURCEDIR%\savefig >nul 2>&1 + goto end +) + %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. From 00bed5ed8b41ffe6b485f6da5cc5ff19fa967f0e Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 4 Jan 2021 12:47:11 -0700 Subject: [PATCH 212/236] merge master, edits to pvsystem.rst from review --- docs/sphinx/source/pvsystem.rst | 126 ++++++++++++++++---------------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 6f4c07ac4c..c68aad827a 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -16,8 +16,10 @@ mounting or single axis trackers. The :py:class:`~pvlib.pvsystem.PVSystem` is supported by the :py:class:`~pvlib.pvsystem.Array` which represents the PV modules in the :py:class:`~pvlib.pvsystem.PVSystem`. An instance of :py:class:`~pvlib.pvsystem.PVSystem` has a single inverter, but can have -multiple instances of :py:class:`~pvlib.pvsystem.Array`. Arrays can have -different tilt, orientation, and number or type of modules. +multiple instances of :py:class:`~pvlib.pvsystem.Array`. An instance of the +Array class represents a group of modules with the same orientation and +module type. Different instances of Array can have different tilt, orientation, +and number or type of modules. The :py:class:`~pvlib.pvsystem.PVSystem` class methods wrap many of the functions in the :py:mod:`~pvlib.pvsystem` module. Similarly, @@ -63,36 +65,6 @@ that describe a PV system's inverter is stored in print(system.inverter_parameters) -In the case of a PV system with a single array, the parameters that describe the -system's modules can be provided directly to `PVSystem.module_parameters`: - -.. ipython:: python - - module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} - system = pvsystem.PVSystem(module_parameters=module_parameters, - inverter_parameters=inverter_parameters) - print(system.module_parameters) - - -In the case of a PV system with several arrays, the module parameters are -provided for each array, and the arrays are provided to -:py:class:`~pvlib.pvsystem.PVSystem` as a tuple or list of instances of -:py:class:`~pvlib.pvsystem.Array`: - -.. ipython:: python - - module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} - array_one = pvsystem.Array(module_parameters=module_parameters) - array_two = pvsystem.Array(module_parameters=module_parameters) - system_two_arrays = pvsystem.PVSystem(arrays=[array_one, array_two], - inverter_parameters=inverter_parameters) - print(system_two_arrays.module_parameters) - print(system_two_arrays.inverter_parameters) - -Note that in the case of a PV system with multiple arrays, the -:py:class:`~pvlib.pvsystem.PVSystem` attribute `module_parameters` contains -a tuple with the `module_parameters` for each array. - Extrinsic data is passed to the arguments of PVSystem methods. For example, the :py:meth:`~pvlib.pvsystem.PVSystem.pvwatts_dc` method accepts extrinsic data irradiance and temperature. @@ -133,6 +105,62 @@ Multiple methods may pull data from the same attribute. For example, the as well as the incidence angle modifier methods. +.. _multiarray: + +PVSystem and Arrays +------------------- + +The PVSystem class can represent a PV system with a single array of modules, +or with multiple arrays. For a PV system with a single array, the parameters +that describe the array can be provided directly to the PVSystem instand. +For example, the parameters that describe the array's modules are can be +passed to `PVSystem.module_parameters`: + +.. ipython:: python + + module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} + system = pvsystem.PVSystem(module_parameters=module_parameters, + inverter_parameters=inverter_parameters) + print(system.module_parameters) + + +A system with multiple arrays is specified by passing a list of +:py:class:`~pvlib.pvsystem.Array` to the :py:class:`~pvlib.pvsystem.PVSystem` +constructor. For a PV system with several arrays, the module parameters are +provided for each array, and the arrays are provided to +:py:class:`~pvlib.pvsystem.PVSystem` as a tuple or list of instances of +:py:class:`~pvlib.pvsystem.Array`: + +.. ipython:: python + + module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} + array_one = pvsystem.Array(module_parameters=module_parameters) + array_two = pvsystem.Array(module_parameters=module_parameters) + system_two_arrays = pvsystem.PVSystem(arrays=[array_one, array_two], + inverter_parameters=inverter_parameters) + print(system_two_arrays.module_parameters) + print(system_two_arrays.inverter_parameters) + +Note that in the case of a PV system with multiple arrays, the +:py:class:`~pvlib.pvsystem.PVSystem` attribute `module_parameters` contains +a tuple with the `module_parameters` for each array. + +The :py:class:`~pvlib.pvsystem.Array` class includes those +:py:class:`~pvlib.pvsystem.PVSystem` attributes that may vary from array +to array. These attributes include `surface_tilt`, `surface_azimuth`, +`module_parameters`, `temperature_model_parameters`, `modules_per_string`, +`strings_per_inverter`, `albedo`, `surface_type`, `module_type`, and +`racking_model`. + +When instantiating a :py:class:`~pvlib.pvsystem.PVSystem` with a tuple or list +of :py:class:`~pvlib.pvsystem.Array`, each array parameter must be specified for +each instance of :py:class:`~pvlib.pvsystem.Array`. For example, if all arrays +are at the same tilt you must still specify the tilt value for +each array. When using :py:class:`~pvlib.pvsystem.Array` you shouldn't +also pass any array attributes to the `PVSystem` attributes; when Array instances +are provided to PVSystem, the PVSystem attributes are ignored. + + .. _pvsystemattributes: PVSystem attributes @@ -176,8 +204,8 @@ for each array using the attributes `Array.surface_tilt` and `Array.surface_azim The `surface_tilt` and `surface_azimuth` attributes are used in PVSystem (or Array) methods such as :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` or :py:meth:`~pvlib.pvsystem.Array.get_aoi`. The angle of incidence (AOI) -(AOI) calculations require `surface_tilt`, `surface_azimuth` and also -the extrinsic sun position. The `PVSystem` method :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` +calculations require `surface_tilt`, `surface_azimuth` and the extrinsic +sun position. The `PVSystem` method :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` uses the `surface_tilt` and `surface_azimuth` attributes from the :py:class:`pvlib.pvsystem.PVSystem` instance, and so requires only `solar_zenith` and `solar_azimuth` as arguments. @@ -285,33 +313,9 @@ Losses The `losses_parameters` attribute contains data that may be used with methods that calculate system losses. At present, these methods include -only :py:meth:`PVSystem.pvwatts_losses -` and -:py:func:`pvsystem.pvwatts_losses `, but -we hope to add more related functions and methods in the future. - - -.. _multiarray: - -PVSystem with multiple Arrays ------------------------------ - -A system with multiple arrays is specified by passing a list of -:py:class:`~pvlib.pvsystem.Array` to the :py:class:`~pvlib.pvsystem.PVSystem` -constructor. The :py:class:`~pvlib.pvsystem.Array` class includes those -:py:class:`~pvlib.pvsystem.PVSystem` attributes that may vary from array -to array. These attributes include `surface_tilt`, `surface_azimuth`, -`module_parameters`, `temperature_model_parameters`, `modules_per_string`, -`strings_per_inverter`, `albedo`, `surface_type`, `module_type`, and -`racking_model`. - -When instantiating a :py:class:`~pvlib.pvsystem.PVSystem` with a tuple or list -of :py:class:`~pvlib.pvsystem.Array`, each array parameter must be specified individually -when each instance of :py:class:`~pvlib.pvsystem.Array` is constructed. -For example, if all arrays are at the same tilt you must specify that tilt for -every array. When using :py:class:`~pvlib.pvsystem.Array` you shouldn't -also pass any array attributes to the `PVSystem` attributes; these values -are ignored. +only :py:meth:`PVSystem.pvwatts_losses` and +:py:func:`pvsystem.pvwatts_losses`, but we hope to add more related functions +and methods in the future. .. _sat: From aa2e65c7f19567b5557a353524ca5a80fcb91669 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 13:48:14 -0700 Subject: [PATCH 213/236] Fix formatting in PVSystem.get_iam() docstring --- pvlib/pvsystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index a5812f5b9a..a469f428a5 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -386,7 +386,8 @@ def get_iam(self, aoi, iam_model='physical'): Raises ------ - ValueError if `iam_model` is not a valid model name. + ValueError + if `iam_model` is not a valid model name. """ aoi = self._validate_per_array(aoi) return tuple(array.get_iam(aoi, iam_model) From af04a55ffdad53718b1464092c663cabf37c204f Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 14:05:59 -0700 Subject: [PATCH 214/236] Pass model and kwargs to Array.get_irradiance() Fixes bug where the `model` param and keyword arguments were not passed when PVSystem.get_irradiance() called Array.get_irradiance() --- pvlib/pvsystem.py | 3 ++- pvlib/tests/test_pvsystem.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index a469f428a5..b7bed48f4b 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -355,7 +355,8 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, return tuple( array.get_irradiance(solar_zenith, solar_azimuth, dni, ghi, dhi, - dni_extra, airmass) + dni_extra, airmass, model, + **kwargs) for array, dni, ghi, dhi in zip( self.arrays, dni, ghi, dhi ) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 2da0d1152a..62d1c6169f 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -12,6 +12,7 @@ from pvlib import inverter, pvsystem from pvlib import atmosphere from pvlib import iam as _iam +from pvlib import irradiance from pvlib.location import Location from pvlib import temperature from pvlib._deprecation import pvlibDeprecationWarning @@ -1496,6 +1497,32 @@ def test_PVSystem_get_irradiance(): assert_frame_equal(irradiance, expected, check_less_precise=2) +def test_PVSystem_get_irradiance(mocker): + spy_perez = mocker.spy(irradiance, 'perez') + spy_haydavies = mocker.spy(irradiance, 'haydavies') + system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) + times = pd.date_range(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) + system.get_irradiance(solar_position['apparent_zenith'], + solar_position['azimuth'], + irrads['dni'], + irrads['ghi'], + irrads['dhi']) + spy_haydavies.assert_called_once() + system.get_irradiance(solar_position['apparent_zenith'], + solar_position['azimuth'], + irrads['dni'], + irrads['ghi'], + irrads['dhi'], + model='perez') + spy_perez.assert_called_once() + + + def test_PVSystem_multi_array_get_irradiance(): array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=135) array_two = pvsystem.Array(surface_tilt=5, surface_azimuth=150) From 1d061d6ad18a07e9a37e1841155ed02f28828374 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 14:19:51 -0700 Subject: [PATCH 215/236] Note which parameters of PVSystem methods accept tuples Add note to parameter type in docstrings. --- pvlib/pvsystem.py | 56 +++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index b7bed48f4b..904252771c 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -328,11 +328,11 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, Solar zenith angle. solar_azimuth : float or Series. Solar azimuth angle. - dni : float or Series + dni : float or Series or tuple of float or Series Direct Normal Irradiance - ghi : float or Series + ghi : float or Series or tuple of float or Series Global horizontal irradiance - dhi : float or Series + dhi : float or Series or tuple of float or Series Diffuse horizontal irradiance dni_extra : None, float or Series, default None Extraterrestrial direct normal irradiance @@ -374,7 +374,7 @@ def get_iam(self, aoi, iam_model='physical'): Parameters ---------- - aoi : numeric + aoi : numeric or tuple of numeric The angle of incidence in degrees. aoi_model : string, default 'physical' @@ -403,10 +403,10 @@ def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): Parameters ---------- - effective_irradiance : numeric + effective_irradiance : numeric or tuple of numeric The irradiance (W/m2) that is converted to photocurrent. - temp_cell : float or Series + temp_cell : float or Series or tuple of float or Series The average cell temperature of cells within a module in C. **kwargs @@ -444,10 +444,10 @@ def calcparams_cec(self, effective_irradiance, temp_cell, **kwargs): Parameters ---------- - effective_irradiance : numeric + effective_irradiance : numeric or tuple of numeric The irradiance (W/m2) that is converted to photocurrent. - temp_cell : float or Series + temp_cell : float or Series or tuple of float or Series The average cell temperature of cells within a module in C. **kwargs @@ -485,10 +485,10 @@ def calcparams_pvsyst(self, effective_irradiance, temp_cell): Parameters ---------- - effective_irradiance : numeric + effective_irradiance : numeric or tuple of numeric The irradiance (W/m2) that is converted to photocurrent. - temp_cell : float or Series + temp_cell : float or Series or tuple of float or Series The average cell temperature of cells within a module in C. Returns @@ -525,10 +525,10 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): Parameters ---------- - effective_irradiance : numeric + effective_irradiance : numeric or tuple of numeric The irradiance (W/m2) that is converted to photocurrent. - temp_cell : float or Series + temp_cell : float or Series or tuple of float or Series The average cell temperature of cells within a module in C. kwargs @@ -554,13 +554,13 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): Parameters ---------- - poa_global : numeric + poa_global : numeric or tuple of numeric Total incident irradiance in W/m^2. - temp_air : numeric + temp_air : numeric or tuple of numeric Ambient dry bulb temperature in degrees C. - wind_speed : numeric + wind_speed : numeric or tuple of numeric Wind speed in m/s at a height of 10 meters. Returns @@ -630,16 +630,16 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, Parameters ---------- - poa_direct : numeric + poa_direct : numeric or tuple of numeric The direct irradiance incident upon the module. [W/m2] - poa_diffuse : numeric + poa_diffuse : numeric or tuple of numeric The diffuse irradiance incident on module. [W/m2] airmass_absolute : numeric Absolute airmass. [unitless] - aoi : numeric + aoi : numeric or tuple of numeric Angle of incidence. [degrees] Returns @@ -665,13 +665,13 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): Parameters ---------- - poa_global : numeric + poa_global : numeric or tuple of numeric Total incident irradiance in W/m^2. - temp_air : numeric + temp_air : numeric or tuple of numeric Ambient dry bulb temperature in degrees C. - wind_speed : numeric, default 1.0 + wind_speed : numeric or tuple of 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. @@ -705,13 +705,13 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): Parameters ---------- - poa_global : numeric + poa_global : numeric or tuple of numeric Total incident irradiance [W/m^2]. - temp_air : numeric + temp_air : numeric or tuple of numeric Ambient dry bulb temperature [C]. - wind_speed : numeric, default 1.0 + wind_speed : numeric or tuple of 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/s is the wind speed at module height used to determine NOCT. [m/s] @@ -741,13 +741,13 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): Parameters ---------- - poa_global : pandas Series + poa_global : pandas Series or tuple of Series Total incident irradiance [W/m^2] - temp_air : pandas Series + temp_air : pandas Series or tuple of Series Ambient dry bulb temperature [C] - wind_speed : pandas Series + wind_speed : pandas Series or tuple of Series Wind speed [m/s] Returns @@ -895,7 +895,7 @@ def scale_voltage_current_power(self, data): Parameters ---------- - data: DataFrame + data: DataFrame or tuple of DataFrame Must contain columns `'v_mp', 'v_oc', 'i_mp' ,'i_x', 'i_xx', 'i_sc', 'p_mp'`. From 036f6014a215a3a0aa466f129682700451d2349c Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 14:24:01 -0700 Subject: [PATCH 216/236] Update Array docstring to describe modules "at same orientation" --- pvlib/pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 904252771c..a20cd31b3d 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1098,7 +1098,7 @@ def __repr__(self): class Array: """ - An Array is a set of of modules at a fixed orientation. + An Array is a set of of modules at the same orientation. Specifically, an array is defined by tilt, azimuth, the module parameters, the number of parallel strings of modules From 268e7363404addf05d0ddebc291729cad6d0a54f Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 14:26:08 -0700 Subject: [PATCH 217/236] Document 0.25 as fallback value for Array.albedo --- pvlib/pvsystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index a20cd31b3d..c9ed7cfecb 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1118,7 +1118,8 @@ class Array: albedo : None or float, default None The ground albedo. If ``None``, will attempt to use ``surface_type`` to look up an albedo value in - ``irradiance.SURFACE_ALBEDOS`` + ``irradiance.SURFACE_ALBEDOS``. If a surface albedo + cannot be found then 0.25 is used. surface_type : None or string, default None The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` From 7bc040a0c0a1294ed76e416801f26721dc8e5a8c Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 14:29:12 -0700 Subject: [PATCH 218/236] Update defaults in Array docstring --- pvlib/pvsystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index c9ed7cfecb..e45729c11d 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1130,7 +1130,7 @@ class Array: May be used to look up the module_parameters dictionary via some other method. - module_type : None or string, default 'glass_polymer' + module_type : None or string, default None Describes the module's construction. Valid strings are 'glass_polymer' and 'glass_glass'. Used for cell and module temperature calculations. @@ -1147,7 +1147,7 @@ class Array: strings: int, default 1 Number of parallel strings in the array. - racking_model : None or string, default 'open_rack' + racking_model : None or string, default None Valid strings are 'open_rack', 'close_mount', and 'insulated_back'. Used to identify a parameter set for the SAPM cell temperature model. From 002ca8457cb27ad3d620924a6f01b9b758795142 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 14:40:20 -0700 Subject: [PATCH 219/236] Rename and clean up duplicate PVSystem test --- pvlib/tests/test_pvsystem.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 62d1c6169f..cf9305f5ce 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1497,7 +1497,7 @@ def test_PVSystem_get_irradiance(): assert_frame_equal(irradiance, expected, check_less_precise=2) -def test_PVSystem_get_irradiance(mocker): +def test_PVSystem_get_irradiance_model(mocker): spy_perez = mocker.spy(irradiance, 'perez') spy_haydavies = mocker.spy(irradiance, 'haydavies') system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) @@ -1505,7 +1505,7 @@ def test_PVSystem_get_irradiance(mocker): 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]}, + irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]}, index=times) system.get_irradiance(solar_position['apparent_zenith'], solar_position['azimuth'], @@ -1522,7 +1522,6 @@ def test_PVSystem_get_irradiance(mocker): spy_perez.assert_called_once() - def test_PVSystem_multi_array_get_irradiance(): array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=135) array_two = pvsystem.Array(surface_tilt=5, surface_azimuth=150) From 2e389fa5e805a7436d487e8fb54ac055dcd0952e Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 14:49:37 -0700 Subject: [PATCH 220/236] Reword PVSystem.sandia_multi docstring --- pvlib/pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index e45729c11d..be183d5c16 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -870,7 +870,7 @@ def sandia_multi(self, v_dc, p_dc): """Uses :py:func:`pvlib.inverter.sandia_multi` to calculate AC power based on ``self.inverter_parameters`` and the input voltage and power. - The parameters `v_dc` and `p_dc` must be tuples of the same length as + The parameters `v_dc` and `p_dc` must be tuples with length equal to ``self.num_arrays`` if the system has more than one array. See :py:func:`pvlib.inverter.sandia_multi` for details. From 19ed9a26c1d5c5e8079f7d27eeaa702c599421cb Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 14:57:19 -0700 Subject: [PATCH 221/236] Add notes on system-wide parameters to PVSystem method docstrings --- pvlib/pvsystem.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index be183d5c16..fb5952012e 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -344,6 +344,14 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, kwargs Extra parameters passed to :func:`irradiance.get_total_irradiance`. + Notes + ----- + Each of `dni`, `ghi`, and `dni` parameters may be passed as a tuple + to provide different irradiance for each array in the system. If not + passed as a tuple then the same value is used for input to each Array. + If passed as a tuple the length must be the same as the number of + Arrays. + Returns ------- poa_irradiance : DataFrame or tuple of DataFrame @@ -567,6 +575,14 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): ------- numeric or tuple of numeric values in degrees C. + + Notes + ----- + The `temp_air` and `wind_speed` parameters may be passed as tuples + to provide different values for each Array in the system. If not + passed as a tuple then the same value is used for input to each Array. + If passed as a tuple the length must be the same as the number of + Arrays. """ poa_global = self._validate_per_array(poa_global) temp_air = self._validate_per_array(temp_air, system_wide=True) @@ -680,6 +696,14 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): ------- numeric or tuple of numeric values in degrees C. + + Notes + ----- + The `temp_air` and `wind_speed` parameters may be passed as tuples + to provide different values for each Array in the system. If not + passed as a tuple then the same value is used for input to each Array. + If passed as a tuple the length must be the same as the number of + Arrays. """ poa_global = self._validate_per_array(poa_global) temp_air = self._validate_per_array(temp_air, system_wide=True) @@ -720,6 +744,14 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): ------- numeric or tuple of numeric values in degrees C. + + Notes + ----- + The `temp_air` and `wind_speed` parameters may be passed as tuples + to provide different values for each Array in the system. If not + passed as a tuple then the same value is used for input to each Array. + If passed as a tuple the length must be the same as the number of + Arrays. """ poa_global = self._validate_per_array(poa_global) temp_air = self._validate_per_array(temp_air, system_wide=True) @@ -763,6 +795,14 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): transposition. This method defaults to using ``self.surface_tilt``, but if you want to match the PVWatts behavior, you can override it by including a ``surface_tilt`` value in ``temperature_model_parameters``. + + Notes + ----- + The `temp_air` and `wind_speed` parameters may be passed as tuples + to provide different values for each Array in the system. If not + passed as a tuple then the same value is used for input to each Array. + If passed as a tuple the length must be the same as the number of + Arrays. """ # default to using the Array attribute, but allow user to # override with a custom surface_tilt value From 68617fa1d89187cc9f6f795020e6f47789fe8caa Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 15:12:48 -0700 Subject: [PATCH 222/236] Add ModelChainResults to classes section of api.rst --- docs/sphinx/source/api.rst | 1 + pvlib/modelchain.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 76c54c3ab1..f4d4a74628 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -23,6 +23,7 @@ corresponding procedural code. pvsystem.Array tracking.SingleAxisTracker modelchain.ModelChain + modelchain.ModelChainResult Solar Position diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index ff4aa3a04e..227e9dbdf9 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -265,6 +265,7 @@ def get_orientation(strategy, **kwargs): @dataclass class ModelChainResult: + """Contains the results of running a ModelChain.""" T = TypeVar('T') PerArray = Union[T, Tuple[T, ...]] # system-level information From 02e17c40b925d8e39a5935c71dc5c90e5b9301f5 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 15:19:18 -0700 Subject: [PATCH 223/236] Expand ModelChain docstring to describe how models are applied Make clear that the same model is applied to each array in the system. --- pvlib/modelchain.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 227e9dbdf9..9a3d1f55a4 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -289,7 +289,10 @@ class ModelChain: """ The ModelChain class to provides a standardized, high-level interface for all of the modeling steps necessary for calculating PV - power from a time series of weather inputs. + power from a time series of weather inputs. The same models are applied + to all ``pvsystem.Array`` objects, so each Array must contain the + appropriate model parameters. For example, if ``dc_model='pvwatts'``, + then each ``Array.module_parameters`` must contain ``'pdc0'``. See https://pvlib-python.readthedocs.io/en/stable/modelchain.html for examples. From 2cd4d7a426eb8d2f3359f3d5d66a89e1a1879327 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 4 Jan 2021 15:28:41 -0700 Subject: [PATCH 224/236] Revert "Add ModelChainResults to classes section of api.rst" This reverts commit 68617fa1. The current sphinx configuration can't handle this because ModelChainResult does not have an explicit __init__ method. This results in an OSError during the build when sphinx tries to make the source code pages. --- docs/sphinx/source/api.rst | 1 - pvlib/modelchain.py | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index f4d4a74628..76c54c3ab1 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -23,7 +23,6 @@ corresponding procedural code. pvsystem.Array tracking.SingleAxisTracker modelchain.ModelChain - modelchain.ModelChainResult Solar Position diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 9a3d1f55a4..ae430d2c96 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -265,7 +265,6 @@ def get_orientation(strategy, **kwargs): @dataclass class ModelChainResult: - """Contains the results of running a ModelChain.""" T = TypeVar('T') PerArray = Union[T, Tuple[T, ...]] # system-level information From 8433e24269dae98af0c7a6c00d3c3484f47e6954 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 5 Jan 2021 07:03:08 -0700 Subject: [PATCH 225/236] Simplify ModelChain.__getattr__() Removes redundant look-up in self.__dict__ --- pvlib/modelchain.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index ae430d2c96..e22fdea973 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -415,11 +415,10 @@ def __getattr__(self, key): f' ModelChain.results.{key} instead' warnings.warn(msg, pvlibDeprecationWarning) return getattr(self.results, key) - else: - try: - return self.__dict__[key] - except(KeyError): - raise AttributeError + # __getattr__ is only called if __getattribute__ fails. + # In that case we should check if key is a deprecated attribute, + # and fail with an AttributeError if it is not. + raise AttributeError def __setattr__(self, key, value): if key in ModelChain._deprecated_attrs: From 252b5458ba09b07604691a023798697e25dcc118 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 5 Jan 2021 07:09:28 -0700 Subject: [PATCH 226/236] Replace object with super() in ModelChain.__setattr__ --- pvlib/modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index e22fdea973..b16b98dda0 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -427,7 +427,7 @@ def __setattr__(self, key, value): warnings.warn(msg, pvlibDeprecationWarning) setattr(self.results, key, value) else: - object.__setattr__(self, key, value) + super().__setattr__(key, value) @classmethod def with_pvwatts(cls, system, location, From 1f138554c4b5bd07336e65b48eadc45a9f036d5d Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 5 Jan 2021 07:12:37 -0700 Subject: [PATCH 227/236] Use mc.results.ac instead of mc.ac in introtutorial.rst --- docs/sphinx/source/introtutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/introtutorial.rst b/docs/sphinx/source/introtutorial.rst index d134d1fe07..6aab5492db 100644 --- a/docs/sphinx/source/introtutorial.rst +++ b/docs/sphinx/source/introtutorial.rst @@ -165,7 +165,7 @@ by examining the parameters defined for the module. # model results (ac, dc) and intermediates (aoi, temps, etc.) # assigned as mc object attributes mc.run_model(weather) - annual_energy = mc.ac.sum() + annual_energy = mc.results.ac.sum() energies[name] = annual_energy energies = pd.Series(energies) From 775d3bf0497de070deb0ae2d1dbd0586e1849a5a Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 5 Jan 2021 07:27:27 -0700 Subject: [PATCH 228/236] Replace deprecated ModelChain attributes in forecasts.rst --- docs/sphinx/source/forecasts.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/forecasts.rst b/docs/sphinx/source/forecasts.rst index e477d00be2..a89904eccd 100644 --- a/docs/sphinx/source/forecasts.rst +++ b/docs/sphinx/source/forecasts.rst @@ -471,7 +471,7 @@ Here's the forecast plane of array irradiance... .. ipython:: python - mc.total_irrad.plot(); + mc.results.total_irrad.plot(); @savefig poa_irrad.png width=6in plt.ylabel('Plane of array irradiance ($W/m^2$)'); plt.legend(loc='best'); @@ -482,7 +482,7 @@ Here's the forecast plane of array irradiance... .. ipython:: python - mc.cell_temperature.plot(); + mc.results.cell_temperature.plot(); @savefig pv_temps.png width=6in plt.ylabel('Cell Temperature (C)'); @suppress @@ -492,7 +492,7 @@ Here's the forecast plane of array irradiance... .. ipython:: python - mc.ac.fillna(0).plot(); + mc.results.ac.fillna(0).plot(); plt.ylim(0, None); @savefig ac_power.png width=6in plt.ylabel('AC Power (W)'); From 8472e98e6132f451115129c20e922b3d7931415a Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 5 Jan 2021 11:09:51 -0700 Subject: [PATCH 229/236] fix pvsystem.rst examples --- docs/sphinx/source/pvsystem.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index c68aad827a..3ea27f538f 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -55,11 +55,12 @@ The data that represents the PV system is *intrinsic*. The data that influences the PV system is *extrinsic*. Intrinsic data is stored in object attributes. For example, the parameters -that describe a PV system's inverter is stored in -`PVSystem.inverter_parameters`. +that describe a PV system's modules and inverter are stored in +`PVSystem.module_parameters` and `PVSystem.inverter_parameters`. .. ipython:: python + module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} inverter_parameters = {'pdc0': 5000, 'eta_inv_nom': 0.96} system = pvsystem.PVSystem(inverter_parameters=inverter_parameters) print(system.inverter_parameters) @@ -119,9 +120,11 @@ passed to `PVSystem.module_parameters`: .. ipython:: python module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} + inverter_parameters = {'pdc0': 5000, 'eta_inv_nom': 0.96} system = pvsystem.PVSystem(module_parameters=module_parameters, inverter_parameters=inverter_parameters) print(system.module_parameters) + print(system.inverter_parameters) A system with multiple arrays is specified by passing a list of From 33ccfebff14ffdef1100e7ce18ccc2cbbd66fefd Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 5 Jan 2021 13:13:14 -0700 Subject: [PATCH 230/236] Fix bug in pvsystem.rst examples Forgot to pass module_parameters to PVSystem constructor. --- docs/sphinx/source/pvsystem.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 3ea27f538f..c90cc6d227 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -62,7 +62,8 @@ that describe a PV system's modules and inverter are stored in module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} inverter_parameters = {'pdc0': 5000, 'eta_inv_nom': 0.96} - system = pvsystem.PVSystem(inverter_parameters=inverter_parameters) + system = pvsystem.PVSystem(inverter_parameters=inverter_parameters, + module_parameters=module_parameters) print(system.inverter_parameters) From c90136bb4682e6c5105efe7d8ced1b8725c518ae Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 5 Jan 2021 13:55:02 -0700 Subject: [PATCH 231/236] Add modelchain.ModelChainResult to Classes section of api.rst Requires slight modification of sphinx/source/conf.py to address getting line numbers for the ModelChainResult.__init__ method which is not explicitly defined (and therefore does not have a line number). Made the T TypeVar private so it does not clutter the attribute list for ModelChainResult. Add docstring for the PerArray type. --- docs/sphinx/source/api.rst | 2 +- docs/sphinx/source/conf.py | 4 ++++ pvlib/modelchain.py | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index b70b747e2f..a88fe6a9c2 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -23,7 +23,7 @@ corresponding procedural code. pvsystem.Array tracking.SingleAxisTracker modelchain.ModelChain - + modelchain.ModelChainResult Solar Position ============== diff --git a/docs/sphinx/source/conf.py b/docs/sphinx/source/conf.py index 37e642c039..f01446e813 100644 --- a/docs/sphinx/source/conf.py +++ b/docs/sphinx/source/conf.py @@ -388,6 +388,10 @@ def get_linenos(obj): lines, start = inspect.getsourcelines(obj) except TypeError: # obj is an attribute or None return None, None + except OSError: # obj listing cannot be found + # This happens for methods that are not explicitly defined + # such as the __init__ method for a dataclass + return None, None else: return start, start + len(lines) - 1 diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index b16b98dda0..c5414fdc12 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -265,8 +265,9 @@ def get_orientation(strategy, **kwargs): @dataclass class ModelChainResult: - T = TypeVar('T') - PerArray = Union[T, Tuple[T, ...]] + _T = TypeVar('T') + PerArray = Union[_T, Tuple[_T, ...]] + """Type for fields that vary between arrays""" # system-level information solar_position: Optional[pd.DataFrame] = field(default=None) airmass: Optional[pd.DataFrame] = field(default=None) From c2a1d7cfb2fed196908ecd6238aaadd43ee79dd6 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 5 Jan 2021 15:29:12 -0700 Subject: [PATCH 232/236] Update contributors list Co-authored-by: Will Holmgren --- docs/sphinx/source/whatsnew/v0.9.0.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 7f7ee44c6d..8a244bd6c2 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -64,3 +64,7 @@ Contributors * Will Holmgren (:ghuser:`wholmgren`) * Cliff Hansen (:ghuser:`cwhanse`) * Will Vining (:ghuser:`wfvining`) +* Anton Driesse (:ghuser:`adriesse`) +* Mark Mikofski (:ghuser:`mikofski`) +* Nate Croft (:ghuser:`ncroft-b4`) +* Kevin Anderson (:ghuser:`kanderso-nrel`) From 8d74ea5a4240f9557a2dd1d747b4e966ea265c67 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Wed, 6 Jan 2021 07:17:29 -0700 Subject: [PATCH 233/236] Add ModelChain.solar_position to deprecated attrs in whatsnew --- docs/sphinx/source/whatsnew/v0.9.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 8a244bd6c2..f06b1d8140 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -20,6 +20,7 @@ Deprecations * ``ModelChain.dc`` * ``ModelChain.diode_params`` * ``ModelChain.effective_irradiance`` + * ``ModelChain.solar_position`` * ``ModelChain.spectral_modifier`` * ``ModelChain.total_irrad`` * ``ModelChain.tracking`` From 159829a32a4726d67f1f42598f84b05b60fb293a Mon Sep 17 00:00:00 2001 From: Will Vining Date: Wed, 6 Jan 2021 07:31:54 -0700 Subject: [PATCH 234/236] Fix single array aoi example output was printing the wrong aoi --- docs/sphinx/source/pvsystem.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index c90cc6d227..58d943ac79 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -233,7 +233,7 @@ operates in a similar manner. # two arrays each at 30 deg tilt with different facing array_one = pvsystem.Array(surface_tilt=30, surface_azimuth=90) array_one_aoi = array_one.get_aoi(solar_zenith=30, solar_azimuth=180) - print(aoi) + print(array_one_aoi) The `PVSystem` method :py:meth:`~pvlib.pvsystem.PVSystem.get_aoi` From 823151f83cef7efa382f1176da42cfa220540db8 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Wed, 6 Jan 2021 07:33:49 -0700 Subject: [PATCH 235/236] Add missing space in ValueError message from PVSystem.get_iam() --- pvlib/pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index fb5952012e..819d00c703 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1409,7 +1409,7 @@ def get_iam(self, aoi, iam_model='physical'): elif model == 'sapm': return iam.sapm(aoi, self.module_parameters) elif model == 'interp': - raise ValueError(model + ' is not implemented as an IAM model' + raise ValueError(model + ' is not implemented as an IAM model ' 'option for Array') else: raise ValueError(model + ' is not a valid IAM model') From f3b3751ba65dd4994e0584b4192752b7ad86b44e Mon Sep 17 00:00:00 2001 From: Will Vining Date: Wed, 6 Jan 2021 08:52:22 -0700 Subject: [PATCH 236/236] Add Results to ModelChain section of api.rst --- docs/sphinx/source/api.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index a88fe6a9c2..f043894ee6 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -584,6 +584,12 @@ Functions to assist with setting up ModelChains to run modelchain.ModelChain.prepare_inputs modelchain.ModelChain.prepare_inputs_from_poa +Results +------- + +Output from the running the ModelChain is stored in the +:py:attr:`modelchain.ModelChain.results` attribute. For more +information see :py:class:`modelchain.ModelChainResult`. Attributes ----------