Skip to content

Add noct_sam cell temperature model to PVSystem, ModelChain #1195

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 15 commits into from
Mar 15, 2021
Merged
2 changes: 2 additions & 0 deletions docs/sphinx/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ PV temperature models
pvsystem.PVSystem.sapm_celltemp
pvsystem.PVSystem.pvsyst_celltemp
pvsystem.PVSystem.faiman_celltemp
pvsystem.PVSystem.fuentes_celltemp
pvsystem.PVSystem.noct_sam_celltemp

Temperature Model Parameters
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
2 changes: 1 addition & 1 deletion docs/sphinx/source/whatsnew/v0.9.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Enhancements
from DC power. Use parameter ``model`` to specify which inverter model to use.
(:pull:`1147`, :issue:`998`, :pull:`1150`)
* Added :py:func:`~pvlib.temperature.noct_sam`, a cell temperature model
implemented in SAM (:pull:`1177`)
implemented in SAM (:pull:`1177`, :pull:`1195`)

Bug fixes
~~~~~~~~~
Expand Down
15 changes: 13 additions & 2 deletions pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ class ModelChain:
as the first argument to a user-defined function.

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

Expand Down Expand Up @@ -935,6 +935,8 @@ def temperature_model(self, model):
self._temperature_model = self.faiman_temp
elif model == 'fuentes':
self._temperature_model = self.fuentes_temp
elif model == 'noct_sam':
self._temperature_model = self.noct_sam_temp
else:
raise ValueError(model + ' is not a valid temperature model')
# check system.temperature_model_parameters for consistency
Expand Down Expand Up @@ -965,6 +967,8 @@ def infer_temperature_model(self):
return self.faiman_temp
elif {'noct_installed'} <= params:
return self.fuentes_temp
elif {'noct', 'eta_m_ref'} <= params:
return self.noct_sam_temp
else:
raise ValueError(f'could not infer temperature model from '
f'system.temperature_model_parameters. Check '
Expand Down Expand Up @@ -994,7 +998,11 @@ def _set_celltemp(self, model):
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)
arg_list = [poa, temp_air, wind_speed]
kwargs = {}
if model == self.system.noct_sam_celltemp:
kwargs['effective_irradiance'] = self.results.effective_irradiance
self.results.cell_temperature = model(*tuple(arg_list))
Comment on lines +1001 to +1005
Copy link
Member

Choose a reason for hiding this comment

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

@cwhanse when attempting to merge your changes into #1197 I realized that this is not correct because kwargs is not passed to the function. I had suggested

self.results.cell_temperature = model(poa, temp_air, wind_speed, **kwargs)

if you want to keep arg_list, this would also work:

self.results.cell_temperature = model(*arg_list, **kwargs)

note there is no need to cast the list to a tuple.

When fixing, we should add a test to ensure this value is passed properly.

Copy link
Member Author

Choose a reason for hiding this comment

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

My mistake, I'll fix it.

return self

def sapm_temp(self):
Expand All @@ -1009,6 +1017,9 @@ def faiman_temp(self):
def fuentes_temp(self):
return self._set_celltemp(self.system.fuentes_celltemp)

def noct_sam_temp(self):
return self._set_celltemp(self.system.noct_sam_celltemp)

@property
def losses_model(self):
return self._losses_model
Expand Down
81 changes: 78 additions & 3 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ class PVSystem:
Module parameters as defined by the SAPM, CEC, or other.

temperature_model_parameters : None, dict or Series, default None.
Temperature model parameters as defined by the SAPM, Pvsyst, or other.
Temperature model parameters as required by one of the models in
pvlib.temperature (excluding poa_global, temp_air and wind_speed).

modules_per_string: int or float, default 1
See system topology discussion above.
Expand Down Expand Up @@ -750,8 +751,6 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed):
if you want to match the PVWatts behavior, you can override it by
including a ``surface_tilt`` value in ``temperature_model_parameters``.

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.
Expand Down Expand Up @@ -781,6 +780,82 @@ def _build_kwargs_fuentes(array):
)
)

@_unwrap_single_value
def noct_sam_celltemp(self, poa_global, temp_air, wind_speed,
effective_irradiance=None):
"""
Use :py:func:`temperature.noct_sam` to calculate cell temperature.

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 at a height of 10 meters.

effective_irradiance : numeric, tuple of numeric, or None.
The irradiance that is converted to photocurrent. If None,
assumed equal to ``poa_global``. [W/m^2]

Returns
-------
temperature_cell : numeric or tuple of numeric
The modeled cell temperature [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.
"""
# 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'] = \
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
temp_model_kwargs['noct'] = \
# 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
)
)

