diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index f06b1d8140..1094973bd5 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -46,6 +46,10 @@ Enhancements :py:class:`~pvlib.pvsystem.PVSystem` and :py:class:`~pvlib.modelchain.ModelChain` (as ``ac_model='sandia_multi'``). (:pull:`1076`, :issue:`1067`) +* :py:class:`~pvlib.modelchain.ModelChain` 'run_model' methods now + automatically switch to using ``'effective_irradiance'`` (if available) for + cell temperature models, when ``'poa_global'`` is not provided in input + weather or calculated from input weather data. Bug fixes ~~~~~~~~~ diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index c5414fdc12..4c67e835f4 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1023,7 +1023,9 @@ def _set_celltemp(self, model): ------- self """ - poa = _tuple_from_dfs(self.results.total_irrad, 'poa_global') + + poa = _irrad_for_celltemp(self.results.total_irrad, + self.results.effective_irradiance) temp_air = _tuple_from_dfs(self.weather, 'temp_air') wind_speed = _tuple_from_dfs(self.weather, 'wind_speed') self.results.cell_temperature = model(poa, temp_air, wind_speed) @@ -1464,13 +1466,22 @@ def prepare_inputs_from_poa(self, data): return self def _get_cell_temperature(self, data, - total_irrad, temperature_model_parameters): + poa, temperature_model_parameters): """Extract the cell temperature data from a DataFrame. - If 'cell_temperature' column exists then it is returned. If - 'module_temperature' column exists then it is used to calculate - the cell temperature. If neither column exists then None is + If 'cell_temperature' column exists in data then it is returned. If + 'module_temperature' column exists in data, then it is used with poa to + calculate the cell temperature. If neither column exists then None is returned. + + Parameters + ---------- + data : DataFrame (not a tuple of DataFrame) + poa : Series (not a tuple of Series) + + Returns + ------- + Series """ if 'cell_temperature' in data: return data['cell_temperature'] @@ -1483,14 +1494,14 @@ def _get_cell_temperature(self, data, # use SAPM cell temperature model only return pvlib.temperature.sapm_cell_from_module( module_temperature=data['module_temperature'], - poa_global=total_irrad['poa_global'], + poa_global=poa, deltaT=temperature_model_parameters['deltaT']) - def _prepare_temperature_single_array(self, data): - """Set cell_temperature using a single weather data frame.""" + def _prepare_temperature_single_array(self, data, poa): + """Set cell_temperature using a single data frame.""" self.results.cell_temperature = self._get_cell_temperature( data, - self.results.total_irrad, + poa, self.system.temperature_model_parameters ) if self.results.cell_temperature is None: @@ -1505,7 +1516,7 @@ def _prepare_temperature(self, data=None): If 'data' contains 'cell_temperature', these values are assigned to attribute ``cell_temperature``. If 'data' contains 'module_temperature` and `temperature_model' is 'sapm', cell temperature is calculated using - :py:func:`pvlib.temperature.sapm_celL_from_module`. Otherwise, cell + :py:func:`pvlib.temperature.sapm_cell_from_module`. Otherwise, cell temperature is calculated by 'temperature_model'. Parameters @@ -1521,14 +1532,16 @@ def _prepare_temperature(self, data=None): Assigns attribute ``results.cell_temperature``. """ + poa = _irrad_for_celltemp(self.results.total_irrad, + self.results.effective_irradiance) if not isinstance(data, tuple) and self.system.num_arrays > 1: + # broadcast data to all arrays data = (data,) * self.system.num_arrays elif not isinstance(data, tuple): - return self._prepare_temperature_single_array(data) + return self._prepare_temperature_single_array(data, poa) given_cell_temperature = tuple(itertools.starmap( self._get_cell_temperature, - zip(data, self.results.total_irrad, - self.system.temperature_model_parameters) + zip(data, poa, self.system.temperature_model_parameters) )) # If cell temperature has been specified for all arrays return # immediately and do not try to compute it. @@ -1716,10 +1729,8 @@ def run_model_from_effective_irradiance(self, data=None): ---------- data : DataFrame, or list or tuple of DataFrame Required column is ``'effective_irradiance'``. - If optional column ``'cell_temperature'`` is provided, these values - are used instead of `temperature_model`. If optional column - ``'module_temperature'`` is provided, `temperature_model` must be - ``'sapm'``. + Optional columns include ``'cell_temperature'``, + ``'module_temperature'`` and ``'poa_global'``. If the ModelChain's PVSystem has multiple arrays, `data` must be a list or tuple with the same length and order as the PVsystem's @@ -1740,6 +1751,20 @@ def run_model_from_effective_irradiance(self, data=None): Notes ----- + Optional ``data`` columns ``'cell_temperature'``, + ``'module_temperature'`` and ``'poa_global'`` are used for determining + cell temperature. + + * If optional column ``'cell_temperature'`` is present, these values + are used and `temperature_model` is ignored. + * If optional column ``'module_temperature'`` is preset, + `temperature_model` must be ``'sapm'``. + * Otherwise, cell temperature is calculated using `temperature_model`. + + The cell temperature models require plane-of-array irradiance as input. + If optional column ``'poa_global'`` is present, these data are used. + If ``'poa_global'`` is not present, ``'effective_irradiance'`` is used. + Assigns attributes: ``weather``, ``total_irrad``, ``effective_irradiance``, ``cell_temperature``, ``dc``, ``ac``, ``losses``, ``diode_params`` (if dc_model is a single diode model). @@ -1760,6 +1785,29 @@ def run_model_from_effective_irradiance(self, data=None): return self +def _irrad_for_celltemp(total_irrad, effective_irradiance): + """ + Determine irradiance to use for cell temperature models, in order + of preference 'poa_global' then 'effective_irradiance' + + Returns + ------- + Series or tuple of Series + tuple if total_irrad is a tuple of DataFrame + + """ + if isinstance(total_irrad, tuple): + if all(['poa_global' in df for df in total_irrad]): + return _tuple_from_dfs(total_irrad, 'poa_global') + else: + return effective_irradiance + else: + if 'poa_global' in total_irrad: + return total_irrad['poa_global'] + else: + return effective_irradiance + + def _snl_params(inverter_params): """Return True if `inverter_params` includes parameters for the Sandia inverter model.""" diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 1f27180a9a..03871143e8 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -49,7 +49,7 @@ def _temperature_model_params(model, parameter_set): def sapm_cell(poa_global, temp_air, wind_speed, a, b, deltaT, - irrad_ref=1000): + irrad_ref=1000.): r''' Calculate cell temperature per the Sandia Array Performance Model. @@ -215,7 +215,7 @@ def sapm_module(poa_global, temp_air, wind_speed, a, b): def sapm_cell_from_module(module_temperature, poa_global, deltaT, - irrad_ref=1000): + irrad_ref=1000.): r''' Calculate cell temperature from module temperature using the Sandia Array Performance Model. diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 4e09f77f84..2987ae563f 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -802,7 +802,7 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays, location, weather, total_irrad): data = weather.copy() - data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad + data[['poa_global', 'poa_direct', 'poa_diffuse']] = total_irrad data_two = data.copy() mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location, aoi_model='no_loss', spectral_model='no_loss') @@ -918,6 +918,31 @@ def test_run_model_from_effective_irradiance(sapm_dc_snl_ac_system, location, assert_series_equal(ac, expected) +def test_run_model_from_effective_irradiance_no_poa_global( + sapm_dc_snl_ac_system, location, weather, total_irrad): + data = weather.copy() + data['effective_irradiance'] = total_irrad['poa_global'] + mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', + spectral_model='no_loss') + ac = mc.run_model_from_effective_irradiance(data).results.ac + expected = pd.Series(np.array([149.280238, 96.678385]), + index=data.index) + assert_series_equal(ac, expected) + + +def test_run_model_from_effective_irradiance_poa_global_differs( + sapm_dc_snl_ac_system, location, weather, total_irrad): + data = weather.copy() + data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad + data['effective_irradiance'] = data['poa_global'] * 0.8 + mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', + spectral_model='no_loss') + ac = mc.run_model_from_effective_irradiance(data).results.ac + expected = pd.Series(np.array([118.302801, 76.099841]), + index=data.index) + assert_series_equal(ac, expected) + + @pytest.mark.parametrize("input_type", [tuple, list]) def test_run_model_from_effective_irradiance_arrays_error( sapm_dc_snl_ac_system_Array, location, weather, total_irrad, @@ -1745,3 +1770,26 @@ def test_modelchain__common_keys(): assert {'b'} == modelchain._common_keys( (series, no_a) ) + + +def test__irrad_for_celltemp(): + total_irrad = pd.DataFrame(index=[0, 1], columns=['poa_global'], + data=[10., 20.]) + empty = total_irrad.drop('poa_global', axis=1) + effect_irrad = pd.Series(index=total_irrad.index, data=[5., 8.]) + # test with single array inputs + poa = modelchain._irrad_for_celltemp(total_irrad, effect_irrad) + assert_series_equal(poa, total_irrad['poa_global']) + poa = modelchain._irrad_for_celltemp(empty, effect_irrad) + assert_series_equal(poa, effect_irrad) + # test with tuples + poa = modelchain._irrad_for_celltemp( + (total_irrad, total_irrad), (effect_irrad, effect_irrad)) + assert len(poa) == 2 + assert_series_equal(poa[0], total_irrad['poa_global']) + assert_series_equal(poa[1], total_irrad['poa_global']) + poa = modelchain._irrad_for_celltemp( + (empty, empty), (effect_irrad, effect_irrad)) + assert len(poa) == 2 + assert_series_equal(poa[0], effect_irrad) + assert_series_equal(poa[1], effect_irrad)