diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 8805d199a4..7ed5ee941d 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -238,6 +238,7 @@ PV temperature models temperature.faiman temperature.fuentes temperature.ross + temperature.noct_sam pvsystem.PVSystem.sapm_celltemp pvsystem.PVSystem.pvsyst_celltemp pvsystem.PVSystem.faiman_celltemp diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 81e7a0c60b..9abc4da039 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -96,6 +96,8 @@ Enhancements * :py:meth:`~pvlib.pvsystem.PVSystem.get_ac` is added to calculate AC power 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`) Bug fixes ~~~~~~~~~ diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 03871143e8..7ff063f64d 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -706,3 +706,109 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, sun0 = sun return pd.Series(tmod_array - 273.15, index=poa_global.index, name='tmod') + + +def _adj_for_mounting_standoff(x): + # supports noct cell temperature function. Except for x > 3.5, the SAM code + # and documentation aren't clear on the precise intervals. The choice of + # < or <= here is pvlib's. + return np.piecewise(x, [x <= 0, (x > 0) & (x < 0.5), + (x >= 0.5) & (x < 1.5), (x >= 1.5) & (x < 2.5), + (x >= 2.5) & (x <= 3.5), x > 3.5], + [0., 18., 11., 6., 2., 0.]) + + +def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, + effective_irradiance=None, transmittance_absorptance=0.9, + array_height=1, mount_standoff=4): + r''' + Cell temperature model from the System Advisor Model (SAM). + + The model is described in [1]_, Section 10.6. + + Parameters + ---------- + poa_global : numeric + Total incident irradiance. [W/m^2] + + temp_air : numeric + Ambient dry bulb temperature. [C] + + wind_speed : numeric + Wind speed in m/s measured at the same height for which the wind loss + factor was determined. The default value 1.0 m/s is the wind + speed at module height used to determine NOCT. [m/s] + + noct : float + Nominal operating cell temperature [C], determined at conditions of + 800 W/m^2 irradiance, 20 C ambient air temperature and 1 m/s wind. + + eta_m_ref : float + Module external efficiency [unitless] at reference conditions of + 1000 W/m^2 and 20C. Calculate as + :math:`\eta_{m} = \frac{V_{mp} I_{mp}}{A \times 1000 W/m^2}` + where A is module area [m^2]. + + effective_irradiance : numeric, default None. + The irradiance that is converted to photocurrent. If None, + assumed equal to poa_global. [W/m^2] + + transmittance_absorptance : numeric, default 0.9 + Coefficient for combined transmittance and absorptance effects. + [unitless] + + array_height : int, default 1 + Height of array above ground in stories (one story is about 3m). Must + be either 1 or 2. For systems elevated less than one story, use 1. + If system is elevated more than two stories, use 2. + + mount_standoff : numeric, default 4 + Distance between array mounting and mounting surface. Use default + if system is ground-mounted. [inches] + + Returns + ------- + cell_temperature : numeric + Cell temperature. [C] + + Raises + ------ + ValueError + If array_height is an invalid value (must be 1 or 2). + + 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 + + if array_height == 1: + wind_adj = 0.51 * wind_speed + elif array_height == 2: + wind_adj = 0.61 * wind_speed + else: + raise ValueError( + 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 - eta_m_ref / tau_alpha + wind_loss = 9.5 / (5.7 + 3.8 * wind_adj) + return temp_air + cell_temp_init * heat_loss * wind_loss diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index f8ea3a8bc1..1ce2e69a47 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -5,7 +5,7 @@ from conftest import DATA_DIR, assert_series_equal from numpy.testing import assert_allclose -from pvlib import temperature +from pvlib import temperature, tools @pytest.fixture @@ -212,3 +212,76 @@ def test_fuentes_timezone(tz): assert_series_equal(out, pd.Series([47.85, 50.85, 50.85], index=index, name='tmod')) + + +def test_noct_sam(): + poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45., + 0.2) + expected = 55.230790492 + result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct, + eta_m_ref) + assert_allclose(result, expected) + # test with different types + result = temperature.noct_sam(np.array(poa_global), np.array(temp_air), + np.array(wind_speed), np.array(noct), + np.array(eta_m_ref)) + assert_allclose(result, expected) + dr = pd.date_range(start='2020-01-01 12:00:00', end='2020-01-01 13:00:00', + freq='1H') + result = temperature.noct_sam(pd.Series(index=dr, data=poa_global), + pd.Series(index=dr, data=temp_air), + pd.Series(index=dr, data=wind_speed), + pd.Series(index=dr, data=noct), + eta_m_ref) + assert_series_equal(result, pd.Series(index=dr, data=expected)) + + +def test_noct_sam_against_sam(): + # test is constructed to reproduce output from SAM v2020.11.29. + # SAM calculation is the default Detailed PV System model (CEC diode model, + # NOCT cell temperature model), with the only change being the soiling + # loss is set to 0. Weather input is TMY3 for Phoenix AZ. + # Values are taken from the Jan 1 12:00:00 timestamp. + poa_total, temp_air, wind_speed, noct, eta_m_ref = ( + 860.673, 25, 3, 46.4, 0.20551) + poa_total_after_refl = 851.458 # from SAM output + # compute effective irradiance + # spectral loss coefficients fixed in lib_cec6par.cpp + a = np.flipud([0.918093, 0.086257, -0.024459, 0.002816, -0.000126]) + # reproduce SAM air mass calculation + zen = 56.4284 + elev = 358 + air_mass = 1. / (tools.cosd(zen) + 0.5057 * (96.080 - zen)**-1.634) + air_mass *= np.exp(-0.0001184 * elev) + f1 = np.polyval(a, air_mass) + effective_irradiance = f1 * poa_total_after_refl + transmittance_absorptance = 0.9 + array_height = 1 + mount_standoff = 4.0 + result = temperature.noct_sam(poa_total, temp_air, wind_speed, noct, + eta_m_ref, effective_irradiance, + transmittance_absorptance, array_height, + mount_standoff) + expected = 43.0655 + # rtol from limited SAM output precision + assert_allclose(result, expected, rtol=1e-5) + + +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 + 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, + mount_standoff) + expected = 60.477703576 + assert_allclose(result, expected) + + +def test_noct_sam_errors(): + with pytest.raises(ValueError): + temperature.noct_sam(1000., 25., 1., 34., 0.2, array_height=3)