From 6b69027886bf14d5c0288cbaf0a1a317bd9c2a53 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 14 Apr 2021 20:30:48 -0600 Subject: [PATCH 01/21] add Array.get_cell_temperature and PVSystem.get_cell_temperature, deprecate old PVSystem tcell methods, update ModelChain._set_celltemp --- pvlib/modelchain.py | 16 +-- pvlib/pvsystem.py | 264 +++++++++++++++++++++++++------------------- 2 files changed, 161 insertions(+), 119 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 331d34d835..08a1ac7b91 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1009,26 +1009,26 @@ def _set_celltemp(self, model): temp_air = _tuple_from_dfs(self.results.weather, 'temp_air') wind_speed = _tuple_from_dfs(self.results.weather, 'wind_speed') kwargs = {} - if model == self.system.noct_sam_celltemp: + if model == 'noct_sam': kwargs['effective_irradiance'] = self.results.effective_irradiance - self.results.cell_temperature = model(poa, temp_air, wind_speed, - **kwargs) + self.results.cell_temperature = self.system.get_cell_temperature( + poa, temp_air, wind_speed, model=model, **kwargs) return self def sapm_temp(self): - return self._set_celltemp(self.system.sapm_celltemp) + return self._set_celltemp('sapm') def pvsyst_temp(self): - return self._set_celltemp(self.system.pvsyst_celltemp) + return self._set_celltemp('pvsyst') def faiman_temp(self): - return self._set_celltemp(self.system.faiman_celltemp) + return self._set_celltemp('faiman') def fuentes_temp(self): - return self._set_celltemp(self.system.fuentes_celltemp) + return self._set_celltemp('fuentes') def noct_sam_temp(self): - return self._set_celltemp(self.system.noct_sam_celltemp) + return self._set_celltemp('noct_sam') @property def dc_ohmic_model(self): diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index f6901617c3..31dcc0aa56 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -378,6 +378,61 @@ def get_iam(self, aoi, iam_model='physical'): return tuple(array.get_iam(aoi, iam_model) for array, aoi in zip(self.arrays, aoi)) + @_unwrap_single_value + def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, + **kwargs): + """ + Determine cell temperature using the method specified by ``model``. + + Parameters + ---------- + poa_global : numeric or tuple of numeric + Total incident irradiance in W/m^2. + + temp_air : numeric or tuple of numeric + Ambient dry bulb temperature in degrees C. + + wind_speed : numeric or tuple of numeric + Wind speed in m/s. + + Returns + ------- + 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) + wind_speed = self._validate_per_array(wind_speed, system_wide=True) + + # additional model-specific (and array-specific) inputs + extra_inputs = [{}] * self.num_arrays + + if 'effective_irradiance' in kwargs: + effective_irradiance = kwargs.pop('effective_irradiance') + if effective_irradiance is None: + effective_irradiance = tuple([None] * self.num_arrays) + else: + effective_irradiance = self._validate_per_array( + effective_irradiance) + for inputs, value in zip(extra_inputs, effective_irradiance): + inputs['effective_irradiance'] = value + + return tuple( + array.get_cell_temperature(poa_global, temp_air, wind_speed, + model, **extra, **kwargs) + for array, poa_global, temp_air, wind_speed, extra in zip( + self.arrays, poa_global, temp_air, wind_speed, extra_inputs + ) + ) + @_unwrap_single_value def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): """ @@ -531,7 +586,8 @@ def sapm(self, effective_irradiance, temp_cell, **kwargs): in zip(self.arrays, effective_irradiance, temp_cell) ) - @_unwrap_single_value + @deprecated('0.9', alternative='PVSystem.get_cell_temperature', + removal='0.10.0') def sapm_celltemp(self, poa_global, temp_air, wind_speed): """Uses :py:func:`temperature.sapm_cell` to calculate cell temperatures. @@ -560,20 +616,8 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): 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) - wind_speed = self._validate_per_array(wind_speed, system_wide=True) - - build_kwargs = functools.partial(_build_kwargs, ['a', 'b', 'deltaT']) - return tuple( - temperature.sapm_cell( - 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 - ) - ) + return self.get_cell_temperature(poa_global, temp_air, wind_speed, + model='sapm') @_unwrap_single_value def sapm_spectral_loss(self, airmass_absolute): @@ -635,7 +679,8 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, in zip(self.arrays, poa_direct, poa_diffuse, aoi) ) - @_unwrap_single_value + @deprecated('0.9', alternative='PVSystem.get_cell_temperature', + removal='0.10.0') def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): """Uses :py:func:`temperature.pvsyst_cell` to calculate cell temperature. @@ -666,24 +711,11 @@ def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0): 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) - wind_speed = self._validate_per_array(wind_speed, system_wide=True) + return self.get_cell_temperature(poa_global, temp_air, wind_speed, + model='pvsyst') - 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 tuple( - 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 - ) - ) - - @_unwrap_single_value + @deprecated('0.9', alternative='PVSystem.get_cell_temperature', + removal='0.10.0') def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): """ Use :py:func:`temperature.faiman` to calculate cell temperature. @@ -714,20 +746,11 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): 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) - 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, temp_air, wind_speed in zip( - self.arrays, poa_global, temp_air, wind_speed - ) - ) + return self.get_cell_temperature(poa_global, temp_air, wind_speed, + model='faiman') - @_unwrap_single_value + @deprecated('0.9', alternative='PVSystem.get_cell_temperature', + removal='0.10.0') def fuentes_celltemp(self, poa_global, temp_air, wind_speed): """ Use :py:func:`temperature.fuentes` to calculate cell temperature. @@ -763,30 +786,11 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): 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 - 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} - 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 tuple( - temperature.fuentes( - 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 - ) - ) + return self.get_cell_temperature(poa_global, temp_air, wind_speed, + model='fuentes') - @_unwrap_single_value + @deprecated('0.9', alternative='PVSystem.get_cell_temperature', + removal='0.10.0') def noct_sam_celltemp(self, poa_global, temp_air, wind_speed, effective_irradiance=None): """ @@ -820,47 +824,9 @@ def noct_sam_celltemp(self, poa_global, temp_air, wind_speed, 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 - 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) - - # need effective_irradiance to be an iterable - if effective_irradiance is None: - effective_irradiance = tuple([None] * self.num_arrays) - else: - effective_irradiance = self._validate_per_array( - effective_irradiance) - - def _build_kwargs_noct_sam(array): - temp_model_kwargs = _build_kwargs([ - 'transmittance_absorptance', - 'array_height', 'mount_standoff'], - array.temperature_model_parameters) - try: - # noct_sam required args - # bundled with kwargs for simplicity - temp_model_kwargs['noct'] = \ - array.temperature_model_parameters['noct'] - temp_model_kwargs['eta_m_ref'] = \ - array.temperature_model_parameters['eta_m_ref'] - except KeyError: - msg = ('Parameters noct and eta_m_ref are required.' - ' Found {} in temperature_model_parameters.' - .format(array.temperature_model_parameters)) - raise KeyError(msg) - return temp_model_kwargs - return tuple( - temperature.noct_sam( - poa_global, temp_air, wind_speed, - effective_irradiance=eff_irrad, - **_build_kwargs_noct_sam(array)) - for array, poa_global, temp_air, wind_speed, eff_irrad in zip( - self.arrays, poa_global, temp_air, wind_speed, - effective_irradiance - ) - ) + return self.get_cell_temperature( + poa_global, temp_air, wind_speed, model='noct_sam', + effective_irradiance=effective_irradiance) @_unwrap_single_value def first_solar_spectral_loss(self, pw, airmass_absolute): @@ -1314,7 +1280,6 @@ def _infer_temperature_model_params(self): 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 @@ -1473,6 +1438,83 @@ def get_iam(self, aoi, iam_model='physical'): else: raise ValueError(model + ' is not a valid IAM model') + def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, + **kwargs): + """ + Determine cell temperature using the method specified by ``model``. + + Parameters + ---------- + poa_global : numeric + Total incident irradiance [W/m^2] + + temp_air : numeric + Ambient dry bulb temperature [C] + + wind_speed : numeric + Wind speed [m/s] + + model : str + Supported models include ``'sapm'``, ``'pvsyst'``, + ``'faiman'``, ``'fuentes'``, and ``'noct_sam'`` + + **kwargs + Extra arguments passed to the model function. + + Returns + ------- + numeric + values in degrees C. + + Notes + ----- + Some temperature models have requirements for the input types; + see the documentation of the underlying model function for details. + """ + if model == 'sapm': + func = temperature.sapm_cell + params = _build_kwargs(['a', 'b', 'deltaT'], + self.temperature_model_parameters) + elif model == 'pvsyst': + func = temperature.pvsyst_cell + params = { + **_build_kwargs(['eta_m', 'alpha_absorption'], + self.module_parameters), + **_build_kwargs(['u_c', 'u_v'], + self.temperature_model_parameters) + } + elif model == 'faiman': + func = temperature.faiman + params = _build_kwargs(['u0', 'u1'], + self.temperature_model_parameters) + elif model == 'fuentes': + func = temperature.fuentes + # default to using the Array attribute, but allow user to override + # with a custom surface_tilt value in temperature_model_parameters + params = _build_kwargs([ + 'noct_installed', 'module_height', 'wind_height', 'emissivity', + 'absorption', 'surface_tilt', 'module_width', 'module_length'], + self.temperature_model_parameters) + if 'surface_tilt' not in params: + params['surface_tilt'] = self.surface_tilt + elif model == 'noct_sam': + func = temperature.noct_sam + params = _build_kwargs(['transmittance_absorptance', 'eta_m_ref', + 'noct', 'array_height', 'mount_standoff'], + self.temperature_model_parameters) + if not {'noct', 'eta_m_ref'}.issubset(params): + msg = ('Parameters noct and eta_m_ref are required.' + ' Found {} in temperature_model_parameters.' + .format(self.temperature_model_parameters)) + raise KeyError(msg) + else: + raise ValueError(f'{model} is not a valid cell temperature model') + + # allow kwargs to override + params.update(kwargs) + temperature_cell = func(poa_global, temp_air, wind_speed, **params) + return temperature_cell + def dc_ohms_from_percent(self): """ Calculates the equivalent resistance of the wires using From 6b7dcff7223faf49c0c4378b3ac47bcb79f7ed7e Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 14 Apr 2021 20:30:56 -0600 Subject: [PATCH 02/21] fix tests --- pvlib/tests/test_modelchain.py | 17 +-- pvlib/tests/test_pvsystem.py | 188 ++++++++++++++++----------------- 2 files changed, 103 insertions(+), 102 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 3652fc097a..8ab22d28ba 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -642,13 +642,14 @@ def test_run_model_with_weather_sapm_temp(sapm_dc_snl_ac_system, location, weather['temp_air'] = 10 mc = ModelChain(sapm_dc_snl_ac_system, location) mc.temperature_model = 'sapm' - m_sapm = mocker.spy(sapm_dc_snl_ac_system, 'sapm_celltemp') + m_sapm = mocker.spy(sapm_dc_snl_ac_system, 'get_cell_temperature') mc.run_model(weather) assert m_sapm.call_count == 1 # assert_called_once_with cannot be used with series, so need to use # assert_series_equal on call_args assert_series_equal(m_sapm.call_args[0][1], weather['temp_air']) # temp assert_series_equal(m_sapm.call_args[0][2], weather['wind_speed']) # wind + assert m_sapm.call_args[1]['model'] == 'sapm' assert not mc.results.ac.empty @@ -662,11 +663,12 @@ def test_run_model_with_weather_pvsyst_temp(sapm_dc_snl_ac_system, location, temperature._temperature_model_params('pvsyst', 'freestanding') mc = ModelChain(sapm_dc_snl_ac_system, location) mc.temperature_model = 'pvsyst' - m_pvsyst = mocker.spy(sapm_dc_snl_ac_system, 'pvsyst_celltemp') + m_pvsyst = mocker.spy(sapm_dc_snl_ac_system, 'get_cell_temperature') mc.run_model(weather) assert m_pvsyst.call_count == 1 assert_series_equal(m_pvsyst.call_args[0][1], weather['temp_air']) assert_series_equal(m_pvsyst.call_args[0][2], weather['wind_speed']) + assert m_pvsyst.call_args[1]['model'] == 'pvsyst' assert not mc.results.ac.empty @@ -680,11 +682,12 @@ def test_run_model_with_weather_faiman_temp(sapm_dc_snl_ac_system, location, } mc = ModelChain(sapm_dc_snl_ac_system, location) mc.temperature_model = 'faiman' - m_faiman = mocker.spy(sapm_dc_snl_ac_system, 'faiman_celltemp') + m_faiman = mocker.spy(sapm_dc_snl_ac_system, 'get_cell_temperature') mc.run_model(weather) 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 m_faiman.call_args[1]['model'] == 'faiman' assert not mc.results.ac.empty @@ -697,11 +700,12 @@ def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location, } mc = ModelChain(sapm_dc_snl_ac_system, location) mc.temperature_model = 'fuentes' - m_fuentes = mocker.spy(sapm_dc_snl_ac_system, 'fuentes_celltemp') + m_fuentes = mocker.spy(sapm_dc_snl_ac_system, 'get_cell_temperature') mc.run_model(weather) 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 m_fuentes.call_args[1]['model'] == 'fuentes' assert not mc.results.ac.empty @@ -714,14 +718,15 @@ def test_run_model_with_weather_noct_sam_temp(sapm_dc_snl_ac_system, location, } mc = ModelChain(sapm_dc_snl_ac_system, location) mc.temperature_model = 'noct_sam' - m_noct_sam = mocker.spy(sapm_dc_snl_ac_system, 'noct_sam_celltemp') + m_noct_sam = mocker.spy(sapm_dc_snl_ac_system, 'get_cell_temperature') mc.run_model(weather) assert m_noct_sam.call_count == 1 assert_series_equal(m_noct_sam.call_args[0][1], weather['temp_air']) assert_series_equal(m_noct_sam.call_args[0][2], weather['wind_speed']) # check that effective_irradiance was used assert m_noct_sam.call_args[1] == { - 'effective_irradiance': mc.results.effective_irradiance} + 'effective_irradiance': mc.results.effective_irradiance, + 'model': 'noct_sam'} def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker): diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 2d20660b98..a7b37ad9d6 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -429,7 +429,7 @@ def test_PVSystem_sapm_celltemp(mocker): temps = 25 irrads = 1000 winds = 1 - out = system.sapm_celltemp(irrads, temps, winds) + out = system.get_cell_temperature(irrads, temps, winds, model='sapm') temperature.sapm_cell.assert_called_once_with(irrads, temps, winds, a, b, deltaT) assert_allclose(out, 57, atol=1) @@ -443,7 +443,7 @@ def test_PVSystem_sapm_celltemp_kwargs(mocker): temps = 25 irrads = 1000 winds = 1 - out = system.sapm_celltemp(irrads, temps, winds) + out = system.get_cell_temperature(irrads, temps, winds, model='sapm') temperature.sapm_cell.assert_called_once_with(irrads, temps, winds, temp_model_params['a'], temp_model_params['b'], @@ -460,8 +460,8 @@ def test_PVSystem_multi_array_sapm_celltemp_different_arrays(): 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 + temp_one, temp_two = system.get_cell_temperature( + (1000, 1000), 25, 1, model='sapm' ) assert temp_one != temp_two @@ -479,7 +479,8 @@ def test_PVSystem_pvsyst_celltemp(mocker): irrad = 800 temp = 45 wind = 0.5 - out = system.pvsyst_celltemp(irrad, temp, wind_speed=wind) + out = system.get_cell_temperature(irrad, temp, wind_speed=wind, + model='pvsyst') temperature.pvsyst_cell.assert_called_once_with( irrad, temp, wind, temp_model_params['u_c'], temp_model_params['u_v'], eta_m, alpha_absorption) @@ -494,7 +495,7 @@ def test_PVSystem_faiman_celltemp(mocker): temps = 25 irrads = 1000 winds = 1 - out = system.faiman_celltemp(irrads, temps, winds) + out = system.get_cell_temperature(irrads, temps, winds, model='faiman') temperature.faiman.assert_called_once_with(irrads, temps, winds, u0, u1) assert_allclose(out, 56.4, atol=1) @@ -506,20 +507,21 @@ def test_PVSystem_noct_celltemp(mocker): temp_model_params = {'noct': noct, 'eta_m_ref': eta_m_ref} system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) mocker.spy(temperature, 'noct_sam') - out = system.noct_sam_celltemp(poa_global, temp_air, wind_speed) + out = system.get_cell_temperature(poa_global, temp_air, wind_speed, + model='noct_sam') temperature.noct_sam.assert_called_once_with( - poa_global, temp_air, wind_speed, effective_irradiance=None, noct=noct, - eta_m_ref=eta_m_ref) + poa_global, temp_air, wind_speed, noct=noct, eta_m_ref=eta_m_ref) assert_allclose(out, expected) - # dufferent types - out = system.noct_sam_celltemp(np.array(poa_global), np.array(temp_air), - np.array(wind_speed)) + # different types + out = system.get_cell_temperature(np.array(poa_global), np.array(temp_air), + np.array(wind_speed), model='noct_sam') assert_allclose(out, expected) dr = pd.date_range(start='2020-01-01 12:00:00', end='2020-01-01 13:00:00', freq='1H') - out = system.noct_sam_celltemp(pd.Series(index=dr, data=poa_global), - pd.Series(index=dr, data=temp_air), - pd.Series(index=dr, data=wind_speed)) + out = system.get_cell_temperature(pd.Series(index=dr, data=poa_global), + pd.Series(index=dr, data=temp_air), + pd.Series(index=dr, data=wind_speed), + model='noct_sam') assert_series_equal(out, pd.Series(index=dr, data=expected)) # now use optional arguments temp_model_params.update({'transmittance_absorptance': 0.8, @@ -527,8 +529,9 @@ def test_PVSystem_noct_celltemp(mocker): 'mount_standoff': 2.0}) expected = 60.477703576 system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) - out = system.noct_sam_celltemp(poa_global, temp_air, wind_speed, - effective_irradiance=1100.) + out = system.get_cell_temperature(poa_global, temp_air, wind_speed, + effective_irradiance=1100., + model='noct_sam') assert_allclose(out, expected) @@ -537,147 +540,120 @@ def test_PVSystem_noct_celltemp_error(): temp_model_params = {'eta_m_ref': eta_m_ref} system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) with pytest.raises(KeyError): - system.noct_sam_celltemp(poa_global, temp_air, wind_speed) + system.get_cell_temperature(poa_global, temp_air, wind_speed, + model='noct_sam') -@pytest.mark.parametrize("celltemp", - [pvsystem.PVSystem.faiman_celltemp, - pvsystem.PVSystem.pvsyst_celltemp, - pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp, - pvsystem.PVSystem.noct_sam_celltemp]) -def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system): +@pytest.mark.parametrize("model", + ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) +def test_PVSystem_multi_array_celltemp_functions(model, two_array_system): 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) + temp_one, temp_two = two_array_system.get_cell_temperature( + (irrad_one, irrad_two), temp_air, wind_speed, model=model) 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, - pvsystem.PVSystem.noct_sam_celltemp]) -def test_PVSystem_multi_array_celltemp_multi_temp(celltemp, two_array_system): +@pytest.mark.parametrize("model", + ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) +def test_PVSystem_multi_array_celltemp_multi_temp(model, 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, + temp_one, temp_two = two_array_system.get_cell_temperature( (irrad, irrad), (temp_air_one, temp_air_two), - wind_speed + wind_speed, + model=model ) assert (temp_one != temp_two).all() - temp_one_swtich, temp_two_switch = celltemp( - two_array_system, + temp_one_swtich, temp_two_switch = two_array_system.get_cell_temperature( (irrad, irrad), (temp_air_two, temp_air_one), - wind_speed + wind_speed, + model=model ) 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, - pvsystem.PVSystem.noct_sam_celltemp]) -def test_PVSystem_multi_array_celltemp_multi_wind(celltemp, two_array_system): +@pytest.mark.parametrize("model", + ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) +def test_PVSystem_multi_array_celltemp_multi_wind(model, 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, + temp_one, temp_two = two_array_system.get_cell_temperature( (irrad, irrad), temp_air, - (wind_speed_one, wind_speed_two) + (wind_speed_one, wind_speed_two), + model=model ) assert (temp_one != temp_two).all() - temp_one_swtich, temp_two_switch = celltemp( - two_array_system, + temp_one_swtich, temp_two_switch = two_array_system.get_cell_temperature( (irrad, irrad), temp_air, - (wind_speed_two, wind_speed_one) + (wind_speed_two, wind_speed_one), + model=model ) 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, - pvsystem.PVSystem.noct_sam_celltemp]) +@pytest.mark.parametrize("model", + ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) def test_PVSystem_multi_array_celltemp_temp_too_short( - celltemp, two_array_system): + model, two_array_system): with pytest.raises(ValueError, match="Length mismatch for per-array parameter"): - celltemp(two_array_system, (1000, 1000), (1,), 1) + two_array_system.get_cell_temperature((1000, 1000), (1,), 1, + model=model) -@pytest.mark.parametrize("celltemp", - [pvsystem.PVSystem.faiman_celltemp, - pvsystem.PVSystem.pvsyst_celltemp, - pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp, - pvsystem.PVSystem.noct_sam_celltemp]) +@pytest.mark.parametrize("model", + ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) def test_PVSystem_multi_array_celltemp_temp_too_long( - celltemp, two_array_system): + model, two_array_system): with pytest.raises(ValueError, match="Length mismatch for per-array parameter"): - celltemp(two_array_system, (1000, 1000), (1, 1, 1), 1) + two_array_system.get_cell_temperature((1000, 1000), (1, 1, 1), 1, + model=model) -@pytest.mark.parametrize("celltemp", - [pvsystem.PVSystem.faiman_celltemp, - pvsystem.PVSystem.pvsyst_celltemp, - pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp, - pvsystem.PVSystem.noct_sam_celltemp]) +@pytest.mark.parametrize("model", + ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) def test_PVSystem_multi_array_celltemp_wind_too_short( - celltemp, two_array_system): + model, two_array_system): with pytest.raises(ValueError, match="Length mismatch for per-array parameter"): - celltemp(two_array_system, (1000, 1000), 25, (1,)) + two_array_system.get_cell_temperature((1000, 1000), 25, (1,), + model=model) -@pytest.mark.parametrize("celltemp", - [pvsystem.PVSystem.faiman_celltemp, - pvsystem.PVSystem.pvsyst_celltemp, - pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp, - pvsystem.PVSystem.noct_sam_celltemp]) +@pytest.mark.parametrize("model", + ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) def test_PVSystem_multi_array_celltemp_wind_too_long( - celltemp, two_array_system): + model, two_array_system): with pytest.raises(ValueError, match="Length mismatch for per-array parameter"): - celltemp(two_array_system, (1000, 1000), 25, (1, 1, 1)) + two_array_system.get_cell_temperature((1000, 1000), 25, (1, 1, 1), + model=model) -@pytest.mark.parametrize("celltemp", - [pvsystem.PVSystem.faiman_celltemp, - pvsystem.PVSystem.pvsyst_celltemp, - pvsystem.PVSystem.sapm_celltemp, - pvsystem.PVSystem.fuentes_celltemp, - pvsystem.PVSystem.noct_sam_celltemp]) +@pytest.mark.parametrize("model", + ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) def test_PVSystem_multi_array_celltemp_poa_length_mismatch( - celltemp, two_array_system): + model, two_array_system): with pytest.raises(ValueError, match="Length mismatch for per-array parameter"): - celltemp(two_array_system, 1000, 25, 1) + two_array_system.get_cell_temperature(1000, 25, 1, model=model) def test_PVSystem_fuentes_celltemp(mocker): @@ -689,7 +665,7 @@ def test_PVSystem_fuentes_celltemp(mocker): temps = pd.Series(25, index) irrads = pd.Series(1000, index) winds = pd.Series(1, index) - out = system.fuentes_celltemp(irrads, temps, winds) + out = system.get_cell_temperature(irrads, temps, winds, model='fuentes') assert_series_equal(spy.call_args[0][0], irrads) assert_series_equal(spy.call_args[0][1], temps) assert_series_equal(spy.call_args[0][2], winds) @@ -713,14 +689,14 @@ def test_PVSystem_fuentes_celltemp_override(mocker): temp_model_params = {'noct_installed': noct_installed} system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params, surface_tilt=20) - system.fuentes_celltemp(irrads, temps, winds) + system.get_cell_temperature(irrads, temps, winds, model='fuentes') assert spy.call_args[1]['surface_tilt'] == 20 # can be overridden temp_model_params = {'noct_installed': noct_installed, 'surface_tilt': 30} system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params, surface_tilt=20) - system.fuentes_celltemp(irrads, temps, winds) + system.get_cell_temperature(irrads, temps, winds, model='fuentes') assert spy.call_args[1]['surface_tilt'] == 30 @@ -2185,3 +2161,23 @@ def test_Array_dc_ohms_from_percent(mocker): '{"Vmpp", "Impp"}.')): array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3}) out = array.dc_ohms_from_percent() + + +@pytest.mark.parametrize('funcname', ['sapm_celltemp', 'pvsyst_celltemp', + 'faiman_celltemp', 'fuentes_celltemp', + 'noct_sam_celltemp']) +def test_PVSystem_temperature_deprecated(funcname): + temp_model_params = { + 'a': -3.47, 'b': -0.0594, 'deltaT': 3, # sapm + 'noct_installed': 45, # fuentes + 'eta_m_ref': 0.2, 'noct': 45, # noct_sam + } + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) + func = getattr(system, funcname) + index = pd.date_range('2019-01-01', freq='h', periods=5) + temps = pd.Series(25, index) + irrads = pd.Series(1000, index) + winds = pd.Series(1, index) + + with pytest.warns(pvlibDeprecationWarning): + func(irrads, temps, winds) From df1e71212a2da35b6f01271e64707102bf7aed76 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 14 Apr 2021 20:31:05 -0600 Subject: [PATCH 03/21] 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 633a9d8cc0..5195febd78 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -239,6 +239,7 @@ PV temperature models temperature.fuentes temperature.ross temperature.noct_sam + pvsystem.PVSystem.get_cell_temperature pvsystem.PVSystem.sapm_celltemp pvsystem.PVSystem.pvsyst_celltemp pvsystem.PVSystem.faiman_celltemp From 3b850b03704ab68c20fc3e426413649579c24f75 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 14 Apr 2021 20:31:12 -0600 Subject: [PATCH 04/21] whatsnew --- docs/sphinx/source/whatsnew/v0.9.0.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 9a8f3878b7..b68681bdea 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -67,6 +67,17 @@ Deprecations * ``ModelChain.weather`` * ``ModelChain.times`` +* The following ``PVSystem`` cell temperature functions have been deprecated + and replaced with the wrapper method + :py:meth:`~pvlib.pvsystem.PVSystem.get_cell_temperature`: + + * :py:meth:`~pvlist.pvsystem.PVSystem.sapm_celltemp` + * :py:meth:`~pvlist.pvsystem.PVSystem.pvsystem.PVSystem.pvsyst_celltemp` + * :py:meth:`~pvlist.pvsystem.PVSystem.pvsystem.PVSystem.faiman_celltemp` + * :py:meth:`~pvlist.pvsystem.PVSystem.pvsystem.PVSystem.fuentes_celltemp` + * :py:meth:`~pvlist.pvsystem.PVSystem.pvsystem.PVSystem.noct_sam_celltemp` + + Enhancements ~~~~~~~~~~~~ * Add :func:`~pvlib.iotools.read_bsrn` for reading BSRN solar radiation data From 42691d1e1d02f7f90389106d75070822fa26cced Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 14 Apr 2021 20:51:42 -0600 Subject: [PATCH 05/21] add test for invalid model string --- 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 a7b37ad9d6..d50562a6dc 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -607,6 +607,12 @@ def test_PVSystem_multi_array_celltemp_multi_wind(model, two_array_system): assert_series_equal(temp_two, temp_one_swtich) +def test_PVSystem_get_cell_temperature_invalid(): + system = pvsystem.PVSystem() + with pytest.raises(ValueError, match='not a valid'): + system.get_cell_temperature(1000, 25, 1, 'not_a_model') + + @pytest.mark.parametrize("model", ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) def test_PVSystem_multi_array_celltemp_temp_too_short( From e44cc26b014702384b7dccbd2844c28a71a4627d Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 14 Apr 2021 20:58:39 -0600 Subject: [PATCH 06/21] stickler, docs --- docs/sphinx/source/whatsnew/v0.9.0.rst | 2 +- pvlib/modelchain.py | 2 +- pvlib/pvsystem.py | 11 +++++++++++ pvlib/tests/test_pvsystem.py | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index b68681bdea..46d0fd6d81 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -69,7 +69,7 @@ Deprecations * The following ``PVSystem`` cell temperature functions have been deprecated and replaced with the wrapper method - :py:meth:`~pvlib.pvsystem.PVSystem.get_cell_temperature`: + :py:meth:`~pvlib.pvsystem.PVSystem.get_cell_temperature` (:pull:`1211`): * :py:meth:`~pvlist.pvsystem.PVSystem.sapm_celltemp` * :py:meth:`~pvlist.pvsystem.PVSystem.pvsystem.PVSystem.pvsyst_celltemp` diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 08a1ac7b91..9addb16ec6 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1012,7 +1012,7 @@ def _set_celltemp(self, model): if model == 'noct_sam': kwargs['effective_irradiance'] = self.results.effective_irradiance self.results.cell_temperature = self.system.get_cell_temperature( - poa, temp_air, wind_speed, model=model, **kwargs) + poa, temp_air, wind_speed, model=model, **kwargs) return self def sapm_temp(self): diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 31dcc0aa56..65f5b94ea3 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -395,11 +395,22 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, wind_speed : numeric or tuple of numeric Wind speed in m/s. + model : str + Supported models include ``'sapm'``, ``'pvsyst'``, + ``'faiman'``, ``'fuentes'``, and ``'noct_sam'`` + + **kwargs + Extra arguments passed to the model function. + Returns ------- numeric or tuple of numeric values in degrees C. + See Also + -------- + Array.get_cell_temperature + Notes ----- The `temp_air` and `wind_speed` parameters may be passed as tuples diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index d50562a6dc..5d1f412782 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -553,7 +553,7 @@ def test_PVSystem_multi_array_celltemp_functions(model, two_array_system): temp_air = pd.Series(25, index=times) wind_speed = pd.Series(1, index=times) temp_one, temp_two = two_array_system.get_cell_temperature( - (irrad_one, irrad_two), temp_air, wind_speed, model=model) + (irrad_one, irrad_two), temp_air, wind_speed, model=model) assert (temp_one != temp_two).all() From e744b049f0bc055619c9f3a613b6d86ff64e5310 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 15 Apr 2021 17:08:49 -0600 Subject: [PATCH 07/21] nudge to trigger CI --- docs/sphinx/source/whatsnew/v0.9.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 46d0fd6d81..b80892b0c1 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -67,8 +67,8 @@ Deprecations * ``ModelChain.weather`` * ``ModelChain.times`` -* The following ``PVSystem`` cell temperature functions have been deprecated - and replaced with the wrapper method +* The following ``PVSystem`` cell temperature methods have been deprecated + and consolidated into the new wrapper method :py:meth:`~pvlib.pvsystem.PVSystem.get_cell_temperature` (:pull:`1211`): * :py:meth:`~pvlist.pvsystem.PVSystem.sapm_celltemp` From ec7b5fee431d098bb731e27edaaf5302bc0db3df Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 26 Apr 2021 17:07:12 -0600 Subject: [PATCH 08/21] fix whatsnew --- docs/sphinx/source/whatsnew/v0.9.0.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index b80892b0c1..3d09deb5da 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -71,11 +71,11 @@ Deprecations and consolidated into the new wrapper method :py:meth:`~pvlib.pvsystem.PVSystem.get_cell_temperature` (:pull:`1211`): - * :py:meth:`~pvlist.pvsystem.PVSystem.sapm_celltemp` - * :py:meth:`~pvlist.pvsystem.PVSystem.pvsystem.PVSystem.pvsyst_celltemp` - * :py:meth:`~pvlist.pvsystem.PVSystem.pvsystem.PVSystem.faiman_celltemp` - * :py:meth:`~pvlist.pvsystem.PVSystem.pvsystem.PVSystem.fuentes_celltemp` - * :py:meth:`~pvlist.pvsystem.PVSystem.pvsystem.PVSystem.noct_sam_celltemp` + * :py:meth:`~pvlib.pvsystem.PVSystem.sapm_celltemp` + * :py:meth:`~pvlib.pvsystem.PVSystem.pvsyst_celltemp` + * :py:meth:`~pvlib.pvsystem.PVSystem.faiman_celltemp` + * :py:meth:`~pvlib.pvsystem.PVSystem.fuentes_celltemp` + * :py:meth:`~pvlib.pvsystem.PVSystem.noct_sam_celltemp` Enhancements From 4c03c9366d555f95aeb3d06cbe2e948c178525c3 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 11 May 2021 20:04:57 -0600 Subject: [PATCH 09/21] docstring fix --- pvlib/modelchain.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 9addb16ec6..30f641f330 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -992,12 +992,9 @@ def _set_celltemp(self, 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). + model : str + A cell temperature model name to pass to + :py:meth:`pvlib.pvsystem.PVSystem.get_cell_temperature`. Returns ------- From 97e400d243f45dd90cdbc037340345ad9bb508de Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 11 May 2021 20:05:17 -0600 Subject: [PATCH 10/21] try out kinder error messages --- pvlib/pvsystem.py | 54 +++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 65f5b94ea3..74130222f6 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1482,13 +1482,26 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, Some temperature models have requirements for the input types; see the documentation of the underlying model function for details. """ + def _build_args(keys): + try: + args = (self.temperature_model_parameters[key] for key in keys) + except KeyError as e: + missing_key = e.args[0] + msg = (f"Missing required parameter '{missing_key}'. Found " + f"{self.temperature_model_parameters} in " + "temperature_model_parameters.") + raise KeyError(msg) + return args + if model == 'sapm': func = temperature.sapm_cell - params = _build_kwargs(['a', 'b', 'deltaT'], - self.temperature_model_parameters) + required = _build_args(['a', 'b', 'deltaT']) + optional = _build_kwargs(['irrad_ref'], + self.temperature_model_parameters) elif model == 'pvsyst': func = temperature.pvsyst_cell - params = { + required = tuple() + optional = { **_build_kwargs(['eta_m', 'alpha_absorption'], self.module_parameters), **_build_kwargs(['u_c', 'u_v'], @@ -1496,34 +1509,33 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, } elif model == 'faiman': func = temperature.faiman - params = _build_kwargs(['u0', 'u1'], - self.temperature_model_parameters) + required = tuple() + optional = _build_kwargs(['u0', 'u1'], + self.temperature_model_parameters) elif model == 'fuentes': func = temperature.fuentes + required = _build_args(['noct_installed']) + optional = _build_kwargs([ + 'module_height', 'wind_height', 'emissivity', 'absorption', + 'surface_tilt', 'module_width', 'module_length'], + self.temperature_model_parameters) # default to using the Array attribute, but allow user to override # with a custom surface_tilt value in temperature_model_parameters - params = _build_kwargs([ - 'noct_installed', 'module_height', 'wind_height', 'emissivity', - 'absorption', 'surface_tilt', 'module_width', 'module_length'], - self.temperature_model_parameters) - if 'surface_tilt' not in params: - params['surface_tilt'] = self.surface_tilt + if 'surface_tilt' not in kwargs: + kwargs['surface_tilt'] = self.surface_tilt elif model == 'noct_sam': func = temperature.noct_sam - params = _build_kwargs(['transmittance_absorptance', 'eta_m_ref', - 'noct', 'array_height', 'mount_standoff'], - self.temperature_model_parameters) - if not {'noct', 'eta_m_ref'}.issubset(params): - msg = ('Parameters noct and eta_m_ref are required.' - ' Found {} in temperature_model_parameters.' - .format(self.temperature_model_parameters)) - raise KeyError(msg) + required = _build_args(['noct', 'module_efficiency']) + optional = _build_kwargs(['transmittance_absorptance', + 'array_height', 'mount_standoff'], + self.temperature_model_parameters) else: raise ValueError(f'{model} is not a valid cell temperature model') # allow kwargs to override - params.update(kwargs) - temperature_cell = func(poa_global, temp_air, wind_speed, **params) + optional.update(kwargs) + temperature_cell = func(poa_global, temp_air, wind_speed, + *required, **optional) return temperature_cell def dc_ohms_from_percent(self): From 2c0cd004e59f04ae6c3d8bfd92a115c3917589b4 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 11 May 2021 20:43:40 -0600 Subject: [PATCH 11/21] fixing bugs --- pvlib/pvsystem.py | 4 ++-- pvlib/tests/test_pvsystem.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 44887ba6d7..e3602643be 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1523,8 +1523,8 @@ def _build_args(keys): self.temperature_model_parameters) # default to using the Array attribute, but allow user to override # with a custom surface_tilt value in temperature_model_parameters - if 'surface_tilt' not in kwargs: - kwargs['surface_tilt'] = self.surface_tilt + if 'surface_tilt' not in optional: + optional['surface_tilt'] = self.surface_tilt elif model == 'noct_sam': func = temperature.noct_sam required = _build_args(['noct', 'module_efficiency']) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 7604752dfc..9db450c1cb 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -512,8 +512,7 @@ def test_PVSystem_noct_celltemp(mocker): out = system.get_cell_temperature(poa_global, temp_air, wind_speed, model='noct_sam') temperature.noct_sam.assert_called_once_with( - poa_global, temp_air, wind_speed, effective_irradiance=None, noct=noct, - module_efficiency=module_efficiency) + poa_global, temp_air, wind_speed, noct, module_efficiency) assert_allclose(out, expected) # different types out = system.get_cell_temperature(np.array(poa_global), np.array(temp_air), @@ -678,7 +677,7 @@ def test_PVSystem_fuentes_celltemp(mocker): assert_series_equal(spy.call_args[0][0], irrads) assert_series_equal(spy.call_args[0][1], temps) assert_series_equal(spy.call_args[0][2], winds) - assert spy.call_args[1]['noct_installed'] == noct_installed + assert spy.call_args[0][3] == noct_installed assert_series_equal(out, pd.Series([52.85, 55.85, 55.85], index, name='tmod')) @@ -2179,7 +2178,7 @@ def test_PVSystem_temperature_deprecated(funcname): temp_model_params = { 'a': -3.47, 'b': -0.0594, 'deltaT': 3, # sapm 'noct_installed': 45, # fuentes - 'eta_m_ref': 0.2, 'noct': 45, # noct_sam + 'module_efficiency': 0.2, 'noct': 45, # noct_sam } system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) func = getattr(system, funcname) From cb85661e5b2fb847f9a750b79d144cfc7caf8501 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 11 May 2021 20:43:52 -0600 Subject: [PATCH 12/21] test for nice error message --- pvlib/pvsystem.py | 2 +- pvlib/tests/test_pvsystem.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index e3602643be..4be58a3e56 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1484,7 +1484,7 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, """ def _build_args(keys): try: - args = (self.temperature_model_parameters[key] for key in keys) + args = [self.temperature_model_parameters[key] for key in keys] except KeyError as e: missing_key = e.args[0] msg = (f"Missing required parameter '{missing_key}'. Found " diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 9db450c1cb..49c7bd36df 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2189,3 +2189,25 @@ def test_PVSystem_temperature_deprecated(funcname): with pytest.warns(pvlibDeprecationWarning): func(irrads, temps, winds) + + +@pytest.mark.parametrize('model,keys', [ + ('sapm', ('a', 'b', 'deltaT')), + ('fuentes', ('noct_installed',)), + ('noct_sam', ('noct', 'module_efficiency')) +]) +def test_Array_temperature_missing_parameters(model, keys): + # test that a nice error is raised when required temp params are missing + array = pvsystem.Array() + index = pd.date_range('2019-01-01', freq='h', periods=5) + temps = pd.Series(25, index) + irrads = pd.Series(1000, index) + winds = pd.Series(1, index) + + for key in keys: + match = f"Missing required parameter '{key}'" + params = {k: 1 for k in keys} # dummy values + params.pop(key) # remove each key in turn + array.temperature_model_parameters = params + with pytest.raises(KeyError, match=match): + array.get_cell_temperature(irrads, temps, winds, model) From f01b9c19a36db163c2b6dd4da77844ad0612e51f Mon Sep 17 00:00:00 2001 From: Kevin Anderson <57452607+kanderso-nrel@users.noreply.github.com> Date: Tue, 11 May 2021 20:44:27 -0600 Subject: [PATCH 13/21] Update pvlib/pvsystem.py Co-authored-by: Cliff Hansen --- pvlib/pvsystem.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 4be58a3e56..8db5e8a7e6 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -414,10 +414,9 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, 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. + to provide different values for each Array in the system. If passed as a + tuple the length must be the same as the number of Arrays. If not + passed as a tuple then the same value is used for each Array. """ poa_global = self._validate_per_array(poa_global) temp_air = self._validate_per_array(temp_air, system_wide=True) From 774807475b9ff67b77408af0f2d62d331dc7fe26 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 11 May 2021 20:45:45 -0600 Subject: [PATCH 14/21] stickler --- pvlib/pvsystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 8db5e8a7e6..634cffeb14 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -414,8 +414,8 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, Notes ----- The `temp_air` and `wind_speed` parameters may be passed as tuples - to provide different values for each Array in the system. If passed as a - tuple the length must be the same as the number of Arrays. If not + to provide different values for each Array in the system. If passed as + a tuple the length must be the same as the number of Arrays. If not passed as a tuple then the same value is used for each Array. """ poa_global = self._validate_per_array(poa_global) From 4f03b08ec9ad60a9ac337fedc54045c5d4b86f5a Mon Sep 17 00:00:00 2001 From: Kevin Anderson <57452607+kanderso-nrel@users.noreply.github.com> Date: Wed, 12 May 2021 10:13:43 -0600 Subject: [PATCH 15/21] Update pvlib/modelchain.py Co-authored-by: Cliff Hansen --- pvlib/modelchain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index ec7f98b6c1..c661a0ec39 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -995,6 +995,7 @@ def _set_celltemp(self, model): model : str A cell temperature model name to pass to :py:meth:`pvlib.pvsystem.PVSystem.get_cell_temperature`. + Valid names are 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam' Returns ------- From 0e59b30da86e2e4d4d6ac84ed350868663f02123 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 13 May 2021 19:07:19 -0600 Subject: [PATCH 16/21] move _build_args to pvlib.tools --- pvlib/pvsystem.py | 23 ++++++++--------------- pvlib/tools.py | 28 +++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 9a7e6fa127..19652a61d5 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -16,7 +16,7 @@ from pvlib import (atmosphere, iam, inverter, irradiance, singlediode as _singlediode, temperature) -from pvlib.tools import _build_kwargs +from pvlib.tools import _build_kwargs, _build_args # a dict of required parameter names for each DC power model @@ -1472,20 +1472,13 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, Some temperature models have requirements for the input types; see the documentation of the underlying model function for details. """ - def _build_args(keys): - try: - args = [self.temperature_model_parameters[key] for key in keys] - except KeyError as e: - missing_key = e.args[0] - msg = (f"Missing required parameter '{missing_key}'. Found " - f"{self.temperature_model_parameters} in " - "temperature_model_parameters.") - raise KeyError(msg) - return args - + def _build_tcell_args(keys): + # convenience wrapper to avoid passing args 2 and 3 every call + return _build_args(keys, self.temperature_model_parameters, + 'temperature_model_parameters') if model == 'sapm': func = temperature.sapm_cell - required = _build_args(['a', 'b', 'deltaT']) + required = _build_tcell_args(['a', 'b', 'deltaT']) optional = _build_kwargs(['irrad_ref'], self.temperature_model_parameters) elif model == 'pvsyst': @@ -1506,7 +1499,7 @@ def _build_args(keys): self.temperature_model_parameters) elif model == 'fuentes': func = temperature.fuentes - required = _build_args(['noct_installed']) + required = _build_tcell_args(['noct_installed']) optional = _build_kwargs([ 'module_height', 'wind_height', 'emissivity', 'absorption', 'surface_tilt', 'module_width', 'module_length'], @@ -1517,7 +1510,7 @@ def _build_args(keys): optional['surface_tilt'] = self.surface_tilt elif model == 'noct_sam': func = temperature.noct_sam - required = _build_args(['noct', 'module_efficiency']) + required = _build_tcell_args(['noct', 'module_efficiency']) optional = _build_kwargs(['transmittance_absorptance', 'array_height', 'mount_standoff'], self.temperature_model_parameters) diff --git a/pvlib/tools.py b/pvlib/tools.py index b6ee3e7c3a..eef80a3b37 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -230,7 +230,7 @@ def _build_kwargs(keys, input_dict): ---------- keys : iterable Typically a list of strings. - adict : dict-like + input_dict : dict-like A dictionary from which to attempt to pull each key. Returns @@ -249,6 +249,32 @@ def _build_kwargs(keys, input_dict): return kwargs +def _build_args(keys, input_dict, dict_name): + """ + Parameters + ---------- + keys : iterable + Typically a list of strings. + input_dict : dict-like + A dictionary from which to pull each key. + dict_name : str + A variable name to include in an error message for missing keys + + Returns + ------- + kwargs : list + A list with values corresponding to keys + """ + try: + args = [input_dict[key] for key in keys] + except KeyError as e: + missing_key = e.args[0] + msg = (f"Missing required parameter '{missing_key}'. Found " + f"{input_dict} in {dict_name}.") + raise KeyError(msg) + return args + + # Created April,2014 # Author: Rob Andrews, Calama Consulting From d5380372ff7060aab6077bbe415a4cad69fc6aac Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 May 2021 07:47:17 -0600 Subject: [PATCH 17/21] remove deprecated items from api.rst --- docs/sphinx/source/api.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 5195febd78..6cd644c588 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -240,11 +240,6 @@ PV temperature models temperature.ross temperature.noct_sam pvsystem.PVSystem.get_cell_temperature - pvsystem.PVSystem.sapm_celltemp - pvsystem.PVSystem.pvsyst_celltemp - pvsystem.PVSystem.faiman_celltemp - pvsystem.PVSystem.fuentes_celltemp - pvsystem.PVSystem.noct_sam_celltemp Temperature Model Parameters ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From b97c04512d1f1bec7a84b95b9e95f86ac65318ca Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 May 2021 07:54:23 -0600 Subject: [PATCH 18/21] def -> partial --- pvlib/pvsystem.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 19652a61d5..766edb82e6 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1472,10 +1472,11 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, Some temperature models have requirements for the input types; see the documentation of the underlying model function for details. """ - def _build_tcell_args(keys): - # convenience wrapper to avoid passing args 2 and 3 every call - return _build_args(keys, self.temperature_model_parameters, - 'temperature_model_parameters') + # convenience wrapper to avoid passing args 2 and 3 every call + _build_tcell_args = functools.partial( + _build_args, input_dict=self.temperature_model_parameters, + dict_name='temperature_model_parameters') + if model == 'sapm': func = temperature.sapm_cell required = _build_tcell_args(['a', 'b', 'deltaT']) From bb08d628533c57c7eca8817c14b69a20f82cdba4 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 May 2021 07:54:46 -0600 Subject: [PATCH 19/21] docstring edits --- pvlib/pvsystem.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 766edb82e6..f7fca95520 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -405,7 +405,7 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, Returns ------- numeric or tuple of numeric - values in degrees C. + Values in degrees C. See Also -------- @@ -1465,7 +1465,12 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, Returns ------- numeric - values in degrees C. + Values in degrees C. + + See Also + -------- + temperature.sapm_cell, temperature.pvsyst_cell, temperature.faiman, + temperature.fuentes, temperature.noct_sam Notes ----- From f50c7fe4d57ffc77d97ff8120183492d35ec38ee Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 May 2021 08:01:26 -0600 Subject: [PATCH 20/21] see also fixes --- pvlib/pvsystem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index f7fca95520..dcffff6f43 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1469,8 +1469,9 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, See Also -------- - temperature.sapm_cell, temperature.pvsyst_cell, temperature.faiman, - temperature.fuentes, temperature.noct_sam + pvlib.temperature.sapm_cell, pvlib.temperature.pvsyst_cell, + pvlib.temperature.faiman, pvlib.temperature.fuentes, + pvlib.temperature.noct_sam Notes ----- From 98c6776e2958c5e94eb204625e032495f4862bdc Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 May 2021 08:51:34 -0600 Subject: [PATCH 21/21] remove kwarg nonsense --- pvlib/pvsystem.py | 43 +++++++++++++++--------------------- pvlib/tests/test_pvsystem.py | 3 ++- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index dcffff6f43..487f7b953d 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -380,7 +380,7 @@ def get_iam(self, aoi, iam_model='physical'): @_unwrap_single_value def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, - **kwargs): + effective_irradiance=None): """ Determine cell temperature using the method specified by ``model``. @@ -399,8 +399,9 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, Supported models include ``'sapm'``, ``'pvsyst'``, ``'faiman'``, ``'fuentes'``, and ``'noct_sam'`` - **kwargs - Extra arguments passed to the model function. + effective_irradiance : numeric or tuple of numeric, optional + The irradiance that is converted to photocurrent in W/m^2. + Only used for some models. Returns ------- @@ -421,25 +422,17 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, 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) - - # additional model-specific (and array-specific) inputs - extra_inputs = [{}] * self.num_arrays - - if 'effective_irradiance' in kwargs: - effective_irradiance = kwargs.pop('effective_irradiance') - if effective_irradiance is None: - effective_irradiance = tuple([None] * self.num_arrays) - else: - effective_irradiance = self._validate_per_array( - effective_irradiance) - for inputs, value in zip(extra_inputs, effective_irradiance): - inputs['effective_irradiance'] = value + # Not used for all models, but Array.get_cell_temperature handles it + effective_irradiance = self._validate_per_array(effective_irradiance, + system_wide=True) return tuple( array.get_cell_temperature(poa_global, temp_air, wind_speed, - model, **extra, **kwargs) - for array, poa_global, temp_air, wind_speed, extra in zip( - self.arrays, poa_global, temp_air, wind_speed, extra_inputs + model, effective_irradiance) + for array, poa_global, temp_air, wind_speed, effective_irradiance + in zip( + self.arrays, poa_global, temp_air, wind_speed, + effective_irradiance ) ) @@ -1440,7 +1433,7 @@ def get_iam(self, aoi, iam_model='physical'): raise ValueError(model + ' is not a valid IAM model') def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, - **kwargs): + effective_irradiance=None): """ Determine cell temperature using the method specified by ``model``. @@ -1459,8 +1452,9 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, Supported models include ``'sapm'``, ``'pvsyst'``, ``'faiman'``, ``'fuentes'``, and ``'noct_sam'`` - **kwargs - Extra arguments passed to the model function. + effective_irradiance : numeric, optional + The irradiance that is converted to photocurrent in W/m^2. + Only used for some models. Returns ------- @@ -1516,7 +1510,8 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, if 'surface_tilt' not in optional: optional['surface_tilt'] = self.surface_tilt elif model == 'noct_sam': - func = temperature.noct_sam + func = functools.partial(temperature.noct_sam, + effective_irradiance=effective_irradiance) required = _build_tcell_args(['noct', 'module_efficiency']) optional = _build_kwargs(['transmittance_absorptance', 'array_height', 'mount_standoff'], @@ -1524,8 +1519,6 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, else: raise ValueError(f'{model} is not a valid cell temperature model') - # allow kwargs to override - optional.update(kwargs) temperature_cell = func(poa_global, temp_air, wind_speed, *required, **optional) return temperature_cell diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 49c7bd36df..4b0badec00 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -512,7 +512,8 @@ def test_PVSystem_noct_celltemp(mocker): out = system.get_cell_temperature(poa_global, temp_air, wind_speed, model='noct_sam') temperature.noct_sam.assert_called_once_with( - poa_global, temp_air, wind_speed, noct, module_efficiency) + poa_global, temp_air, wind_speed, noct, module_efficiency, + effective_irradiance=None) assert_allclose(out, expected) # different types out = system.get_cell_temperature(np.array(poa_global), np.array(temp_air),