Skip to content
Open
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
12 changes: 12 additions & 0 deletions docs/sphinx/source/whatsnew/v0.16.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,21 @@ Breaking Changes

Deprecations
~~~~~~~~~~~~
* The signature of :py:func:`pvlib.temperature.noct_sam` has changed: the
first parameter is now ``effective_irradiance``, ``poa_global`` is removed,
and the remaining optional parameters are keyword-only. Calls using the
previous signature still work but emit a deprecation warning, and support
for them will be removed in v0.18.0. (:issue:`1462`, :pull:`2905`)


Bug fixes
~~~~~~~~~
* :py:func:`pvlib.temperature.noct_sam` now calculates cell temperature from
``effective_irradiance`` directly, matching the corrected SAM implementation
(NREL/ssc#1233). Previously the ratio of effective to total irradiance was
folded into ``transmittance_absorptance``, so reflection losses did not
reduce the modeled temperature of an open-circuit module, and small ratios
gave cell temperatures below ambient. (:issue:`1462`, :pull:`2905`)
* :py:func:`pvlib.tracking.calc_surface_orientation` returns scalar when input
is scalar (:issue:`2881`, :pull:`2882`)

Expand Down Expand Up @@ -62,6 +73,7 @@ Contributors
* Chen Yi (:ghuser:`chenyi6758-source`)
* Rajiv Daxini (:ghuser:`rdaxini`)
* jason-writer (:ghuser:`jason-writer`)
* :ghuser:`asystemoffields`



15 changes: 11 additions & 4 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,9 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model,

effective_irradiance : numeric or tuple of numeric, optional
The irradiance that is converted to photocurrent in W/m^2.
Only used for some models.
Only used for some models. For ``'noct_sam'``, the cell
temperature is calculated from ``effective_irradiance`` when it
is provided and from ``poa_global`` otherwise.

longwave_down: numeric or tuple of numeric, optional
Downwelling long-wave radiation from the sky, measured on a
Expand Down Expand Up @@ -1368,7 +1370,9 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model,

effective_irradiance : numeric, optional
The irradiance that is converted to photocurrent in W/m^2.
Only used for some models.
Only used for some models. For ``'noct_sam'``, the cell
temperature is calculated from ``effective_irradiance`` when it
is provided and from ``poa_global`` otherwise.

longwave_down: numeric, optional
Downwelling long-wave radiation from the sky, measured on a
Expand Down Expand Up @@ -1432,8 +1436,11 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model,
if self.mount.module_height is not None:
optional['module_height'] = self.mount.module_height
elif model == 'noct_sam':
func = functools.partial(temperature.noct_sam,
effective_irradiance=effective_irradiance)
func = temperature.noct_sam
# noct_sam takes the irradiance absorbed by the module; fall
# back to poa_global when effective_irradiance is not provided
if effective_irradiance is not None:
poa_global = effective_irradiance
required = _build_tcell_args(['noct', 'module_efficiency'])
optional = _build_kwargs(['transmittance_absorptance',
'array_height', 'mount_standoff'],
Expand Down
116 changes: 88 additions & 28 deletions pvlib/temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import scipy
import scipy.constants
import warnings
import functools
from pvlib._deprecation import pvlibDeprecationWarning


TEMPERATURE_MODEL_PARAMETERS = {
Expand Down Expand Up @@ -937,18 +939,76 @@ def _adj_for_mounting_standoff(x):
[0., 18., 11., 6., 2., 0.])


def noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency,
effective_irradiance=None, transmittance_absorptance=0.9,
def _noct_sam_legacy_signature(func):
# Deprecation shim for the signature of noct_sam before v0.16.2:
# noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency,
# effective_irradiance=None, transmittance_absorptance=0.9,
# array_height=1, mount_standoff=4)
# Remove in v0.18.0, together with the tests decorated with
# @fail_on_pvlib_version('0.18.0') in tests/test_temperature.py.
legacy_optional = ('transmittance_absorptance', 'array_height',
'mount_standoff')
preamble = (
"The signature of pvlib.temperature.noct_sam changed in pvlib 0.16.2:"
" the first parameter is now effective_irradiance, poa_global was"
" removed, and the optional parameters are keyword-only. ")
epilogue = " Support for the previous signature will be removed in 0.18.0."

def _warn(detail):
warnings.warn(preamble + detail + epilogue, pvlibDeprecationWarning,
stacklevel=3)

@functools.wraps(func)
def wrapper(*args, **kwargs):
poa_global = kwargs.pop('poa_global', None)
has_eff_kw = 'effective_irradiance' in kwargs
eff_kw = kwargs.pop('effective_irradiance', None)
if len(args) > 9:
raise TypeError(f"noct_sam() takes at most 9 positional arguments"
f" ({len(args)} given)")
if len(args) > 5:
# legacy positional effective_irradiance (and options)
_warn("Pass effective_irradiance as the first argument, and"
" transmittance_absorptance, array_height and"
" mount_standoff by keyword.")
kwargs.update(zip(legacy_optional, args[6:]))
args = (args[5],) + tuple(args[1:5])
elif eff_kw is not None:
if args:
_warn("The positional poa_global argument is ignored; pass"
" effective_irradiance as the first argument.")
args = (eff_kw,) + tuple(args[1:])
else:
if poa_global is not None:
_warn("The poa_global argument is ignored.")
kwargs['effective_irradiance'] = eff_kw
elif poa_global is not None:
_warn("poa_global was passed in place of effective_irradiance.")
kwargs['effective_irradiance'] = poa_global
elif has_eff_kw:
_warn("effective_irradiance=None is not accepted; the first"
" argument is used as the effective irradiance.")
return func(*args, **kwargs)

return wrapper


@_noct_sam_legacy_signature
def noct_sam(effective_irradiance, temp_air, wind_speed, noct,
module_efficiency, *, transmittance_absorptance=0.9,
array_height=1, mount_standoff=4):
r'''
r"""
Cell temperature model from the System Advisor Model (SAM).

The model is described in [1]_, Section 10.6.
The model is described in [1]_, Section 10.6, and is implemented as in
SAM [2]_.

Parameters
----------
poa_global : numeric
Total incident irradiance. [W/m^2]
effective_irradiance : numeric
Irradiance absorbed by the module, i.e., plane-of-array irradiance
after reflection and spectral losses, as converted to photocurrent.
[W/m^2]

temp_air : numeric
Ambient dry bulb temperature. [C]
Expand All @@ -968,10 +1028,6 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency,
:math:`\eta_{m} = \frac{V_{mp} I_{mp}}{A \times 1000 W/m^2}`
where A is module area [m^2].

effective_irradiance : numeric, optional
The irradiance that is converted to photocurrent. If not specified,
assumed equal to poa_global. [W/m^2]

transmittance_absorptance : numeric, default 0.9
Coefficient for combined transmittance and absorptance effects.
[unitless]
Expand All @@ -995,25 +1051,27 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency,
ValueError
If array_height is an invalid value (must be 1 or 2).

Notes
-----
.. versionchanged:: 0.16.2
``effective_irradiance`` replaced ``poa_global`` as the first
parameter, and the optional parameters became keyword-only.
Previously, the ratio of ``effective_irradiance`` to ``poa_global``
was folded into ``transmittance_absorptance``, so reflection losses
did not reduce the modeled temperature of an open-circuit module, and
small ratios gave cell temperatures below ambient. SAM made the same
correction in [2]_. The previous signature is deprecated and will be
removed in 0.18.0.

References
----------
.. [1] Gilman, P., Dobos, A., DiOrio, N., Freeman, J., Janzou, S.,
Ryberg, D., 2018, "SAM Photovoltaic Model Technical Reference
Update", National Renewable Energy Laboratory Report
NREL/TP-6A20-67399.
'''
# in [1] the denominator for irr_ratio isn't precisely clear. From
# reproducing output of the SAM function noct_celltemp_t, we determined
# that:
# - G_total (SAM) is broadband plane-of-array irradiance before
# reflections. Equivalent to pvlib variable poa_global
# - Geff_total (SAM) is POA irradiance after reflections and
# adjustment for spectrum. Equivalent to effective_irradiance
if effective_irradiance is None:
irr_ratio = 1.
else:
irr_ratio = effective_irradiance / poa_global

.. [2] NREL/ssc pull request 1233, "Noct taualpha", merged 2024-11-04.
https://github.com/NREL/ssc/pull/1233
"""
if array_height == 1:
wind_adj = 0.51 * wind_speed
elif array_height == 2:
Expand All @@ -1023,12 +1081,14 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency,
f'array_height must be 1 or 2, {array_height} was given')

noct_adj = noct + _adj_for_mounting_standoff(mount_standoff)
tau_alpha = transmittance_absorptance * irr_ratio

# [1] Eq. 10.37 isn't clear on exactly what "G" is. SAM SSC code uses
# poa_global where G appears
cell_temp_init = poa_global / 800. * (noct_adj - 20.)
heat_loss = 1 - module_efficiency / tau_alpha
# [1] Eq. 10.37 isn't explicit about which irradiance "G" is. SAM
# (noct_celltemp_t in shared/lib_cec6par.cpp) used the total POA
# irradiance and scaled tau_alpha by the ratio of effective to total
# irradiance until NREL/ssc#1233, which switched to the effective
# irradiance and removed the scaling. pvlib follows SAM.
cell_temp_init = effective_irradiance / 800. * (noct_adj - 20.)
heat_loss = 1 - module_efficiency / transmittance_absorptance
wind_loss = 9.5 / (5.7 + 3.8 * wind_adj)
return temp_air + cell_temp_init * heat_loss * wind_loss

Expand Down
28 changes: 25 additions & 3 deletions tests/test_pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,8 +621,7 @@ def test_PVSystem_noct_celltemp(mocker):
out = system.get_cell_temperature(poa_global, temp_air, wind_speed,
model='noct_sam')
temperature.noct_sam.assert_called_once_with(
poa_global, temp_air, wind_speed, noct, module_efficiency,
effective_irradiance=None)
poa_global, temp_air, wind_speed, noct, module_efficiency)
assert_allclose(out, expected)
# different types
out = system.get_cell_temperature(np.array(poa_global), np.array(temp_air),
Expand All @@ -639,14 +638,37 @@ def test_PVSystem_noct_celltemp(mocker):
temp_model_params.update({'transmittance_absorptance': 0.8,
'array_height': 2,
'mount_standoff': 2.0})
expected = 60.477703576
# effective_irradiance is used in place of poa_global
expected = 62.877665877
system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
out = system.get_cell_temperature(poa_global, temp_air, wind_speed,
effective_irradiance=1100.,
model='noct_sam')
assert_allclose(out, expected)


def test_PVSystem_noct_celltemp_against_sam():
# poa_global and effective_irradiance both given, as ModelChain does.
# Reference is SAM's cell temperature for hour 11 of
# pvlib/data/723170TYA.CSV; see test_temperature.test_noct_sam_against_sam
# for the SAM configuration. poa_global is SAM's POA irradiance before
# reflection (subarray1_poa_shaded_soiled); effective_irradiance is the
# POA irradiance after reflection times the air mass modifier.
zenith = 60.4221
air_mass = 1. / (np.cos(np.radians(zenith))
+ 0.5057 * (96.080 - zenith)**-1.634)
air_mass *= np.exp(-0.0001184 * 273.)
a = np.flipud([0.918093, 0.086257, -0.024459, 0.002816, -0.000126])
effective_irradiance = 265.4939 * np.polyval(a, air_mass)
temp_model_params = {'noct': 43.1,
'module_efficiency': 12.82 * 41.4 / (1000. * 2.58)}
system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
out = system.get_cell_temperature(
272.9785, 11.7, 5.2, model='noct_sam',
effective_irradiance=effective_irradiance)
assert_allclose(out, 15.305202, atol=1e-5)


def test_PVSystem_noct_celltemp_error():
poa_global, temp_air, wind_speed, module_efficiency = (1000., 25., 1., 0.2)
temp_model_params = {'module_efficiency': module_efficiency}
Expand Down
Loading