@_unwrap_single_value
def first_solar_spectral_loss(self, pw, airmass_absolute):

Expand Down
40 changes: 37 additions & 3 deletions pvlib/tests/test_modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,18 @@ def pvwatts_dc_pvwatts_ac_fuentes_temp_system():
return system


@pytest.fixture(scope="function")
def pvwatts_dc_pvwatts_ac_noct_sam_temp_system():
module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003}
temp_model_params = {'noct': 45, 'eta_m_ref': 0.2}
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 @@ -693,6 +705,23 @@ def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location,
assert not mc.results.ac.empty


def test_run_model_with_weather_noct_sam_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 = {
Copy link
Member

Choose a reason for hiding this comment

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

#1196 will need to update for this one too

'noct': 45, 'eta_m_ref': 0.2
}
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')
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'])
assert not mc.results.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 @@ -907,7 +936,9 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays,
({'u0': 25.0, 'u1': 6.84},
ModelChain.faiman_temp),
({'noct_installed': 45},
ModelChain.fuentes_temp)])
ModelChain.fuentes_temp),
({'noct': 45, 'eta_m_ref': 0.2},
ModelChain.noct_sam_temp)])
def test_temperature_models_arrays_multi_weather(
temp_params, temp_model,
sapm_dc_snl_ac_system_same_arrays,
Expand Down Expand Up @@ -1256,16 +1287,19 @@ def test_infer_spectral_model(location, sapm_dc_snl_ac_system,


@pytest.mark.parametrize('temp_model', [
'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp'])
'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp',
'noct_sam_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,
pvwatts_dc_pvwatts_ac_noct_sam_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,
'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_temp_system}
'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_temp_system,
'noct_sam_temp': pvwatts_dc_pvwatts_ac_noct_sam_temp_system}
system = dc_systems[temp_model]
mc = ModelChain(system, location, aoi_model='physical',
spectral_model='no_loss')
Expand Down
69 changes: 61 additions & 8 deletions pvlib/tests/test_pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,11 @@ def two_array_system(pvsyst_module_params, cec_module_params):
# Need u_v to be non-zero so wind-speed changes cell temperature
# under the pvsyst model.
temperature_model['u_v'] = 1.0
# parameter for fuentes temperature model
temperature_model['noct_installed'] = 45
# parameters for noct_sam temperature model
temperature_model['noct'] = 45.
temperature_model['eta_m_ref'] = 0.2
module_params = {**pvsyst_module_params, **cec_module_params}
return pvsystem.PVSystem(
arrays=[
Expand Down Expand Up @@ -495,11 +499,53 @@ def test_PVSystem_faiman_celltemp(mocker):
assert_allclose(out, 56.4, atol=1)


def test_PVSystem_noct_celltemp(mocker):
poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45.,
0.2)
expected = 55.230790492
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)
temperature.noct_sam.assert_called_once_with(
poa_global, temp_air, wind_speed, effective_irradiance=None, 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))
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))
assert_series_equal(out, pd.Series(index=dr, data=expected))
# now use optional arguments
temp_model_params.update({'transmittance_absorptance': 0.8,
'array_height': 2,
'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.)
assert_allclose(out, expected)


def test_PVSystem_noct_celltemp_error():
poa_global, temp_air, wind_speed, eta_m_ref = (1000., 25., 1., 0.2)
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)


@pytest.mark.parametrize("celltemp",
[pvsystem.PVSystem.faiman_celltemp,
pvsystem.PVSystem.pvsyst_celltemp,
pvsystem.PVSystem.sapm_celltemp,
pvsystem.PVSystem.fuentes_celltemp])
pvsystem.PVSystem.fuentes_celltemp,
pvsystem.PVSystem.noct_sam_celltemp])
def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system):
times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3)
irrad_one = pd.Series(1000, index=times)
Expand All @@ -515,7 +561,8 @@ def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system):
[pvsystem.PVSystem.faiman_celltemp,
pvsystem.PVSystem.pvsyst_celltemp,
pvsystem.PVSystem.sapm_celltemp,
pvsystem.PVSystem.fuentes_celltemp])
pvsystem.PVSystem.fuentes_celltemp,
pvsystem.PVSystem.noct_sam_celltemp])
def test_PVSystem_multi_array_celltemp_multi_temp(celltemp, two_array_system):
times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3)
irrad = pd.Series(1000, index=times)
Expand Down Expand Up @@ -543,7 +590,8 @@ def test_PVSystem_multi_array_celltemp_multi_temp(celltemp, two_array_system):
[pvsystem.PVSystem.faiman_celltemp,
pvsystem.PVSystem.pvsyst_celltemp,
pvsystem.PVSystem.sapm_celltemp,
pvsystem.PVSystem.fuentes_celltemp])
pvsystem.PVSystem.fuentes_celltemp,
pvsystem.PVSystem.noct_sam_celltemp])
def test_PVSystem_multi_array_celltemp_multi_wind(celltemp, two_array_system):
times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3)
irrad = pd.Series(1000, index=times)
Expand Down Expand Up @@ -571,7 +619,8 @@ def test_PVSystem_multi_array_celltemp_multi_wind(celltemp, two_array_system):
[pvsystem.PVSystem.faiman_celltemp,
pvsystem.PVSystem.pvsyst_celltemp,
pvsystem.PVSystem.sapm_celltemp,
pvsystem.PVSystem.fuentes_celltemp])
pvsystem.PVSystem.fuentes_celltemp,
pvsystem.PVSystem.noct_sam_celltemp])
def test_PVSystem_multi_array_celltemp_temp_too_short(
celltemp, two_array_system):
with pytest.raises(ValueError,
Expand All @@ -583,7 +632,8 @@ def test_PVSystem_multi_array_celltemp_temp_too_short(
[pvsystem.PVSystem.faiman_celltemp,
pvsystem.PVSystem.pvsyst_celltemp,
pvsystem.PVSystem.sapm_celltemp,
pvsystem.PVSystem.fuentes_celltemp])
pvsystem.PVSystem.fuentes_celltemp,
pvsystem.PVSystem.noct_sam_celltemp])
def test_PVSystem_multi_array_celltemp_temp_too_long(
celltemp, two_array_system):
with pytest.raises(ValueError,
Expand All @@ -595,7 +645,8 @@ def test_PVSystem_multi_array_celltemp_temp_too_long(
[pvsystem.PVSystem.faiman_celltemp,
pvsystem.PVSystem.pvsyst_celltemp,
pvsystem.PVSystem.sapm_celltemp,
pvsystem.PVSystem.fuentes_celltemp])
pvsystem.PVSystem.fuentes_celltemp,
pvsystem.PVSystem.noct_sam_celltemp])
def test_PVSystem_multi_array_celltemp_wind_too_short(
celltemp, two_array_system):
with pytest.raises(ValueError,
Expand All @@ -607,7 +658,8 @@ def test_PVSystem_multi_array_celltemp_wind_too_short(
[pvsystem.PVSystem.faiman_celltemp,
pvsystem.PVSystem.pvsyst_celltemp,
pvsystem.PVSystem.sapm_celltemp,
pvsystem.PVSystem.fuentes_celltemp])
pvsystem.PVSystem.fuentes_celltemp,
pvsystem.PVSystem.noct_sam_celltemp])
def test_PVSystem_multi_array_celltemp_wind_too_long(
celltemp, two_array_system):
with pytest.raises(ValueError,
Expand All @@ -618,8 +670,9 @@ def test_PVSystem_multi_array_celltemp_wind_too_long(
@pytest.mark.parametrize("celltemp",
[pvsystem.PVSystem.faiman_celltemp,
pvsystem.PVSystem.pvsyst_celltemp,
pvsystem.PVSystem.sapm_celltemp,
pvsystem.PVSystem.fuentes_celltemp,
pvsystem.PVSystem.sapm_celltemp])
pvsystem.PVSystem.noct_sam_celltemp])
def test_PVSystem_multi_array_celltemp_poa_length_mismatch(
celltemp, two_array_system):
with pytest.raises(ValueError,
Expand Down
4 changes: 2 additions & 2 deletions pvlib/tests/test_temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,12 @@ def test_noct_sam_options():
poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45.,
0.2)
effective_irradiance = 1100.
transmittance_absorbtance = 0.8
transmittance_absorptance = 0.8
array_height = 2
mount_standoff = 2.0
result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct,
eta_m_ref, effective_irradiance,
transmittance_absorbtance, array_height,
transmittance_absorptance, array_height,
mount_standoff)
expected = 60.477703576
assert_allclose(result, expected)
Expand Down