Skip to content

Expose temperature.fuentes in PVSystem and ModelChain #1073

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
Oct 9, 2020
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
1 change: 1 addition & 0 deletions docs/sphinx/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ ModelChain model definitions.
modelchain.ModelChain.sapm_temp
modelchain.ModelChain.pvsyst_temp
modelchain.ModelChain.faiman_temp
modelchain.ModelChain.fuentes_temp
modelchain.ModelChain.pvwatts_losses
modelchain.ModelChain.no_extra_losses

Expand Down
2 changes: 2 additions & 0 deletions docs/sphinx/source/whatsnew/v0.8.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Deprecations

Enhancements
~~~~~~~~~~~~
* Create :py:func:`~pvlib.pvsystem.PVSystem.fuentes_celltemp` and add ``temperature_model='fuentes'``
option to :py:class:`~pvlib.modelchain.ModelChain`. (:pull:`1042`) (:issue:`1073`)
* Added :py:func:`pvlib.temperature.ross` for cell temperature modeling using
only NOCT. (:pull:`1045`)

Expand Down
16 changes: 13 additions & 3 deletions pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,9 @@ class ModelChain:
as the first argument to a user-defined function.

temperature_model: None, str or function, default None
Valid strings are 'sapm', 'pvsyst', and 'faiman'. The ModelChain
instance will be passed as the first argument to a user-defined
function.
Valid strings are 'sapm', 'pvsyst', 'faiman', and 'fuentes'.
The ModelChain instance will be passed as the first argument to a
user-defined function.

losses_model: str or function, default 'no_loss'
Valid strings are 'pvwatts', 'no_loss'. The ModelChain instance
Expand Down Expand Up @@ -866,6 +866,8 @@ def temperature_model(self, model):
self._temperature_model = self.pvsyst_temp
elif model == 'faiman':
self._temperature_model = self.faiman_temp
elif model == 'fuentes':
self._temperature_model = self.fuentes_temp
else:
raise ValueError(model + ' is not a valid temperature model')
# check system.temperature_model_parameters for consistency
Expand All @@ -891,6 +893,8 @@ def infer_temperature_model(self):
return self.pvsyst_temp
elif {'u0', 'u1'} <= params:
return self.faiman_temp
elif {'noct_installed'} <= params:
return self.fuentes_temp
else:
raise ValueError('could not infer temperature model from '
'system.temperature_module_parameters {}.'
Expand All @@ -914,6 +918,12 @@ def faiman_temp(self):
self.weather['wind_speed'])
return self

def fuentes_temp(self):
self.cell_temperature = self.system.fuentes_celltemp(
self.total_irrad['poa_global'], self.weather['temp_air'],
self.weather['wind_speed'])
return self

@property
def losses_model(self):
return self._losses_model
Expand Down
40 changes: 40 additions & 0 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,46 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0):
return temperature.faiman(poa_global, temp_air, wind_speed,
**kwargs)

def fuentes_celltemp(self, poa_global, temp_air, wind_speed):
"""
Use :py:func:`temperature.fuentes` to calculate cell temperature.

Parameters
----------
poa_global : pandas Series
Total incident irradiance [W/m^2]

temp_air : pandas Series
Ambient dry bulb temperature [C]

wind_speed : pandas Series
Wind speed [m/s]

Returns
-------
temperature_cell : pandas Series
The modeled cell temperature [C]

Notes
-----
The Fuentes thermal model uses the module surface tilt for convection
modeling. The SAM implementation of PVWatts hardcodes the surface tilt
value at 30 degrees, ignoring whatever value is used for irradiance
transposition. This method defaults to using ``self.surface_tilt``, but
if you want to match the PVWatts behavior, you can override it by
including a ``surface_tilt`` value in ``temperature_model_parameters``.
"""
# default to using the PVSystem attribute, but allow user to
# override with a custom surface_tilt value
kwargs = {'surface_tilt': self.surface_tilt}
temp_model_kwargs = _build_kwargs([
'noct_installed', 'module_height', 'wind_height', 'emissivity',
'absorption', 'surface_tilt', 'module_width', 'module_length'],
Copy link
Member

Choose a reason for hiding this comment

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

I think 'surface_tilt' is a PVSystem attribute, not to be found in temperature_model_parameters

We might also consider getting 'module_width' and 'module_length' from module_parameters, although: the database doesn't supply these values and they aren't used anywhere else. 'module_height' is really an attribute of the PV System.

For now, I'm OK expecting the 'module_' values in temperature_model_parameters but they will move once another, non-temperature function needs this information - e.g., a bifacial irradiance function.

Copy link
Member Author

Choose a reason for hiding this comment

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

Valid points. I unthinkingly pulled all the extra parameters from temperature.fuentes and dumped them there.

I can imagine some people objecting to using Fuentes with the real surface_tilt value because they want the same behavior as the SSC implementation of PVWatts, which hardcodes the tilt at 30. Would it make sense to use the PVSystem attributes by default but allow the user to override them by providing alternate values in the temperature_model_parameters dictionary?

Copy link
Member

@cwhanse cwhanse Oct 1, 2020

Choose a reason for hiding this comment

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

I don't have a better idea at the moment, than using temperature_model_parameters['surface_tilt'] as the signal to override. But I think it risks confusing users, to have two 'surface_tilt' quantities with different meanings. Maybe a Note in the PVSystem.celltemp_fuentes docstring and move ahead.

Copy link
Member

Choose a reason for hiding this comment

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

Also something to consider when creating an Array class.

self.temperature_model_parameters)
kwargs.update(temp_model_kwargs)
return temperature.fuentes(poa_global, temp_air, wind_speed,
**kwargs)

