diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 58d943ac79..25b9a1c9e6 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -97,7 +97,7 @@ default value may be overridden by specifying the `temp_ref` key in the .. ipython:: python - system.module_parameters['temp_ref'] = 0 + system.arrays[0].module_parameters['temp_ref'] = 0 # lower temp_ref should lead to lower DC power than calculated above pdc = system.pvwatts_dc(1000, 30) print(pdc) @@ -124,7 +124,7 @@ passed to `PVSystem.module_parameters`: 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.arrays[0].module_parameters) print(system.inverter_parameters) @@ -142,12 +142,9 @@ provided for each array, and the arrays are provided to 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([array.module_parameters for array in system_two_arrays.arrays]) 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 @@ -188,7 +185,8 @@ these parameters can be specified using the `PVSystem.surface_tilt` and # 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) + print(system_one_array.arrays[0].surface_tilt, + system_one_array.arrays[0].surface_azimuth) In the case of a PV system with several arrays, the parameters are specified @@ -201,8 +199,7 @@ for each array using the attributes `Array.surface_tilt` and `Array.surface_azim 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 + [(array.surface_tilt, array.surface_azimuth) for array in system.arrays] The `surface_tilt` and `surface_azimuth` attributes are used in PVSystem @@ -218,7 +215,8 @@ and `solar_azimuth` as arguments. # 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) + print(system_one_array.arrays[0].surface_tilt, + system_one_array.arrays[0].surface_azimuth) # call get_aoi with solar_zenith, solar_azimuth aoi = system_one_array.get_aoi(solar_zenith=30, solar_azimuth=180) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 240fad9c03..43b49a33a2 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -85,6 +85,22 @@ Deprecations * The ``eta_m`` parameter for :py:func:`~pvlib.temperature.pvsyst_cell` is replaced by parameter ``module_efficiency``. (:issue:`1188`, :pull:`1218`) +* The following attributes of :py:class:`pvlib.pvsystem.PVSystem` and + :py:class:`pvlib.tracking.SingleAxisTracker` have been deprecated in + favor of the corresponding :py:class:`pvlib.pvsystem.Array` attributes: + + * ``PVSystem.albedo`` + * ``PVSystem.module`` + * ``PVSystem.module_parameters`` + * ``PVSystem.module_type`` + * ``PVSystem.modules_per_string`` + * ``PVSystem.racking_model`` + * ``PVSystem.strings_per_inverter`` + * ``PVSystem.surface_tilt`` + * ``PVSystem.surface_azimuth`` + * ``PVSystem.temperature_model_parameters`` + + Enhancements ~~~~~~~~~~~~ * Add :func:`~pvlib.iotools.read_bsrn` for reading BSRN solar radiation data diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 8cc49153fa..eb0db817fd 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -408,29 +408,32 @@ class ModelChain: Passed to location.get_airmass. dc_model: None, str, or function, default None - If None, the model will be inferred from the contents of - system.module_parameters. Valid strings are 'sapm', - 'desoto', 'cec', 'pvsyst', 'pvwatts'. The ModelChain instance will - be passed as the first argument to a user-defined function. + If None, the model will be inferred from the parameters that + are common to all of system.arrays[i].module_parameters. + Valid strings are 'sapm', 'desoto', 'cec', 'pvsyst', 'pvwatts'. + The ModelChain instance will be passed as the first argument + to a user-defined function. ac_model: None, str, or function, default None - If None, the model will be inferred from the contents of - system.inverter_parameters and system.module_parameters. Valid - strings are 'sandia', 'adr', 'pvwatts'. The + If None, the model will be inferred from the parameters that + are common to all of system.inverter_parameters. + Valid strings are 'sandia', 'adr', 'pvwatts'. The ModelChain instance will be passed as the first argument to a user-defined function. aoi_model: None, str, or function, default None - If None, the model will be inferred from the contents of - system.module_parameters. Valid strings are 'physical', - 'ashrae', 'sapm', 'martin_ruiz', 'no_loss'. The ModelChain instance - will be passed as the first argument to a user-defined function. + If None, the model will be inferred from the parameters that + are common to all of system.arrays[i].module_parameters. + Valid strings are 'physical', 'ashrae', 'sapm', 'martin_ruiz', + 'no_loss'. The ModelChain instance will be passed as the + first argument to a user-defined function. spectral_model: None, str, or function, default None - If None, the model will be inferred from the contents of - system.module_parameters. Valid strings are 'sapm', - 'first_solar', 'no_loss'. The ModelChain instance will be passed - as the first argument to a user-defined function. + If None, the model will be inferred from the parameters that + are common to all of system.arrays[i].module_parameters. + Valid strings are 'sapm', 'first_solar', 'no_loss'. + The ModelChain instance will be passed as the first argument to + a user-defined function. temperature_model: None, str or function, default None Valid strings are: 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam'. @@ -691,9 +694,10 @@ def dc_model(self, model): model = model.lower() if model in _DC_MODEL_PARAMS.keys(): # validate module parameters + module_parameters = tuple( + array.module_parameters for array in self.system.arrays) missing_params = ( - _DC_MODEL_PARAMS[model] - - _common_keys(self.system.module_parameters)) + _DC_MODEL_PARAMS[model] - _common_keys(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 ' @@ -716,7 +720,8 @@ def dc_model(self, model): def infer_dc_model(self): """Infer DC power model from Array module parameters.""" - params = _common_keys(self.system.module_parameters) + params = _common_keys( + tuple(array.module_parameters for array in self.system.arrays)) if {'A0', 'A1', 'C7'} <= params: return self.sapm, 'sapm' elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', @@ -730,10 +735,11 @@ def infer_dc_model(self): elif {'pdc0', 'gamma_pdc'} <= params: return self.pvwatts_dc, 'pvwatts' else: - raise ValueError('could not infer DC model from ' - 'system.module_parameters. Check ' - 'system.module_parameters or explicitly ' - 'set the model with the dc_model kwarg.') + raise ValueError( + 'Could not infer DC model from the module_parameters ' + 'attributes of system.arrays. Check the module_parameters ' + 'attributes or explicitly set the model with the dc_model ' + 'keyword argument.') def sapm(self): dc = self.system.sapm(self.results.effective_irradiance, @@ -782,7 +788,7 @@ def pvwatts_dc(self): """Calculate DC power using the PVWatts model. Results are stored in ModelChain.results.dc. DC power is computed - from PVSystem.module_parameters['pdc0'] and then scaled by + from PVSystem.arrays[i].module_parameters['pdc0'] and then scaled by PVSystem.modules_per_string and PVSystem.strings_per_inverter. Returns @@ -891,7 +897,9 @@ def aoi_model(self, model): self._aoi_model = partial(model, self) def infer_aoi_model(self): - params = _common_keys(self.system.module_parameters) + module_parameters = tuple( + array.module_parameters for array in self.system.arrays) + params = _common_keys(module_parameters) if {'K', 'L', 'n'} <= params: return self.physical_aoi_loss elif {'B5', 'B4', 'B3', 'B2', 'B1', 'B0'} <= params: @@ -902,8 +910,8 @@ def infer_aoi_model(self): return self.martin_ruiz_aoi_loss else: raise ValueError('could not infer AOI model from ' - 'system.module_parameters. Check that the ' - 'module_parameters for all Arrays in ' + 'system.arrays[i].module_parameters. Check that ' + 'the 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 ' @@ -966,7 +974,9 @@ def spectral_model(self, model): def infer_spectral_model(self): """Infer spectral model from system attributes.""" - params = _common_keys(self.system.module_parameters) + module_parameters = tuple( + array.module_parameters for array in self.system.arrays) + params = _common_keys(module_parameters) if {'A4', 'A3', 'A2', 'A1', 'A0'} <= params: return self.sapm_spectral_loss elif ((('Technology' in params or @@ -976,8 +986,8 @@ def infer_spectral_model(self): return self.first_solar_spectral_loss else: raise ValueError('could not infer spectral model from ' - 'system.module_parameters. Check that the ' - 'module_parameters for all Arrays in ' + 'system.arrays[i].module_parameters. Check that ' + 'the module_parameters for all Arrays in ' 'system.arrays contain valid ' 'first_solar_spectral_coefficients, a valid ' 'Material or Technology value, or set ' @@ -1028,20 +1038,24 @@ def temperature_model(self, model): # check system.temperature_model_parameters for consistency name_from_params = self.infer_temperature_model().__name__ if self._temperature_model.__name__ != name_from_params: + common_params = _common_keys(tuple( + array.temperature_model_parameters + for array in self.system.arrays)) raise ValueError( 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. Common temperature model ' - f'parameters: ' - f'{_common_keys(self.system.temperature_model_parameters)}' + f'parameters: {common_params}' ) else: self._temperature_model = partial(model, self) def infer_temperature_model(self): """Infer temperature model from system attributes.""" - params = _common_keys(self.system.temperature_model_parameters) + temperature_model_parameters = tuple( + array.temperature_model_parameters for array in self.system.arrays) + params = _common_keys(temperature_model_parameters) # remove or statement in v0.9 if {'a', 'b', 'deltaT'} <= params or ( not params and self.system.racking_model is None @@ -1195,7 +1209,7 @@ def _eff_irrad(module_parameters, total_irrad, spect_mod, aoi_mod): self.results.spectral_modifier, self.results.aoi_modifier)) else: self.results.effective_irradiance = _eff_irrad( - self.system.module_parameters, + self.system.arrays[0].module_parameters, self.results.total_irrad, self.results.spectral_modifier, self.results.aoi_modifier @@ -1629,7 +1643,7 @@ def _prepare_temperature_single_array(self, data, poa): self.results.cell_temperature = self._get_cell_temperature( data, poa, - self.system.temperature_model_parameters + self.system.arrays[0].temperature_model_parameters ) if self.results.cell_temperature is None: self.temperature_model() diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 7623cc009f..43b856b903 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -64,6 +64,37 @@ def f(*args, **kwargs): return f +def _check_deprecated_passthrough(func): + """ + Decorator to warn or error when getting and setting the "pass-through" + PVSystem properties that have been moved to Array. Emits a warning for + PVSystems with only one Array and raises an error for PVSystems with + more than one Array. + """ + + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + pvsystem_attr = func.__name__ + class_name = self.__class__.__name__ # PVSystem or SingleAxisTracker + overrides = { # some Array attrs aren't the same as PVSystem + 'strings_per_inverter': 'strings', + } + array_attr = overrides.get(pvsystem_attr, pvsystem_attr) + alternative = f'{class_name}.arrays[i].{array_attr}' + + if len(self.arrays) > 1: + raise AttributeError( + f'{class_name}.{pvsystem_attr} not supported for multi-array ' + f'systems. Set {array_attr} for each Array in ' + f'{class_name}.arrays instead.') + + wrapped = deprecated('0.9', alternative=alternative, removal='0.10', + name=f"{class_name}.{pvsystem_attr}")(func) + return wrapped(self, *args, **kwargs) + + return wrapper + + # 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. @@ -1083,75 +1114,125 @@ def dc_ohms_from_percent(self): @property @_unwrap_single_value + @_check_deprecated_passthrough def module_parameters(self): return tuple(array.module_parameters for array in self.arrays) + @module_parameters.setter + @_check_deprecated_passthrough + def module_parameters(self, value): + for array in self.arrays: + array.module_parameters = value + @property @_unwrap_single_value + @_check_deprecated_passthrough def module(self): return tuple(array.module for array in self.arrays) + @module.setter + @_check_deprecated_passthrough + def module(self, value): + for array in self.arrays: + array.module = value + @property @_unwrap_single_value + @_check_deprecated_passthrough def module_type(self): return tuple(array.module_type for array in self.arrays) + @module_type.setter + @_check_deprecated_passthrough + def module_type(self, value): + for array in self.arrays: + array.module_type = value + @property @_unwrap_single_value + @_check_deprecated_passthrough def temperature_model_parameters(self): return tuple(array.temperature_model_parameters for array in self.arrays) @temperature_model_parameters.setter + @_check_deprecated_passthrough def temperature_model_parameters(self, value): for array in self.arrays: array.temperature_model_parameters = value @property @_unwrap_single_value + @_check_deprecated_passthrough def surface_tilt(self): return tuple(array.surface_tilt for array in self.arrays) @surface_tilt.setter + @_check_deprecated_passthrough def surface_tilt(self, value): for array in self.arrays: array.surface_tilt = value @property @_unwrap_single_value + @_check_deprecated_passthrough def surface_azimuth(self): return tuple(array.surface_azimuth for array in self.arrays) @surface_azimuth.setter + @_check_deprecated_passthrough def surface_azimuth(self, value): for array in self.arrays: array.surface_azimuth = value @property @_unwrap_single_value + @_check_deprecated_passthrough def albedo(self): return tuple(array.albedo for array in self.arrays) + @albedo.setter + @_check_deprecated_passthrough + def albedo(self, value): + for array in self.arrays: + array.albedo = value + @property @_unwrap_single_value + @_check_deprecated_passthrough def racking_model(self): return tuple(array.racking_model for array in self.arrays) @racking_model.setter + @_check_deprecated_passthrough def racking_model(self, value): for array in self.arrays: array.racking_model = value @property @_unwrap_single_value + @_check_deprecated_passthrough def modules_per_string(self): return tuple(array.modules_per_string for array in self.arrays) + @modules_per_string.setter + @_check_deprecated_passthrough + def modules_per_string(self, value): + for array in self.arrays: + array.modules_per_string = value + @property @_unwrap_single_value + @_check_deprecated_passthrough def strings_per_inverter(self): return tuple(array.strings for array in self.arrays) + @strings_per_inverter.setter + @_check_deprecated_passthrough + def strings_per_inverter(self, value): + for array in self.arrays: + array.strings = value + @property def num_arrays(self): """The number of Arrays in the system.""" diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 88b2104c99..4d2edbccae 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -658,8 +658,8 @@ def test_run_model_with_weather_pvsyst_temp(sapm_dc_snl_ac_system, location, # test with pvsyst cell temperature model weather['wind_speed'] = 5 weather['temp_air'] = 10 - sapm_dc_snl_ac_system.racking_model = 'freestanding' - sapm_dc_snl_ac_system.temperature_model_parameters = \ + sapm_dc_snl_ac_system.arrays[0].racking_model = 'freestanding' + sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters = \ temperature._temperature_model_params('pvsyst', 'freestanding') mc = ModelChain(sapm_dc_snl_ac_system, location) mc.temperature_model = 'pvsyst' @@ -677,7 +677,7 @@ def test_run_model_with_weather_faiman_temp(sapm_dc_snl_ac_system, location, # test with faiman cell temperature model weather['wind_speed'] = 5 weather['temp_air'] = 10 - sapm_dc_snl_ac_system.temperature_model_parameters = { + sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters = { 'u0': 25.0, 'u1': 6.84 } mc = ModelChain(sapm_dc_snl_ac_system, location) @@ -695,7 +695,7 @@ def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location, weather, mocker): weather['wind_speed'] = 5 weather['temp_air'] = 10 - sapm_dc_snl_ac_system.temperature_model_parameters = { + sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters = { 'noct_installed': 45 } mc = ModelChain(sapm_dc_snl_ac_system, location) @@ -731,9 +731,9 @@ def test_run_model_with_weather_noct_sam_temp(sapm_dc_snl_ac_system, location, def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker): system = SingleAxisTracker( - module_parameters=sapm_dc_snl_ac_system.module_parameters, + module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, temperature_model_parameters=( - sapm_dc_snl_ac_system.temperature_model_parameters + sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters ), inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters) mocker.spy(system, 'singleaxis') @@ -752,9 +752,9 @@ def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker): def test_run_model_tracker_list( sapm_dc_snl_ac_system, location, weather, mocker): system = SingleAxisTracker( - module_parameters=sapm_dc_snl_ac_system.module_parameters, + module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, temperature_model_parameters=( - sapm_dc_snl_ac_system.temperature_model_parameters + sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters ), inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters) mocker.spy(system, 'singleaxis') @@ -952,8 +952,8 @@ 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 + for array in sapm_dc_snl_ac_system_same_arrays.arrays: + array.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 @@ -1024,9 +1024,9 @@ def test_run_model_from_poa_arrays_solar_position_weather( def test_run_model_from_poa_tracking(sapm_dc_snl_ac_system, location, total_irrad): system = SingleAxisTracker( - module_parameters=sapm_dc_snl_ac_system.module_parameters, + module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, temperature_model_parameters=( - sapm_dc_snl_ac_system.temperature_model_parameters + sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters ), inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters) mc = ModelChain(system, location, aoi_model='no_loss', @@ -1243,11 +1243,13 @@ def test_infer_dc_model(sapm_dc_snl_ac_system, cec_dc_snl_ac_system, temp_model_params = {'sapm': {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3}, 'pvsyst': {'u_c': 29.0, 'u_v': 0}} system = dc_systems[dc_model] - system.temperature_model_parameters = temp_model_params[ - temp_model_function[dc_model]] + for array in system.arrays: + array.temperature_model_parameters = temp_model_params[ + temp_model_function[dc_model]] # remove Adjust from model parameters for desoto, singlediode if dc_model in ['desoto', 'singlediode']: - system.module_parameters.pop('Adjust') + for array in system.arrays: + array.module_parameters.pop('Adjust') m = mocker.spy(pvsystem, dc_model_function[dc_model]) mc = ModelChain(system, location, aoi_model='no_loss', spectral_model='no_loss', @@ -1257,6 +1259,15 @@ def test_infer_dc_model(sapm_dc_snl_ac_system, cec_dc_snl_ac_system, assert isinstance(mc.results.dc, (pd.Series, pd.DataFrame)) +def test_infer_dc_model_incomplete(multi_array_sapm_dc_snl_ac_system, + location): + match = 'Could not infer DC model from the module_parameters attributes ' + system = multi_array_sapm_dc_snl_ac_system['two_array_system'] + system.arrays[0].module_parameters.pop('A0') + with pytest.raises(ValueError, match=match): + ModelChain(system, location) + + @pytest.mark.parametrize('dc_model', ['cec', 'desoto', 'pvsyst']) def test_singlediode_dc_arrays(location, dc_model, cec_dc_snl_ac_arrays, @@ -1272,10 +1283,11 @@ def test_singlediode_dc_arrays(location, dc_model, 'pvsyst': temp_pvsyst} temp_model = {'cec': 'sapm', 'desoto': 'sapm', 'pvsyst': 'pvsyst'} system = systems[dc_model] - system.temperature_model_parameters = temp_model_params[dc_model] + for array in system.arrays: + array.temperature_model_parameters = temp_model_params[dc_model] if dc_model == 'desoto': - for module_parameters in system.module_parameters: - module_parameters.pop('Adjust') + for array in system.arrays: + array.module_parameters.pop('Adjust') mc = ModelChain(system, location, aoi_model='no_loss', spectral_model='no_loss', temperature_model=temp_model[dc_model]) @@ -1320,7 +1332,7 @@ def test_infer_temp_model(location, sapm_dc_snl_ac_system, def test_infer_temp_model_invalid(location, sapm_dc_snl_ac_system): - sapm_dc_snl_ac_system.temperature_model_parameters.pop('a') + sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters.pop('a') with pytest.raises(ValueError): ModelChain(sapm_dc_snl_ac_system, location, aoi_model='physical', spectral_model='no_loss') @@ -1498,7 +1510,7 @@ def test_aoi_model_user_func(sapm_dc_snl_ac_system, location, weather, mocker): ]) def test_infer_aoi_model(location, system_no_aoi, aoi_model): for k in iam._IAM_MODEL_PARAMS[aoi_model]: - system_no_aoi.module_parameters.update({k: 1.0}) + system_no_aoi.arrays[0].module_parameters.update({k: 1.0}) mc = ModelChain(system_no_aoi, location, spectral_model='no_loss') assert isinstance(mc, ModelChain) @@ -1703,19 +1715,24 @@ def test_invalid_dc_model_params(sapm_dc_snl_ac_system, cec_dc_snl_ac_system, kwargs = {'dc_model': 'sapm', 'ac_model': 'sandia', 'aoi_model': 'no_loss', 'spectral_model': 'no_loss', 'temperature_model': 'sapm', 'losses_model': 'no_loss'} - sapm_dc_snl_ac_system.module_parameters.pop('A0') # remove a parameter + for array in sapm_dc_snl_ac_system.arrays: + array.module_parameters.pop('A0') # remove a parameter with pytest.raises(ValueError): ModelChain(sapm_dc_snl_ac_system, location, **kwargs) kwargs['dc_model'] = 'singlediode' - cec_dc_snl_ac_system.module_parameters.pop('a_ref') # remove a parameter + for array in cec_dc_snl_ac_system.arrays: + array.module_parameters.pop('a_ref') # remove a parameter with pytest.raises(ValueError): ModelChain(cec_dc_snl_ac_system, location, **kwargs) kwargs['dc_model'] = 'pvwatts' kwargs['ac_model'] = 'pvwatts' - pvwatts_dc_pvwatts_ac_system.module_parameters.pop('pdc0') - with pytest.raises(ValueError): + for array in pvwatts_dc_pvwatts_ac_system.arrays: + array.module_parameters.pop('pdc0') + + match = 'one or more Arrays are missing one or more required parameters' + with pytest.raises(ValueError, match=match): ModelChain(pvwatts_dc_pvwatts_ac_system, location, **kwargs) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 19bfc40e25..d0abdefd4b 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1581,7 +1581,9 @@ def test_PVSystem_get_ac_invalid(cec_inverter_parameters): def test_PVSystem_creation(): pv_system = pvsystem.PVSystem(module='blah', inverter='blarg') # ensure that parameter attributes are dict-like. GH 294 - pv_system.module_parameters['pdc0'] = 1 + with pytest.warns(pvlibDeprecationWarning): + pv_system.module_parameters['pdc0'] = 1 + pv_system.inverter_parameters['Paco'] = 1 @@ -1589,9 +1591,6 @@ 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}) assert pv_system.arrays == (array_one, array_two) with pytest.raises(TypeError): pvsystem.PVSystem(arrays=array_one) @@ -1770,41 +1769,60 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad(): 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 + with pytest.warns(pvlibDeprecationWarning): + assert system.surface_azimuth == 180 + with pytest.warns(pvlibDeprecationWarning): + system.surface_azimuth = 90 + with pytest.warns(pvlibDeprecationWarning): + assert system.surface_azimuth == 90 -def test_PVSystem_get_albedo(two_array_system): +def test_PVSystem_get_albedo(): system = pvsystem.PVSystem( arrays=[pvsystem.Array(albedo=0.5)] ) - assert system.albedo == 0.5 - assert two_array_system.albedo == (0.25, 0.25) + with pytest.warns(pvlibDeprecationWarning): + assert system.albedo == 0.5 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 + with pytest.warns(pvlibDeprecationWarning): + 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 + with pytest.warns(pvlibDeprecationWarning): + assert system.strings_per_inverter == 5 + + +@fail_on_pvlib_version('0.10') +@pytest.mark.parametrize('attr', ['module_parameters', 'module', 'module_type', + 'temperature_model_parameters', 'albedo', + 'surface_tilt', 'surface_azimuth', + 'racking_model', 'modules_per_string', + 'strings_per_inverter']) +def test_PVSystem_multi_array_attributes(attr): + array_one = pvsystem.Array() + array_two = pvsystem.Array() + system = pvsystem.PVSystem(arrays=[array_one, array_two]) + with pytest.raises(AttributeError): + getattr(system, attr) + + with pytest.raises(AttributeError): + setattr(system, attr, 'dummy') + + system = pvsystem.PVSystem() + with pytest.warns(pvlibDeprecationWarning): + getattr(system, attr) + + with pytest.warns(pvlibDeprecationWarning): + setattr(system, attr, 'dummy') def test_PVSystem___repr__(): @@ -1959,7 +1977,8 @@ def test_PVSystem_pvwatts_dc(pvwatts_system_defaults, mocker): expected = 90 out = pvwatts_system_defaults.pvwatts_dc(irrad, temp_cell) pvsystem.pvwatts_dc.assert_called_once_with( - irrad, temp_cell, **pvwatts_system_defaults.module_parameters) + irrad, temp_cell, + **pvwatts_system_defaults.arrays[0].module_parameters) assert_allclose(expected, out, atol=10) @@ -1970,7 +1989,7 @@ def test_PVSystem_pvwatts_dc_kwargs(pvwatts_system_kwargs, mocker): expected = 90 out = pvwatts_system_kwargs.pvwatts_dc(irrad, temp_cell) pvsystem.pvwatts_dc.assert_called_once_with( - irrad, temp_cell, **pvwatts_system_kwargs.module_parameters) + irrad, temp_cell, **pvwatts_system_kwargs.arrays[0].module_parameters) assert_allclose(expected, out, atol=10) diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index b6ae20ca67..ee4a9b8d12 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -297,7 +297,7 @@ def test_SingleAxisTracker_creation(): assert system.max_angle == 45 assert system.gcr == .25 - assert system.module == 'blah' + assert system.arrays[0].module == 'blah' assert system.inverter == 'blarg' @@ -309,7 +309,7 @@ def test_SingleAxisTracker_one_array_only(): surface_azimuth=None )] ) - assert system.module == 'foo' + assert system.arrays[0].module == 'foo' with pytest.raises(ValueError, match="SingleAxisTracker does not support " r"multiple arrays\."): diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 8dd9e43461..9aa4919198 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -239,7 +239,7 @@ def get_irradiance(self, surface_tilt, surface_azimuth, dni_extra=dni_extra, airmass=airmass, model=model, - albedo=self.albedo, + albedo=self.arrays[0].albedo, **kwargs) for array, dni, ghi, dhi in zip( self.arrays, dni, ghi, dhi