From ad30ffe1b0529aca83d099146a106c42de2818bf Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 25 Feb 2021 14:09:09 -0700 Subject: [PATCH 01/14] initial code for noct cell temperature function --- pvlib/temperature.py | 75 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 03871143e8..1444e44bb3 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -706,3 +706,78 @@ 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_noct(x): + return np.piecewise(x, [x < 0.5, (x >= 0.5) & (x < 1.5), + (x >= 1.5) & (x < 2.5), (x >= 2.5) & (x < 3.5), + x >= 3.5], [18., 11., 6., 2., 0.]) + + +def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, + effective_irradiance=None, transmittance_absorbtance=0.9, + array_height=1, mount_standoff=3.5): + ''' + Parameters + ---------- + poa_global : numeric + Total incident irradiance. [W/m^2] + + temp_air : numeric + Ambient dry bulb temperature. [C] + + wind_speed : numeric, default 1.0 + 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 : numeric + Nominal operating cell temperature [C], determined at conditions of + 800 W/m^2 irradiance, 20 C ambient air temperature and 1 m/s wind. + + effective_irradiance : numeric, default None. + The irradiance that is converted to photocurrent. If None, + assumed equal to poa_global. [W/m^2] + + eta_m_ref : numeric + Module external efficiency at reference conditions of 1000 W/m^2 and + 20C. Calculate as P_mp (V_mp x I_mp) divided by 1000 W/m^2. [unitless] + + 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. [unitless] + + mount_standoff : numeric, default 3.5 + Distance between array mounting and mounting surface. Use default + if system is ground-mounted. [inches] + + Returns + ------- + cell_temperature : numeric + Cell temperature. [C] + + ''' + 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() + + noct_adj = noct + _adj_noct(mount_standoff) + tau_alpha = transmittance_absorbtance * irr_ratio + + cell_temp_init = ross(poa_global, temp_air, noct_adj) + heat_loss = 1 - eta_m_ref / tau_alpha + wind_loss = 9.5 / (5.7 + 3.8 * wind_adj) + return cell_temp_init * heat_loss * wind_loss From 7ab38cd70a47a0cba1e6a56db483cb89018569fd Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 1 Mar 2021 13:22:19 -0700 Subject: [PATCH 02/14] error message, test, docs --- docs/sphinx/source/api.rst | 1 + docs/sphinx/source/whatsnew/v0.9.0.rst | 2 ++ pvlib/tests/test_temperature.py | 42 ++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 8805d199a4..8d8d55c391 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 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..7d5fe00733 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`, a cell temperature model + implemented in SAM (:pull:`1177`) Bug fixes ~~~~~~~~~ diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index f8ea3a8bc1..5424e1dcb5 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -212,3 +212,45 @@ def test_fuentes_timezone(tz): assert_series_equal(out, pd.Series([47.85, 50.85, 50.85], index=index, name='tmod')) + + +def test_noct(): + poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45., + 0.2) + expected = 54.41542289 + result = temperature.noct(poa_global, temp_air, wind_speed, noct, + eta_m_ref) + assert np.isclose(result, expected) + # test with different types + result = temperature.noct(np.array(poa_global), np.array(temp_air), + np.array(wind_speed), np.array(noct), + np.array(eta_m_ref)) + assert np.isclose(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(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_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(poa_global, temp_air, wind_speed, noct, + eta_m_ref, effective_irradiance, + transmittance_absorbtance, array_height, + mount_standoff) + expected = 58.36654459 + assert np.isclose(result, expected) + + +def test_noct_errors(): + with pytest.raises(ValueError): + temperature.noct(1000., 25., 1., 34., 0.2, array_height=3) From 5bf2eefbbf5c714b3997f4083bbb52be2bebfb90 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 1 Mar 2021 13:35:21 -0700 Subject: [PATCH 03/14] complete the docstring --- pvlib/temperature.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 1444e44bb3..ec5ff1dc4d 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -718,6 +718,10 @@ def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, effective_irradiance=None, transmittance_absorbtance=0.9, array_height=1, mount_standoff=3.5): ''' + Cell temperature model from the System Advisor Model (SAM). + + The model is described in [1], Section 10.6. + Parameters ---------- poa_global : numeric @@ -750,7 +754,7 @@ def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, 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. [unitless] + If system is elevated more than two stories, use 2. mount_standoff : numeric, default 3.5 Distance between array mounting and mounting surface. Use default @@ -761,6 +765,17 @@ def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, 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. ''' if effective_irradiance is None: irr_ratio = 1. @@ -772,7 +787,8 @@ def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, elif array_height == 2: wind_adj = 0.61 * wind_speed else: - raise ValueError() + raise ValueError( + f'array_height must be 1 or 2, {array_height} was given') noct_adj = noct + _adj_noct(mount_standoff) tau_alpha = transmittance_absorbtance * irr_ratio From 31240dbc7821b07c6b51798ec975e391853a704c Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 1 Mar 2021 17:46:19 -0700 Subject: [PATCH 04/14] correct noct function, adjust tests, add comments --- pvlib/temp_bright_sun_dev.py | 24 ++++++++++++++++++++++++ pvlib/temperature.py | 26 ++++++++++++++++---------- pvlib/tests/test_temperature.py | 10 +++++----- 3 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 pvlib/temp_bright_sun_dev.py diff --git a/pvlib/temp_bright_sun_dev.py b/pvlib/temp_bright_sun_dev.py new file mode 100644 index 0000000000..90030ec72f --- /dev/null +++ b/pvlib/temp_bright_sun_dev.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Feb 3 13:45:02 2021 + +@author: cliff +""" + +import pandas as pd +import pytz +from pvlib import solarposition + +tz = pytz.timezone('Etc/GMT+7') +dt = pd.date_range(start='02-01-2021 00:00:00', end='02-27-2021 00:00:00', + freq='1H', tz=tz) + +rs_spa = solarposition.sun_rise_set_transit_spa(dt, 35, -116) + +rs_pyephem = solarposition.sun_rise_set_transit_ephem(dt, 35, -116) + +both = pd.DataFrame(index=dt) +both['rise_spa'] = rs_spa['sunrise'] +both['rise_ephem'] = rs_pyephem['sunrise'] +both['set_spa'] = rs_spa['sunset'] +both['set_ephem'] = rs_pyephem['sunset'] diff --git a/pvlib/temperature.py b/pvlib/temperature.py index ec5ff1dc4d..abaa80f70b 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -708,19 +708,23 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, return pd.Series(tmod_array - 273.15, index=poa_global.index, name='tmod') -def _adj_noct(x): - return np.piecewise(x, [x < 0.5, (x >= 0.5) & (x < 1.5), - (x >= 1.5) & (x < 2.5), (x >= 2.5) & (x < 3.5), - x >= 3.5], [18., 11., 6., 2., 0.]) +def _adj_for_mounting_standoff(x): + # supports noct cell temperature model. + # the exact division of the range of x into intervals is not specified + # in the SAM code, except for the last interval x > 3.5. + 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(poa_global, temp_air, wind_speed, noct, eta_m_ref, effective_irradiance=None, transmittance_absorbtance=0.9, - array_height=1, mount_standoff=3.5): + array_height=1, mount_standoff=4): ''' 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. Parameters ---------- @@ -756,7 +760,7 @@ def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, 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 3.5 + mount_standoff : numeric, default 4 Distance between array mounting and mounting surface. Use default if system is ground-mounted. [inches] @@ -790,10 +794,12 @@ def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, raise ValueError( f'array_height must be 1 or 2, {array_height} was given') - noct_adj = noct + _adj_noct(mount_standoff) + noct_adj = noct + _adj_for_mounting_standoff(mount_standoff) tau_alpha = transmittance_absorbtance * irr_ratio - cell_temp_init = ross(poa_global, temp_air, noct_adj) + # [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 cell_temp_init * heat_loss * wind_loss + 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 5424e1dcb5..53fa0de9ba 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -217,15 +217,15 @@ def test_fuentes_timezone(tz): def test_noct(): poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45., 0.2) - expected = 54.41542289 + expected = 54.151119403 result = temperature.noct(poa_global, temp_air, wind_speed, noct, eta_m_ref) - assert np.isclose(result, expected) + assert assert_allclose(result, expected) # test with different types result = temperature.noct(np.array(poa_global), np.array(temp_air), np.array(wind_speed), np.array(noct), np.array(eta_m_ref)) - assert np.isclose(result, expected) + assert 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(pd.Series(index=dr, data=poa_global), @@ -247,8 +247,8 @@ def test_noct_options(): eta_m_ref, effective_irradiance, transmittance_absorbtance, array_height, mount_standoff) - expected = 58.36654459 - assert np.isclose(result, expected) + expected = 60.477703576 + assert_allclose(result, expected) def test_noct_errors(): From 077c6843c06cccfc6273d46905f96b5e1a276f0e Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 1 Mar 2021 17:46:55 -0700 Subject: [PATCH 05/14] Revert "correct noct function, adjust tests, add comments" This reverts commit 31240dbc7821b07c6b51798ec975e391853a704c. --- pvlib/temp_bright_sun_dev.py | 24 ------------------------ pvlib/temperature.py | 26 ++++++++++---------------- pvlib/tests/test_temperature.py | 10 +++++----- 3 files changed, 15 insertions(+), 45 deletions(-) delete mode 100644 pvlib/temp_bright_sun_dev.py diff --git a/pvlib/temp_bright_sun_dev.py b/pvlib/temp_bright_sun_dev.py deleted file mode 100644 index 90030ec72f..0000000000 --- a/pvlib/temp_bright_sun_dev.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Feb 3 13:45:02 2021 - -@author: cliff -""" - -import pandas as pd -import pytz -from pvlib import solarposition - -tz = pytz.timezone('Etc/GMT+7') -dt = pd.date_range(start='02-01-2021 00:00:00', end='02-27-2021 00:00:00', - freq='1H', tz=tz) - -rs_spa = solarposition.sun_rise_set_transit_spa(dt, 35, -116) - -rs_pyephem = solarposition.sun_rise_set_transit_ephem(dt, 35, -116) - -both = pd.DataFrame(index=dt) -both['rise_spa'] = rs_spa['sunrise'] -both['rise_ephem'] = rs_pyephem['sunrise'] -both['set_spa'] = rs_spa['sunset'] -both['set_ephem'] = rs_pyephem['sunset'] diff --git a/pvlib/temperature.py b/pvlib/temperature.py index abaa80f70b..ec5ff1dc4d 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -708,23 +708,19 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, return pd.Series(tmod_array - 273.15, index=poa_global.index, name='tmod') -def _adj_for_mounting_standoff(x): - # supports noct cell temperature model. - # the exact division of the range of x into intervals is not specified - # in the SAM code, except for the last interval x > 3.5. - 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 _adj_noct(x): + return np.piecewise(x, [x < 0.5, (x >= 0.5) & (x < 1.5), + (x >= 1.5) & (x < 2.5), (x >= 2.5) & (x < 3.5), + x >= 3.5], [18., 11., 6., 2., 0.]) def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, effective_irradiance=None, transmittance_absorbtance=0.9, - array_height=1, mount_standoff=4): + array_height=1, mount_standoff=3.5): ''' 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. Parameters ---------- @@ -760,7 +756,7 @@ def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, 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 + mount_standoff : numeric, default 3.5 Distance between array mounting and mounting surface. Use default if system is ground-mounted. [inches] @@ -794,12 +790,10 @@ def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, raise ValueError( f'array_height must be 1 or 2, {array_height} was given') - noct_adj = noct + _adj_for_mounting_standoff(mount_standoff) + noct_adj = noct + _adj_noct(mount_standoff) tau_alpha = transmittance_absorbtance * 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.) + cell_temp_init = ross(poa_global, temp_air, noct_adj) 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 + return cell_temp_init * heat_loss * wind_loss diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index 53fa0de9ba..5424e1dcb5 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -217,15 +217,15 @@ def test_fuentes_timezone(tz): def test_noct(): poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45., 0.2) - expected = 54.151119403 + expected = 54.41542289 result = temperature.noct(poa_global, temp_air, wind_speed, noct, eta_m_ref) - assert assert_allclose(result, expected) + assert np.isclose(result, expected) # test with different types result = temperature.noct(np.array(poa_global), np.array(temp_air), np.array(wind_speed), np.array(noct), np.array(eta_m_ref)) - assert assert_allclose(result, expected) + assert np.isclose(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(pd.Series(index=dr, data=poa_global), @@ -247,8 +247,8 @@ def test_noct_options(): eta_m_ref, effective_irradiance, transmittance_absorbtance, array_height, mount_standoff) - expected = 60.477703576 - assert_allclose(result, expected) + expected = 58.36654459 + assert np.isclose(result, expected) def test_noct_errors(): From f4d6622bd7cb4b0f41e948d52fa232ccde19ee51 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 1 Mar 2021 18:57:28 -0700 Subject: [PATCH 06/14] redo function fixes and tests --- pvlib/temperature.py | 24 +++++++++++++++--------- pvlib/tests/test_temperature.py | 4 ++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index ec5ff1dc4d..a1e2b2fe88 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -708,15 +708,19 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, return pd.Series(tmod_array - 273.15, index=poa_global.index, name='tmod') -def _adj_noct(x): - return np.piecewise(x, [x < 0.5, (x >= 0.5) & (x < 1.5), - (x >= 1.5) & (x < 2.5), (x >= 2.5) & (x < 3.5), - x >= 3.5], [18., 11., 6., 2., 0.]) +def _adj_for_mounting_standoff(x): + # supports noct cell temperature function. 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(poa_global, temp_air, wind_speed, noct, eta_m_ref, effective_irradiance=None, transmittance_absorbtance=0.9, - array_height=1, mount_standoff=3.5): + array_height=1, mount_standoff=4): ''' Cell temperature model from the System Advisor Model (SAM). @@ -756,7 +760,7 @@ def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, 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 3.5 + mount_standoff : numeric, default 4 Distance between array mounting and mounting surface. Use default if system is ground-mounted. [inches] @@ -790,10 +794,12 @@ def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, raise ValueError( f'array_height must be 1 or 2, {array_height} was given') - noct_adj = noct + _adj_noct(mount_standoff) + noct_adj = noct + _adj_for_mounting_standoff(mount_standoff) tau_alpha = transmittance_absorbtance * irr_ratio - cell_temp_init = ross(poa_global, temp_air, noct_adj) + # [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 cell_temp_init * heat_loss * wind_loss + 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 5424e1dcb5..2fd4bbf466 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -217,7 +217,7 @@ def test_fuentes_timezone(tz): def test_noct(): poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45., 0.2) - expected = 54.41542289 + expected = 54.151119403 result = temperature.noct(poa_global, temp_air, wind_speed, noct, eta_m_ref) assert np.isclose(result, expected) @@ -247,7 +247,7 @@ def test_noct_options(): eta_m_ref, effective_irradiance, transmittance_absorbtance, array_height, mount_standoff) - expected = 58.36654459 + expected = 60.477703576 assert np.isclose(result, expected) From 1bd108b5d8135f33cd11f67c4a7c3bf3c867f9db Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 1 Mar 2021 19:01:57 -0700 Subject: [PATCH 07/14] finish redo of edits --- pvlib/temperature.py | 2 +- pvlib/tests/test_temperature.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index a1e2b2fe88..39704ee836 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -724,7 +724,7 @@ def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, ''' 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. Parameters ---------- diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index 2fd4bbf466..8a9e55bd3e 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -220,12 +220,12 @@ def test_noct(): expected = 54.151119403 result = temperature.noct(poa_global, temp_air, wind_speed, noct, eta_m_ref) - assert np.isclose(result, expected) + assert assert_allclose(result, expected) # test with different types result = temperature.noct(np.array(poa_global), np.array(temp_air), np.array(wind_speed), np.array(noct), np.array(eta_m_ref)) - assert np.isclose(result, expected) + assert 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(pd.Series(index=dr, data=poa_global), @@ -248,7 +248,7 @@ def test_noct_options(): transmittance_absorbtance, array_height, mount_standoff) expected = 60.477703576 - assert np.isclose(result, expected) + assert assert_allclose(result, expected) def test_noct_errors(): From abdfccb9cdc1c6e7f6362f193c0432d59a1a0907 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 1 Mar 2021 21:28:13 -0700 Subject: [PATCH 08/14] fix tests, change name to noct_sam --- docs/sphinx/source/api.rst | 2 +- docs/sphinx/source/whatsnew/v0.9.0.rst | 2 +- pvlib/temperature.py | 6 ++-- pvlib/tests/test_temperature.py | 38 +++++++++++++------------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 8d8d55c391..7ed5ee941d 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -238,7 +238,7 @@ PV temperature models temperature.faiman temperature.fuentes temperature.ross - temperature.noct + 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 7d5fe00733..9abc4da039 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -96,7 +96,7 @@ 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`, a cell temperature model +* 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 39704ee836..09dbf09523 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -718,9 +718,9 @@ def _adj_for_mounting_standoff(x): [0., 18., 11., 6., 2., 0.]) -def noct(poa_global, temp_air, wind_speed, noct, eta_m_ref, - effective_irradiance=None, transmittance_absorbtance=0.9, - array_height=1, mount_standoff=4): +def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, + effective_irradiance=None, transmittance_absorbtance=0.9, + array_height=1, mount_standoff=4): ''' Cell temperature model from the System Advisor Model (SAM). diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index 8a9e55bd3e..d59a7146d3 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -214,43 +214,43 @@ def test_fuentes_timezone(tz): name='tmod')) -def test_noct(): +def test_noct_sam(): poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45., 0.2) - expected = 54.151119403 - result = temperature.noct(poa_global, temp_air, wind_speed, noct, - eta_m_ref) + expected = 55.230790492 + result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct, + eta_m_ref) assert assert_allclose(result, expected) # test with different types - result = temperature.noct(np.array(poa_global), np.array(temp_air), - np.array(wind_speed), np.array(noct), - np.array(eta_m_ref)) + 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 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(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) + 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_options(): +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(poa_global, temp_air, wind_speed, noct, - eta_m_ref, effective_irradiance, - transmittance_absorbtance, array_height, - mount_standoff) + 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 assert_allclose(result, expected) -def test_noct_errors(): +def test_noct_sam_errors(): with pytest.raises(ValueError): - temperature.noct(1000., 25., 1., 34., 0.2, array_height=3) + temperature.noct_sam(1000., 25., 1., 34., 0.2, array_height=3) From 442dd26058b0d050691fa41df3f245e4997c4693 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Sun, 7 Mar 2021 10:09:01 -0700 Subject: [PATCH 09/14] add test to reproduce SAM output --- pvlib/temperature.py | 29 +++++++++++++++++++++-------- pvlib/tests/test_temperature.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 09dbf09523..61eb587433 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -709,9 +709,9 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, def _adj_for_mounting_standoff(x): - # supports noct cell temperature function. The SAM code and documentation - # aren't clear on the precise intervals, the choice of < or <= here is - # pvlib's. + # 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], @@ -719,7 +719,7 @@ def _adj_for_mounting_standoff(x): def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, - effective_irradiance=None, transmittance_absorbtance=0.9, + effective_irradiance=None, transmittance_absorptance=0.9, array_height=1, mount_standoff=4): ''' Cell temperature model from the System Advisor Model (SAM). @@ -739,7 +739,7 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, factor was determined. The default value 1.0 m/s is the wind speed at module height used to determine NOCT. [m/s] - noct : numeric + 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. @@ -747,9 +747,15 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, The irradiance that is converted to photocurrent. If None, assumed equal to poa_global. [W/m^2] - eta_m_ref : numeric + eta_m_ref : float Module external efficiency at reference conditions of 1000 W/m^2 and - 20C. Calculate as P_mp (V_mp x I_mp) divided by 1000 W/m^2. [unitless] + 20C. Calculate as + + .. math:: + + \eta_m = \frac{V_{mp} I_{mp}}{A \times 1000 W/m^2} + + where A is module area [m^2]. transmittance_absorptance : numeric, default 0.9 Coefficient for combined transmittance and absorptance effects. @@ -781,6 +787,13 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, 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: @@ -795,7 +808,7 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, f'array_height must be 1 or 2, {array_height} was given') noct_adj = noct + _adj_for_mounting_standoff(mount_standoff) - tau_alpha = transmittance_absorbtance * irr_ratio + 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 diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index d59a7146d3..a2b2a8a7a9 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 @@ -236,6 +236,36 @@ def test_noct_sam(): 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_absorbtance = 0.9 + array_height = 2 + mount_standoff = 0.0 + result = temperature.noct_sam(poa_total, temp_air, wind_speed, noct, + eta_m_ref, effective_irradiance, + transmittance_absorbtance, array_height, + mount_standoff) + expected = 60.477703576 + assert assert_allclose(result, expected) + + def test_noct_sam_options(): poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45., 0.2) From 2c0b224ba3d177001475a965212744f958b7df59 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Sun, 7 Mar 2021 10:14:18 -0700 Subject: [PATCH 10/14] format, fix text asserts --- pvlib/temperature.py | 2 +- pvlib/tests/test_temperature.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 61eb587433..e77199f416 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -753,7 +753,7 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, .. math:: - \eta_m = \frac{V_{mp} I_{mp}}{A \times 1000 W/m^2} + \eta_m = \frac{V_{mp} I_{mp}}{A \times 1000 W/m^2} # noQA: W605 where A is module area [m^2]. diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index a2b2a8a7a9..a73a56cd02 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -225,7 +225,7 @@ def test_noct_sam(): 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 assert_allclose(result, expected) + 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), @@ -262,7 +262,7 @@ def test_noct_sam_against_sam(): eta_m_ref, effective_irradiance, transmittance_absorbtance, array_height, mount_standoff) - expected = 60.477703576 + expected = 43.0655 assert assert_allclose(result, expected) @@ -278,7 +278,7 @@ def test_noct_sam_options(): transmittance_absorbtance, array_height, mount_standoff) expected = 60.477703576 - assert assert_allclose(result, expected) + assert_allclose(result, expected) def test_noct_sam_errors(): From 88ebe922765d786f5aade56ccf98bd882130dfe1 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Sun, 7 Mar 2021 10:21:45 -0700 Subject: [PATCH 11/14] more assert fixes, add rtol --- pvlib/tests/test_temperature.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index a73a56cd02..887f37790a 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -220,7 +220,7 @@ def test_noct_sam(): expected = 55.230790492 result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref) - assert assert_allclose(result, expected) + 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), @@ -263,7 +263,8 @@ def test_noct_sam_against_sam(): transmittance_absorbtance, array_height, mount_standoff) expected = 43.0655 - assert assert_allclose(result, expected) + # rtol from limited SAM output precision + assert_allclose(result, expected, rtol=1e-5) def test_noct_sam_options(): From 74494f394ac7d509a7c23935569449ccdcacea22 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Sun, 7 Mar 2021 11:22:26 -0700 Subject: [PATCH 12/14] fix tes --- pvlib/temperature.py | 3 +-- pvlib/tests/test_temperature.py | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index e77199f416..7221ec1038 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -750,10 +750,9 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, eta_m_ref : float Module external efficiency 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} # noQA: W605 + \eta_{m} = \frac{V_{mp} I_{mp}}{A \times 1000 W/m^2} # noQA: W605 where A is module area [m^2]. diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index 887f37790a..1ce2e69a47 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -255,12 +255,12 @@ def test_noct_sam_against_sam(): air_mass *= np.exp(-0.0001184 * elev) f1 = np.polyval(a, air_mass) effective_irradiance = f1 * poa_total_after_refl - transmittance_absorbtance = 0.9 - array_height = 2 - mount_standoff = 0.0 + 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_absorbtance, array_height, + transmittance_absorptance, array_height, mount_standoff) expected = 43.0655 # rtol from limited SAM output precision From 9e3c6f3fb93e40873890745e7245cb6659ec11f4 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Sun, 7 Mar 2021 11:24:36 -0700 Subject: [PATCH 13/14] docstring format --- pvlib/temperature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 7221ec1038..e80c5a4c0f 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -721,7 +721,7 @@ def _adj_for_mounting_standoff(x): 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. From 979734bf9852839a8ca8e0ba412add111fb261cd Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Sun, 7 Mar 2021 18:50:53 -0700 Subject: [PATCH 14/14] fixes from review --- pvlib/temperature.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pvlib/temperature.py b/pvlib/temperature.py index e80c5a4c0f..7ff063f64d 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -734,7 +734,7 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, temp_air : numeric Ambient dry bulb temperature. [C] - wind_speed : numeric, default 1.0 + 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] @@ -743,19 +743,16 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref, 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] - eta_m_ref : float - Module external efficiency 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} # noQA: W605 - - where A is module area [m^2]. - transmittance_absorptance : numeric, default 0.9 Coefficient for combined transmittance and absorptance effects. [unitless]