Skip to content
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
14 changes: 12 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,10 @@ 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]
if model == self.system.noct_sam_celltemp:
arg_list += [self.results.effective_irradiance]
self.results.cell_temperature = model(*tuple(arg_list))
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
arg_list = [poa, temp_air, wind_speed]
if model == self.system.noct_sam_celltemp:
arg_list += [self.results.effective_irradiance]
self.results.cell_temperature = model(*tuple(arg_list))
kwargs = {}
if model == self.system.noct_sam_celltemp:
kwargs['effective_irradiance'] = self.results.effective_irradiance
self.results.cell_temperature = model(poa, temp_air, wind_speed, **kwargs)

Copy link
Member

Choose a reason for hiding this comment

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

I like to pass keyword arguments as keyword arguments, but I don't feel strongly about it so feel free to reject.

return self

def sapm_temp(self):
Expand All @@ -1009,6 +1016,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
79 changes: 76 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,80 @@ 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.
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
effective_irradiance : numeric, tuple of numeric or None.
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:
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 = ('Parameter noct and eta_m_ref are required.'
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
msg = ('Parameter noct and eta_m_ref are required.'
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