Skip to content

Handle poa_global and effective_irradiance for cell temperature models #1129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.9.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~
Expand Down
82 changes: 65 additions & 17 deletions pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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']
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


* 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).
Expand All @@ -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."""
Expand Down
4 changes: 2 additions & 2 deletions pvlib/temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
50 changes: 49 additions & 1 deletion pvlib/tests/test_modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also need a test for when both effective irradiance and poa global are provided? I think not but thought it was worth mentioning.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth adding this test. All of the tests for run_model_from_effective_irradiance copy the values for 'poa_global' to 'effective_irradiance' (for convenience) so can't distinguish if the wrong values are being used to calculate power.

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,
Expand Down Expand Up @@ -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)