From 1ee16e1e8b2b176161b313c905a58043c062719c Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 13 Mar 2021 12:36:21 -0700 Subject: [PATCH 01/15] add and apply decorator --- pvlib/pvsystem.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index a727673265..0ab6cbe3e7 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -64,6 +64,36 @@ 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. Use {alternative} 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. @@ -1019,72 +1049,86 @@ def pvwatts_ac(self, pdc): @property @_unwrap_single_value + @_check_deprecated_passthrough def module_parameters(self): return tuple(array.module_parameters for array in self.arrays) @property @_unwrap_single_value + @_check_deprecated_passthrough def module(self): return tuple(array.module for array in self.arrays) @property @_unwrap_single_value + @_check_deprecated_passthrough def module_type(self): return tuple(array.module_type for array in self.arrays) @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) @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) @property @_unwrap_single_value + @_check_deprecated_passthrough def strings_per_inverter(self): return tuple(array.strings for array in self.arrays) From 4d27febd1bfad1e72c60025299f69653b5ce299e Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 13 Mar 2021 12:36:31 -0700 Subject: [PATCH 02/15] fix everything that broke --- pvlib/modelchain.py | 57 +++++++++++++++++++------------- pvlib/tests/test_modelchain.py | 52 +++++++++++++++++------------- pvlib/tests/test_pvsystem.py | 59 +++++++++++++++++++++------------- pvlib/tests/test_tracking.py | 4 +-- pvlib/tracking.py | 2 +- 5 files changed, 102 insertions(+), 72 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 990598ae1e..8f43d3616b 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -328,26 +328,26 @@ class ModelChain: 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', + 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 + system.inverter_parameters and system.arrays[i].module_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', + 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', + 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. @@ -605,9 +605,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 ' @@ -630,7 +631,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', @@ -645,9 +647,10 @@ def infer_dc_model(self): 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.') + 'system.arrays[i].module_parameters. Check ' + 'system.arrays[i].module_parameters or ' + 'explicitly set the model with the dc_model ' + 'kwarg.') def sapm(self): dc = self.system.sapm(self.results.effective_irradiance, @@ -696,7 +699,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 @@ -805,7 +808,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: @@ -816,8 +821,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 ' @@ -880,7 +885,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 @@ -890,8 +897,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 ' @@ -940,20 +947,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 @@ -1057,7 +1068,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 @@ -1481,7 +1492,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/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index d4cb14814e..c7471d81c9 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -645,8 +645,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' @@ -663,7 +663,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) @@ -680,7 +680,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) @@ -695,9 +695,9 @@ def test_run_model_with_weather_fuentes_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') @@ -716,9 +716,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') @@ -912,8 +912,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 @@ -984,9 +984,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', @@ -1200,11 +1200,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', @@ -1229,10 +1231,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]) @@ -1274,7 +1277,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') @@ -1452,7 +1455,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) @@ -1572,18 +1575,21 @@ 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') + for array in pvwatts_dc_pvwatts_ac_system.arrays: + array.module_parameters.pop('pdc0') with pytest.raises(ValueError): ModelChain(pvwatts_dc_pvwatts_ac_system, location, **kwargs) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 5c0ef7b4a2..7b04ffc0ed 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1543,7 +1543,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 @@ -1551,9 +1553,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) @@ -1732,41 +1731,54 @@ 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): 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) + + system = pvsystem.PVSystem() + with pytest.warns(pvlibDeprecationWarning): + getattr(system, attr) def test_PVSystem___repr__(): @@ -1921,7 +1933,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) @@ -1932,7 +1945,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 248e3e3d53..75aa58dcfd 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 From f5494aa291583cff8642b9341950dbc3951dba49 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 13 Mar 2021 13:25:18 -0700 Subject: [PATCH 03/15] whatsnew --- docs/sphinx/source/whatsnew/v0.9.0.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index cb8a34756e..71fd67321d 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -65,6 +65,22 @@ Deprecations * ``ModelChain.total_irrad`` * ``ModelChain.tracking`` +* 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.module_parameters`` + * ``PVSystem.module`` + * ``PVSystem.module_type`` + * ``PVSystem.albedo`` + * ``PVSystem.temperature_model_parameters`` + * ``PVSystem.surface_tilt`` + * ``PVSystem.surface_azimuth`` + * ``PVSystem.racking_model`` + * ``PVSystem.modules_per_string`` + * ``PVSystem.strings_per_inverter`` + + Enhancements ~~~~~~~~~~~~ * Add :func:`~pvlib.iotools.read_bsrn` for reading BSRN solar radiation data From 9852c1616700a3f07fb996c3004111c8f2f12b32 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 28 Mar 2021 15:56:13 -0600 Subject: [PATCH 04/15] remove unnecessary fixture, on the correct branch this time --- 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 7b04ffc0ed..4a4f4220ec 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1739,7 +1739,7 @@ def test_PVSystem_change_surface_azimuth(): 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)] ) From 49635f4b37541684081d5fa812216cabe91d13c0 Mon Sep 17 00:00:00 2001 From: Kevin Anderson <57452607+kanderso-nrel@users.noreply.github.com> Date: Tue, 11 May 2021 20:59:54 -0600 Subject: [PATCH 05/15] Update pvlib/modelchain.py Co-authored-by: Cliff Hansen --- pvlib/modelchain.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 8f43d3616b..ae5962da50 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -328,7 +328,9 @@ class ModelChain: dc_model: None, str, or function, default None If None, the model will be inferred from the contents of - system.arrays[i].module_parameters. Valid strings are 'sapm', + 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. From 7588a813ebb8596d510a0baf24a069b248ecbd30 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 11 May 2021 21:00:48 -0600 Subject: [PATCH 06/15] reflow docstring --- pvlib/modelchain.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index ae5962da50..f817b5e4b6 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -327,12 +327,11 @@ 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 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. + 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 From 1824bd60fd7334513e0496c66e6d36c564a1677f Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 11 May 2021 21:04:51 -0600 Subject: [PATCH 07/15] more docstring updates --- pvlib/modelchain.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index f817b5e4b6..91d7d9d55e 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -334,23 +334,25 @@ class ModelChain: 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.arrays[i].module_parameters. + 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.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. + 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.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. + 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', and 'fuentes'. From 01099eda962aa648fac272d29c67287cf30ca77d Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 19 May 2021 13:38:40 -0600 Subject: [PATCH 08/15] alphabetize --- docs/sphinx/source/whatsnew/v0.9.0.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 67c07a1aa6..43b49a33a2 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -89,16 +89,16 @@ Deprecations :py:class:`pvlib.tracking.SingleAxisTracker` have been deprecated in favor of the corresponding :py:class:`pvlib.pvsystem.Array` attributes: - * ``PVSystem.module_parameters`` + * ``PVSystem.albedo`` * ``PVSystem.module`` + * ``PVSystem.module_parameters`` * ``PVSystem.module_type`` - * ``PVSystem.albedo`` - * ``PVSystem.temperature_model_parameters`` - * ``PVSystem.surface_tilt`` - * ``PVSystem.surface_azimuth`` - * ``PVSystem.racking_model`` * ``PVSystem.modules_per_string`` + * ``PVSystem.racking_model`` * ``PVSystem.strings_per_inverter`` + * ``PVSystem.surface_tilt`` + * ``PVSystem.surface_azimuth`` + * ``PVSystem.temperature_model_parameters`` Enhancements From a29acaeaa2c8645d5794c0cc1dd206a30eda9ec9 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 19 May 2021 13:57:51 -0600 Subject: [PATCH 09/15] rephrase error message --- pvlib/modelchain.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index f1b42e5e2c..eb0db817fd 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -735,11 +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.arrays[i].module_parameters. Check ' - 'system.arrays[i].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, From c775bc0fc9796f46ba366ea3e28b06ca69872e74 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 19 May 2021 14:32:30 -0600 Subject: [PATCH 10/15] pvsystem.rst edits --- docs/sphinx/source/pvsystem.rst | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 58d943ac79..782680c40c 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) From 7b963953bba50a12a680862493b37e9f35493940 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 19 May 2021 14:40:41 -0600 Subject: [PATCH 11/15] create missing PVSystem setters --- pvlib/pvsystem.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 05fa5d2aab..f2d07caa2b 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1117,18 +1117,36 @@ def dc_ohms_from_percent(self): 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 @@ -1172,6 +1190,12 @@ def surface_azimuth(self, value): 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 @@ -1190,12 +1214,24 @@ def racking_model(self, value): 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.""" From 71b07da40c08aa66a92bbba0c14ebea2de355628 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 19 May 2021 14:41:59 -0600 Subject: [PATCH 12/15] rephrase error message --- pvlib/pvsystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index f2d07caa2b..43b856b903 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -85,7 +85,8 @@ def wrapper(self, *args, **kwargs): if len(self.arrays) > 1: raise AttributeError( f'{class_name}.{pvsystem_attr} not supported for multi-array ' - f'systems. Use {alternative} instead.') + 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) From 53ea45d11e6bf15cca918eb07591cd25259464df Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 19 May 2021 14:47:28 -0600 Subject: [PATCH 13/15] another pvsystem.rst edit --- 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 782680c40c..25b9a1c9e6 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -199,7 +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 - [array.surface_tilt, array.surface_azimuth for array in system.arrays] + [(array.surface_tilt, array.surface_azimuth) for array in system.arrays] The `surface_tilt` and `surface_azimuth` attributes are used in PVSystem From 4619a1eceb3c228fb27a6c2303f1f4793f1b0502 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 19 May 2021 15:03:27 -0600 Subject: [PATCH 14/15] fix coverage drop --- pvlib/tests/test_pvsystem.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 0f092744c4..d0abdefd4b 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1814,10 +1814,16 @@ def test_PVSystem_multi_array_attributes(attr): 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__(): system = pvsystem.PVSystem( From 62a549e01b39b3c0713f35a7c5130b31dd89ea38 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 20 May 2021 15:22:13 -0600 Subject: [PATCH 15/15] add missing test --- pvlib/tests/test_modelchain.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index fc4b8162d1..4d2edbccae 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -1259,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, @@ -1721,7 +1730,9 @@ def test_invalid_dc_model_params(sapm_dc_snl_ac_system, cec_dc_snl_ac_system, kwargs['ac_model'] = 'pvwatts' for array in pvwatts_dc_pvwatts_ac_system.arrays: array.module_parameters.pop('pdc0') - with pytest.raises(ValueError): + + 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)