def first_solar_spectral_loss(self, pw, airmass_absolute):

"""
Expand Down
35 changes: 33 additions & 2 deletions pvlib/tests/test_modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@ def pvwatts_dc_pvwatts_ac_pvsyst_temp_system():
return system


@pytest.fixture(scope="function")
def pvwatts_dc_pvwatts_ac_fuentes_temp_system():
module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003}
temp_model_params = {'noct_installed': 45}
inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95}
system = PVSystem(surface_tilt=32.2, surface_azimuth=180,
module_parameters=module_parameters,
temperature_model_parameters=temp_model_params,
inverter_parameters=inverter_parameters)
return system


@pytest.fixture(scope="function")
def system_no_aoi(cec_module_cs5p_220m, sapm_temperature_cs5p_220m,
cec_inverter_parameters):
Expand Down Expand Up @@ -317,6 +329,23 @@ def test_run_model_with_weather_faiman_temp(sapm_dc_snl_ac_system, location,
assert not mc.ac.empty


def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location,
weather, mocker):
weather['wind_speed'] = 5
weather['temp_air'] = 10
sapm_dc_snl_ac_system.temperature_model_parameters = {
'noct_installed': 45
}
mc = ModelChain(sapm_dc_snl_ac_system, location)
mc.temperature_model = 'fuentes'
m_fuentes = mocker.spy(sapm_dc_snl_ac_system, 'fuentes_celltemp')
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 not mc.ac.empty


def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker):
system = SingleAxisTracker(
module_parameters=sapm_dc_snl_ac_system.module_parameters,
Expand Down Expand Up @@ -479,14 +508,16 @@ def test_infer_spectral_model(location, sapm_dc_snl_ac_system,


@pytest.mark.parametrize('temp_model', [
'sapm_temp', 'faiman_temp', 'pvsyst_temp'])
'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp'])
def test_infer_temp_model(location, sapm_dc_snl_ac_system,
pvwatts_dc_pvwatts_ac_pvsyst_temp_system,
pvwatts_dc_pvwatts_ac_faiman_temp_system,
pvwatts_dc_pvwatts_ac_fuentes_temp_system,
temp_model):
dc_systems = {'sapm_temp': sapm_dc_snl_ac_system,
'pvsyst_temp': pvwatts_dc_pvwatts_ac_pvsyst_temp_system,
'faiman_temp': pvwatts_dc_pvwatts_ac_faiman_temp_system}
'faiman_temp': pvwatts_dc_pvwatts_ac_faiman_temp_system,
'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_temp_system}
system = dc_systems[temp_model]
mc = ModelChain(system, location,
orientation_strategy='None', aoi_model='physical',
Expand Down
44 changes: 44 additions & 0 deletions pvlib/tests/test_pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,50 @@ def test_PVSystem_faiman_celltemp(mocker):
assert_allclose(out, 56.4, atol=1)


def test_PVSystem_fuentes_celltemp(mocker):
noct_installed = 45
temp_model_params = {'noct_installed': noct_installed}
system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
spy = mocker.spy(temperature, 'fuentes')
index = pd.date_range('2019-01-01 11:00', freq='h', periods=3)
temps = pd.Series(25, index)
irrads = pd.Series(1000, index)
winds = pd.Series(1, index)
out = system.fuentes_celltemp(irrads, temps, winds)
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_series_equal(out, pd.Series([52.85, 55.85, 55.85], index,
name='tmod'))


def test_PVSystem_fuentes_celltemp_override(mocker):
# test that the surface_tilt value in the cell temp calculation can be
# overridden but defaults to the surface_tilt attribute of the PVSystem
spy = mocker.spy(temperature, 'fuentes')

noct_installed = 45
index = pd.date_range('2019-01-01 11:00', freq='h', periods=3)
temps = pd.Series(25, index)
irrads = pd.Series(1000, index)
winds = pd.Series(1, index)

# uses default value
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)
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)
assert spy.call_args[1]['surface_tilt'] == 30


def test__infer_temperature_model_params():
system = pvsystem.PVSystem(module_parameters={},
racking_model='open_rack',
Expand Down