diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 633a9d8cc0..6cd644c588 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -239,11 +239,7 @@ PV temperature models temperature.fuentes temperature.ross temperature.noct_sam - pvsystem.PVSystem.sapm_celltemp - pvsystem.PVSystem.pvsyst_celltemp - pvsystem.PVSystem.faiman_celltemp - pvsystem.PVSystem.fuentes_celltemp - pvsystem.PVSystem.noct_sam_celltemp + pvsystem.PVSystem.get_cell_temperature Temperature Model Parameters ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index b953cee238..8e0a828778 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -71,6 +71,16 @@ Deprecations * ``ModelChain.weather`` * ``ModelChain.times`` +* 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:`~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` + * The ``eta_m`` parameter for :py:func:`~pvlib.temperature.pvsyst_cell` is replaced by parameter ``module_efficiency``. (:issue:`1188`, :pull:`1218`) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 207b445e50..c661a0ec39 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -992,12 +992,10 @@ 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`. + Valid names are 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam' Returns ------- @@ -1009,26 +1007,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 3a662968ad..487f7b953d 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 @@ -378,6 +378,64 @@ 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, + effective_irradiance=None): + """ + 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. + + model : str + Supported models include ``'sapm'``, ``'pvsyst'``, + ``'faiman'``, ``'fuentes'``, and ``'noct_sam'`` + + 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 + ------- + 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 + 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) + wind_speed = self._validate_per_array(wind_speed, system_wide=True) + # 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, effective_irradiance) + for array, poa_global, temp_air, wind_speed, effective_irradiance + in zip( + self.arrays, poa_global, temp_air, wind_speed, + effective_irradiance + ) + ) + @_unwrap_single_value def calcparams_desoto(self, effective_irradiance, temp_cell): """ @@ -522,7 +580,8 @@ def sapm(self, effective_irradiance, temp_cell): 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. @@ -551,20 +610,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): @@ -626,7 +673,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. @@ -657,26 +705,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) - - def build_celltemp_kwargs(array): - # TODO remove 'eta_m' after deprecation of this parameter - return {**_build_kwargs(['eta_m', 'module_efficiency', - '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 - ) - ) + return self.get_cell_temperature(poa_global, temp_air, wind_speed, + model='pvsyst') - @_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. @@ -707,20 +740,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. @@ -756,30 +780,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): """ @@ -813,47 +818,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['module_efficiency'] = \ - array.temperature_model_parameters['module_efficiency'] - except KeyError: - msg = ('Parameters noct and module_efficiency 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): @@ -1307,7 +1274,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 @@ -1466,6 +1432,97 @@ 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, + effective_irradiance=None): + """ + 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'`` + + effective_irradiance : numeric, optional + The irradiance that is converted to photocurrent in W/m^2. + Only used for some models. + + Returns + ------- + numeric + Values in degrees C. + + See Also + -------- + pvlib.temperature.sapm_cell, pvlib.temperature.pvsyst_cell, + pvlib.temperature.faiman, pvlib.temperature.fuentes, + pvlib.temperature.noct_sam + + Notes + ----- + Some temperature models have requirements for the input types; + see the documentation of the underlying model function for details. + """ + # 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']) + optional = _build_kwargs(['irrad_ref'], + self.temperature_model_parameters) + elif model == 'pvsyst': + func = temperature.pvsyst_cell + required = tuple() + optional = { + # TODO remove 'eta_m' after deprecation of this parameter + **_build_kwargs(['eta_m', 'module_efficiency', + 'alpha_absorption'], + self.module_parameters), + **_build_kwargs(['u_c', 'u_v'], + self.temperature_model_parameters) + } + elif model == 'faiman': + func = temperature.faiman + required = tuple() + optional = _build_kwargs(['u0', 'u1'], + self.temperature_model_parameters) + elif model == 'fuentes': + func = temperature.fuentes + required = _build_tcell_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 + if 'surface_tilt' not in optional: + optional['surface_tilt'] = self.surface_tilt + elif model == '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'], + self.temperature_model_parameters) + else: + raise ValueError(f'{model} is not a valid cell temperature model') + + temperature_cell = func(poa_global, temp_air, wind_speed, + *required, **optional) + return temperature_cell + def dc_ohms_from_percent(self): """ Calculates the equivalent resistance of the wires using diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 7a353a1a84..e8692cd1fa 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 0a4125dd5b..4b0badec00 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 @@ -480,7 +480,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_speed=wind, u_c=temp_model_params['u_c'], u_v=temp_model_params['u_v'], module_efficiency=module_efficiency, @@ -496,7 +497,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) @@ -508,20 +509,22 @@ def test_PVSystem_noct_celltemp(mocker): temp_model_params = {'noct': noct, 'module_efficiency': module_efficiency} 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, - module_efficiency=module_efficiency) + poa_global, temp_air, wind_speed, noct, module_efficiency, + effective_irradiance=None) 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, @@ -529,8 +532,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) @@ -539,147 +543,126 @@ def test_PVSystem_noct_celltemp_error(): temp_model_params = {'module_efficiency': module_efficiency} 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]) +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( - 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): @@ -691,11 +674,11 @@ 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) - 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')) @@ -715,14 +698,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 @@ -2187,3 +2170,45 @@ 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 + 'module_efficiency': 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) + + +@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) 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