From 4911f162254abf1de2293c351e394414cf98ce1f Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 29 Sep 2022 17:02:43 -0400 Subject: [PATCH 01/28] initial code from Yu Xie --- pvlib/iam.py | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/pvlib/iam.py b/pvlib/iam.py index a8592f4036..1871b3dc76 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -748,3 +748,124 @@ def marion_integrate(function, surface_tilt, region, num=None): Fd = pd.Series(Fd, surface_tilt.index) return Fd + + +def fedis(aoi, surface_tilt, rfn, rfnt=1.4585): + ''' + The “Fresnel Equations” for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS) + is an analytical solution of diffuse transmission based on the rigorous integration of + an alternate form of the Fresnel equations. The approach leads to a simple yet accurate + relative transmittance model that reconciles the solar energy sensed by pyranometers and PV panels. + + Parameters + ---------- + aoi : Angle of incidence in degrees. + + rfnt: the refractive index of the pyranometer cover + For a fused silica dome over a CMP22, the rfnt is 1.4585 + + rfn: the refractive index of the PV cover + The suggested value is 1.5 + + surface_tilt : numeric + Surface tilt angles in decimal degrees. + The tilt angle is defined as degrees from horizontal + (e.g. surface facing up = 0, surface facing horizon = 90). + + Returns + ------- + cd : the incidence angle modifier (IAM) for direct radiation + cuk: the IAM for diffuse radiation from the sky + cug: the IAM for diffuse radiation from the ground reflection + + Usage + ---------- + The solar energy received by a PV can be given by + F = cd*Fd + cuk*Fuk + cug*Fug + Fd, Fuk, and Fug are the POA irradiances, observed by the pyranometer, + that are associated with direct radiation, diffuse radiation from the sky, and + diffuse radiation from ground reflection, respectively. + An example can be found in Eq.(1-2) from the reference + + rfnt = 1.4585 + rfn = np.arange(nPV) + aoi = np.arange(n) + surface_tilt = np.arange(n) + cd, cuk, cug = FEDIS(aoi, surface_tilt, rfn, rfnt ) + Will return 2 dimensional arrays (nPV, n) + n represents the number of total scenarios + nPV represents the number of PV cover types + + Example + ------- + belta = np.arange(100)*0.9 + rfn = np.array([1.3, 2.0]) + theta0p = belta*np.pi/180.0 + aoi = theta0p*180.0/np.pi + surface_tilt = belta + + cd, cuk, cug = FEDIS(aoi, surface_tilt, rfn, rfnt ) + print( cd[0][:] ) + print( cuk[0][:] ) + print( cug[0][:] ) + This generate the results of Fig.3 in the reference + + Reference + ---------- + Xie, Y., M. Sengupta, A. Habte, A. Andreas, The "Fresnel Equations for Diffuse + radiation on Inclined photovoltaic Surfaces (FEDIS), Rew. Sus. En. Rev., 161, 112362 + + ''' + + surface_tilt[surface_tilt<0.01] = 0.01 + aoi[aoi==0.0] = 0.001 + + r0 = ( (rfnt-1.0)/(rfnt+1.0) )**2.0 + aoi = aoi*np.pi/180.0 + theta0tp = [] + for rfn1 in rfn: + theta0tp1 = np.arcsin( np.sin(aoi)/rfn1 ) + theta0tp.append( theta0tp1 ) + theta0tp = np.asarray(theta0tp, dtype=np.float) + + rd = [ ] + for i in range( rfn.shape[0] ): + rd1 = ( np.sin(aoi-theta0tp[i,:])/np.sin(aoi+theta0tp[i,:]) )**2.0 + \ + ( np.tan(aoi-theta0tp[i,:])/np.tan(aoi+theta0tp[i,:]) )**2.0 + rd1 = rd1*0.5 + rd.append( rd1 ) + rd = np.asarray(rd, dtype=np.float) + + cd = [ ] + for j in range( surface_tilt.shape[0] ): + cd1 = (1.0-rd[:,j])/(1.0-r0) + cd.append( cd1 ) + cd = np.asarray(cd, dtype=np.float) + cd = cd.T + + cuk = [ ] + cug = [ ] + w = 2.77526e-09 +3.74953*rfn -5.18727*rfn**2.0 +3.41186*rfn**3.0 -1.08794*rfn**4.0 +0.136060*rfn**5.0 + w = w*(rfn*(1.0+rfnt)**2.0)/( rfnt*(1.0+rfn)**2.0 ) + + for i in range( rfn.shape[0] ): + w1 = w[i] + cuk1 = 30.0*np.pi/7.0 - (160.0/21.0)*(surface_tilt*np.pi/180.0) - (10.0*np.pi/3.0)*np.cos(surface_tilt*np.pi/180.0) + \ + (160.0/21.0)*np.cos(surface_tilt*np.pi/180.0)*np.sin(surface_tilt*np.pi/180.0) \ + - (5.0*np.pi/3.0)*np.cos(surface_tilt*np.pi/180.0)*( np.sin(surface_tilt*np.pi/180.0) )**2.0 \ + + (20.0/7.0)*np.cos(surface_tilt*np.pi/180.0)*( np.sin(surface_tilt*np.pi/180.0) )**3.0 \ + - (5.0*np.pi/16.0)*np.cos(surface_tilt*np.pi/180.0)*( np.sin(surface_tilt*np.pi/180.0) )**4.0 \ + + (16.0/105.0)*np.cos(surface_tilt*np.pi/180.0)*( np.sin(surface_tilt*np.pi/180.0) )**5.0 + cuk1 = cuk1*w1*2.0/(np.pi*(1.0+np.cos(surface_tilt*np.pi/180.0))) + cuk.append( cuk1 ) + cug1 = 40.0*w1/(21.0*(1.0-np.cos(surface_tilt*np.pi/180.0))) - \ + (1.0+np.cos(surface_tilt*np.pi/180.0))*cuk1/(1.0-np.cos(surface_tilt*np.pi/180.0)) + cug.append( cug1 ) + cuk = np.asarray( cuk, dtype=np.float) + cug = np.asarray( cug, dtype=np.float) + + return cd, cuk, cug + + + + From d2c0758775bf28960e00b70e9188e722aa98bd52 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 29 Sep 2022 18:34:18 -0400 Subject: [PATCH 02/28] edits for pvlib style --- pvlib/iam.py | 184 ++++++++++++++++++++++----------------------------- 1 file changed, 78 insertions(+), 106 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 1871b3dc76..28b84b5a0f 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -750,122 +750,94 @@ def marion_integrate(function, surface_tilt, region, num=None): return Fd -def fedis(aoi, surface_tilt, rfn, rfnt=1.4585): - ''' - The “Fresnel Equations” for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS) - is an analytical solution of diffuse transmission based on the rigorous integration of - an alternate form of the Fresnel equations. The approach leads to a simple yet accurate - relative transmittance model that reconciles the solar energy sensed by pyranometers and PV panels. +def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): + """ + Determine the incidence angle modifiers (iam) for direct, diffuse sky, + and ground-reflected radiation using the FEDIS transmittance model. + + The "Fresnel Equations" for Diffuse radiation on Inclined photovoltaic + Surfaces (FEDIS) [1]_ is an analytical solution of diffuse transmission + based on the rigorous integration of an alternate form of the + Fresnel equations. The approach leads to a simple yet accurate + relative transmittance model that reconciles the solar energy + sensed by pyranometers and PV panels. Parameters ---------- - aoi : Angle of incidence in degrees. + aoi : numeric + Angle of incidence. [degrees] - rfnt: the refractive index of the pyranometer cover - For a fused silica dome over a CMP22, the rfnt is 1.4585 + surface_tilt : numeric + Surface tilt angle measured from horizontal (e.g. surface facing + up = 0, surface facing horizon = 90). [degrees] - rfn: the refractive index of the PV cover - The suggested value is 1.5 + n : float, default 1.5 + Refractive index of the PV cover. The default value of 1.5 + was used for an IMT reference cell in [1]_. [unitless] - surface_tilt : numeric - Surface tilt angles in decimal degrees. - The tilt angle is defined as degrees from horizontal - (e.g. surface facing up = 0, surface facing horizon = 90). + n_ref : float, default 1.4585 + Refractive index of the pyranometer cover. The default value + was used for a fused silica dome over a CMP22 in [1]_. Returns ------- - cd : the incidence angle modifier (IAM) for direct radiation - cuk: the IAM for diffuse radiation from the sky - cug: the IAM for diffuse radiation from the ground reflection - - Usage - ---------- - The solar energy received by a PV can be given by - F = cd*Fd + cuk*Fuk + cug*Fug - Fd, Fuk, and Fug are the POA irradiances, observed by the pyranometer, - that are associated with direct radiation, diffuse radiation from the sky, and - diffuse radiation from ground reflection, respectively. - An example can be found in Eq.(1-2) from the reference - - rfnt = 1.4585 - rfn = np.arange(nPV) - aoi = np.arange(n) - surface_tilt = np.arange(n) - cd, cuk, cug = FEDIS(aoi, surface_tilt, rfn, rfnt ) - Will return 2 dimensional arrays (nPV, n) - n represents the number of total scenarios - nPV represents the number of PV cover types - - Example - ------- - belta = np.arange(100)*0.9 - rfn = np.array([1.3, 2.0]) - theta0p = belta*np.pi/180.0 - aoi = theta0p*180.0/np.pi - surface_tilt = belta - - cd, cuk, cug = FEDIS(aoi, surface_tilt, rfn, rfnt ) - print( cd[0][:] ) - print( cuk[0][:] ) - print( cug[0][:] ) - This generate the results of Fig.3 in the reference - - Reference - ---------- - Xie, Y., M. Sengupta, A. Habte, A. Andreas, The "Fresnel Equations for Diffuse - radiation on Inclined photovoltaic Surfaces (FEDIS), Rew. Sus. En. Rev., 161, 112362 - - ''' - - surface_tilt[surface_tilt<0.01] = 0.01 - aoi[aoi==0.0] = 0.001 - - r0 = ( (rfnt-1.0)/(rfnt+1.0) )**2.0 - aoi = aoi*np.pi/180.0 - theta0tp = [] - for rfn1 in rfn: - theta0tp1 = np.arcsin( np.sin(aoi)/rfn1 ) - theta0tp.append( theta0tp1 ) - theta0tp = np.asarray(theta0tp, dtype=np.float) - - rd = [ ] - for i in range( rfn.shape[0] ): - rd1 = ( np.sin(aoi-theta0tp[i,:])/np.sin(aoi+theta0tp[i,:]) )**2.0 + \ - ( np.tan(aoi-theta0tp[i,:])/np.tan(aoi+theta0tp[i,:]) )**2.0 - rd1 = rd1*0.5 - rd.append( rd1 ) - rd = np.asarray(rd, dtype=np.float) - - cd = [ ] - for j in range( surface_tilt.shape[0] ): - cd1 = (1.0-rd[:,j])/(1.0-r0) - cd.append( cd1 ) - cd = np.asarray(cd, dtype=np.float) - cd = cd.T - - cuk = [ ] - cug = [ ] - w = 2.77526e-09 +3.74953*rfn -5.18727*rfn**2.0 +3.41186*rfn**3.0 -1.08794*rfn**4.0 +0.136060*rfn**5.0 - w = w*(rfn*(1.0+rfnt)**2.0)/( rfnt*(1.0+rfn)**2.0 ) - - for i in range( rfn.shape[0] ): - w1 = w[i] - cuk1 = 30.0*np.pi/7.0 - (160.0/21.0)*(surface_tilt*np.pi/180.0) - (10.0*np.pi/3.0)*np.cos(surface_tilt*np.pi/180.0) + \ - (160.0/21.0)*np.cos(surface_tilt*np.pi/180.0)*np.sin(surface_tilt*np.pi/180.0) \ - - (5.0*np.pi/3.0)*np.cos(surface_tilt*np.pi/180.0)*( np.sin(surface_tilt*np.pi/180.0) )**2.0 \ - + (20.0/7.0)*np.cos(surface_tilt*np.pi/180.0)*( np.sin(surface_tilt*np.pi/180.0) )**3.0 \ - - (5.0*np.pi/16.0)*np.cos(surface_tilt*np.pi/180.0)*( np.sin(surface_tilt*np.pi/180.0) )**4.0 \ - + (16.0/105.0)*np.cos(surface_tilt*np.pi/180.0)*( np.sin(surface_tilt*np.pi/180.0) )**5.0 - cuk1 = cuk1*w1*2.0/(np.pi*(1.0+np.cos(surface_tilt*np.pi/180.0))) - cuk.append( cuk1 ) - cug1 = 40.0*w1/(21.0*(1.0-np.cos(surface_tilt*np.pi/180.0))) - \ - (1.0+np.cos(surface_tilt*np.pi/180.0))*cuk1/(1.0-np.cos(surface_tilt*np.pi/180.0)) - cug.append( cug1 ) - cuk = np.asarray( cuk, dtype=np.float) - cug = np.asarray( cug, dtype=np.float) - - return cd, cuk, cug + iam : dict + IAM values for each type of irradiance: + * 'direct': radiation from the solar disc + * 'sky': radiation from the sky dome (zenith <= 90) + * 'ground': radiation reflected from the ground (zenith >= 90) + References + ---------- + .. [1] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations' + for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)", + Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022. + :doi:`10.1016/j.rser.2022.112362` + """ + # avoid undefined results for horizontal or upside-down surfaces + zeroang = 1e-06 + surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt) + # similar for AOI > 90 + aoi = np.where(aoi <= 90, zeroang, aoi) + + # angle between module normal and refracted ray: + theta_0tp = asind(sind(aoi) / n) # Eq 3c + + # reflectance of direct radiation on PV cover: + sin_term = sind(aoi - theta_0tp)**2 / sind(aoi + theta_0tp)**2 / 2 + tan_term = tand(aoi - theta_0tp)**2 / tand(aoi + theta_0tp)**2 / 2 + rd = sin_term + tan_term # Eq 3b + + # reflectance on pyranometer cover: + r0 = ((n_ref-1.0)/(n_ref+1.0))**2.0 # Eq 3e + + # relative transmittance of direct radiation by PV cover: + cd = (1 - rd) / (1 - r0) # Eq 3a + + # weighting function + term1 = n*(n_ref+1)**2 / (n_ref*(n+1)**2) + polycoeffs = [2.77526e-09, 3.74953, -5.18727, 3.41186, -1.08794, -0.136060] + term2 = np.polynomial.polynomial.polyval(n, polycoeffs) + w = term1 * term2 # Eq 5 + + # relative transmittance of sky diffuse radiation by PV cover: + cosB = cosd(surface_tilt) + sinB = sind(surface_tilt) + cuk = (2*w / (np.pi * (1 + cosB))) * ( + (30/7)*np.pi - (169/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB + + (160/21)*cosB*sinB - (5/3)*np.pi*cosB*sinB**2 + (20/7)*cosB*sinB**3 + - (5/16)*np.pi*cosB*sinB**4 + (16/105)*cosB*sinB**5 + ) # Eq 4 + + # relative transmittance of ground-reflected radiation by PV cover: + cug = 40 * w / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk + + out = { + 'direct': cd, + 'sky': cuk, + 'ground': cug, + } + return out From bd0563a2951d9ad88fbe9f1bfa2d9b3569312c32 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 29 Sep 2022 19:05:46 -0400 Subject: [PATCH 03/28] API and whatsnew entries --- docs/sphinx/source/reference/pv_modeling.rst | 1 + docs/sphinx/source/whatsnew/v0.9.4.rst | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/reference/pv_modeling.rst b/docs/sphinx/source/reference/pv_modeling.rst index 31c380c1bb..464e61f430 100644 --- a/docs/sphinx/source/reference/pv_modeling.rst +++ b/docs/sphinx/source/reference/pv_modeling.rst @@ -28,6 +28,7 @@ Incident angle modifiers iam.interp iam.marion_diffuse iam.marion_integrate + iam.fedis PV temperature models --------------------- diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index c95502bae1..cb68b8a609 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -10,6 +10,9 @@ Deprecations Enhancements ~~~~~~~~~~~~ * Multiple code style issues fixed that were reported by LGTM analysis. (:issue:`1275`, :pull:`1559`) +* Added :py:func:`pvlib.iam.fedis`, a model for calculating direct and diffuse IAM + coefficients. (:pull:`1562`) + Bug fixes ~~~~~~~~~ @@ -34,4 +37,5 @@ Requirements Contributors ~~~~~~~~~~~~ -* Christian Orner (:ghuser:`chrisorner`) \ No newline at end of file +* Christian Orner (:ghuser:`chrisorner`) +* Yu Xie (:ghuser:`xieyupku`) From 82ef3bb35572f30f93929094f78ab313b6553a4e Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 29 Sep 2022 19:56:05 -0400 Subject: [PATCH 04/28] tests --- pvlib/iam.py | 13 ++++++++--- pvlib/tests/test_iam.py | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 28b84b5a0f..b99d9e51ab 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -799,8 +799,12 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): # avoid undefined results for horizontal or upside-down surfaces zeroang = 1e-06 surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt) + surface_tilt = np.where(surface_tilt >= 90, 90 - zeroang, surface_tilt) + + # and for aoi: + aoi = np.where(aoi <= 0, zeroang, aoi) # similar for AOI > 90 - aoi = np.where(aoi <= 90, zeroang, aoi) + aoi = np.where(aoi >= 90, 90 - zeroang, aoi) # angle between module normal and refracted ray: theta_0tp = asind(sind(aoi) / n) # Eq 3c @@ -818,7 +822,8 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): # weighting function term1 = n*(n_ref+1)**2 / (n_ref*(n+1)**2) - polycoeffs = [2.77526e-09, 3.74953, -5.18727, 3.41186, -1.08794, -0.136060] + # note: the last coefficient here differs in sign from the reference + polycoeffs = [2.77526e-09, 3.74953, -5.18727, 3.41186, -1.08794, 0.136060] term2 = np.polynomial.polynomial.polyval(n, polycoeffs) w = term1 * term2 # Eq 5 @@ -826,7 +831,7 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): cosB = cosd(surface_tilt) sinB = sind(surface_tilt) cuk = (2*w / (np.pi * (1 + cosB))) * ( - (30/7)*np.pi - (169/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB + (30/7)*np.pi - (160/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB + (160/21)*cosB*sinB - (5/3)*np.pi*cosB*sinB**2 + (20/7)*cosB*sinB**3 - (5/16)*np.pi*cosB*sinB**4 + (16/105)*cosB*sinB**5 ) # Eq 4 @@ -834,6 +839,8 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): # relative transmittance of ground-reflected radiation by PV cover: cug = 40 * w / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk + # handle tilt=0 case correctly: + cug = np.where(surface_tilt == zeroang, 0, cug) out = { 'direct': cd, 'sky': cuk, diff --git a/pvlib/tests/test_iam.py b/pvlib/tests/test_iam.py index 4310ee837a..2560e5591b 100644 --- a/pvlib/tests/test_iam.py +++ b/pvlib/tests/test_iam.py @@ -322,3 +322,51 @@ def test_marion_integrate_invalid(): with pytest.raises(ValueError): _iam.marion_integrate(_iam.ashrae, 0, 'bad', 180) + + +def test_fedis_defaults(): + # expected values generated with code from model authors + surface_tilt = np.array([0, 30, 60, 90]) + aoi = np.array([0, 30, 60, 90]) + + expected = { + 'direct': np.array([0.994592623, 0.99301513, 0.943633517, 0.0]), + 'sky': np.array([0.9410498, 0.95449355, 0.95357626, 0.9410498]), + 'ground': np.array([0.0, 0.75380253, 0.9034704, 0.9410498]), + } + # numpy arrays + actual = _iam.fedis(aoi, surface_tilt) + assert_allclose(expected['direct'], actual['direct'], atol=1e-6) + assert_allclose(expected['sky'], actual['sky']) + assert_allclose(expected['ground'], actual['ground'], atol=1e-6) + + # scalars + for i in range(len(aoi)): + actual = _iam.fedis(aoi[i], surface_tilt[i]) + assert_allclose(expected['direct'][i], actual['direct'], atol=1e-6) + assert_allclose(expected['sky'][i], actual['sky']) + assert_allclose(expected['ground'][i], actual['ground'], atol=1e-6) + + +def test_fedis_kwargs(): + # expected values generated with code from model authors + surface_tilt = np.array([0, 30, 60, 90]) + aoi = np.array([0, 30, 60, 90]) + + expected = { + 'direct': np.array([0.948928986, 0.947089588, 0.894889901, 0.0]), + 'sky': np.array([0.89536659, 0.90815772, 0.90728496, 0.89536659]), + 'ground': np.array([0.0, 0.717209232, 0.859611482, 0.895366593]), + } + # numpy arrays + actual = _iam.fedis(aoi, surface_tilt, n=1.7, n_ref=1.3) + assert_allclose(expected['direct'], actual['direct'], atol=1e-6) + assert_allclose(expected['sky'], actual['sky']) + assert_allclose(expected['ground'], actual['ground'], atol=1e-6) + + # scalars + for i in range(len(aoi)): + actual = _iam.fedis(aoi[i], surface_tilt[i], n=1.7, n_ref=1.3) + assert_allclose(expected['direct'][i], actual['direct'], atol=1e-6) + assert_allclose(expected['sky'][i], actual['sky']) + assert_allclose(expected['ground'][i], actual['ground'], atol=1e-6) From f89cfb0815885fc008e4b1e07a69fec92171b4f5 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 29 Sep 2022 19:58:15 -0400 Subject: [PATCH 05/28] add a comment --- pvlib/iam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index b99d9e51ab..e21e59a8bb 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -837,7 +837,7 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): ) # Eq 4 # relative transmittance of ground-reflected radiation by PV cover: - cug = 40 * w / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk + cug = 40 * w / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk # Eq 6 # handle tilt=0 case correctly: cug = np.where(surface_tilt == zeroang, 0, cug) From dfee4545cf4a8d9cf281583f893009b3b6c31515 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 29 Sep 2022 20:12:03 -0400 Subject: [PATCH 06/28] add notes section --- pvlib/iam.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pvlib/iam.py b/pvlib/iam.py index e21e59a8bb..b0e80c36bd 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -788,6 +788,11 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): * 'sky': radiation from the sky dome (zenith <= 90) * 'ground': radiation reflected from the ground (zenith >= 90) + Notes + ----- + This implementation corrects a typo in the reference regarding the sign + of the last polynomial term in Equation 5. + References ---------- .. [1] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations' From 51b5f0134dca86c61a322cf80ecfd9076f184d09 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 30 Sep 2022 10:01:48 -0400 Subject: [PATCH 07/28] Apply suggestions from code review Co-authored-by: Cliff Hansen --- pvlib/iam.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index b0e80c36bd..9340a785aa 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -752,7 +752,7 @@ def marion_integrate(function, surface_tilt, region, num=None): def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): """ - Determine the incidence angle modifiers (iam) for direct, diffuse sky, + Determine the incidence angle modifiers (IAM) for direct, diffuse sky, and ground-reflected radiation using the FEDIS transmittance model. The "Fresnel Equations" for Diffuse radiation on Inclined photovoltaic @@ -772,7 +772,7 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): up = 0, surface facing horizon = 90). [degrees] n : float, default 1.5 - Refractive index of the PV cover. The default value of 1.5 + Refractive index of the PV front surface material. The default value of 1.5 was used for an IMT reference cell in [1]_. [unitless] n_ref : float, default 1.4585 @@ -785,12 +785,12 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): IAM values for each type of irradiance: * 'direct': radiation from the solar disc - * 'sky': radiation from the sky dome (zenith <= 90) - * 'ground': radiation reflected from the ground (zenith >= 90) + * 'sky': diffuse radiation from the sky dome + * 'ground': radiation reflected from the ground Notes ----- - This implementation corrects a typo in the reference regarding the sign + This implementation corrects a typo in [1]_ regarding the sign of the last polynomial term in Equation 5. References From ecebb05864b0b2a441d018f65a97958b0b6a3388 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 30 Sep 2022 10:06:39 -0400 Subject: [PATCH 08/28] other changes from review --- pvlib/iam.py | 6 +++--- pvlib/tests/test_iam.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 9340a785aa..f316bb993c 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -784,9 +784,9 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): iam : dict IAM values for each type of irradiance: - * 'direct': radiation from the solar disc - * 'sky': diffuse radiation from the sky dome - * 'ground': radiation reflected from the ground + * 'direct': irradiance from the solar disc + * 'sky': irradiance radiation from the sky dome + * 'ground': irradiance reflected from the ground Notes ----- diff --git a/pvlib/tests/test_iam.py b/pvlib/tests/test_iam.py index 2560e5591b..95941bf258 100644 --- a/pvlib/tests/test_iam.py +++ b/pvlib/tests/test_iam.py @@ -325,10 +325,11 @@ def test_marion_integrate_invalid(): def test_fedis_defaults(): - # expected values generated with code from model authors surface_tilt = np.array([0, 30, 60, 90]) aoi = np.array([0, 30, 60, 90]) + # expected values generated with code from model authors: + # https://github.com/NREL/FEDIS/commit/7ae7186caa39aa85848163a39dac46df56fb9819 # noqa: E501 expected = { 'direct': np.array([0.994592623, 0.99301513, 0.943633517, 0.0]), 'sky': np.array([0.9410498, 0.95449355, 0.95357626, 0.9410498]), @@ -349,10 +350,11 @@ def test_fedis_defaults(): def test_fedis_kwargs(): - # expected values generated with code from model authors surface_tilt = np.array([0, 30, 60, 90]) aoi = np.array([0, 30, 60, 90]) + # expected values generated with code from model authors: + # https://github.com/NREL/FEDIS/commit/7ae7186caa39aa85848163a39dac46df56fb9819 # noqa: E501 expected = { 'direct': np.array([0.948928986, 0.947089588, 0.894889901, 0.0]), 'sky': np.array([0.89536659, 0.90815772, 0.90728496, 0.89536659]), From 96445690dab3f23e01aad90d882e0aaf5e394d6f Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 7 Oct 2022 17:17:04 -0400 Subject: [PATCH 09/28] add iam.schlick --- pvlib/iam.py | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index f316bb993c..dbedbedea3 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -750,7 +750,50 @@ def marion_integrate(function, surface_tilt, region, num=None): return Fd -def fedis(aoi, surface_tilt, n=1.5, n_ref=1.4585): +def schlick(aoi): + """ + Determine incidence angle modifier (IAM) using the Schlick approximation + to the Fresnel equations. + + The Schlick approximation was proposed in [1]_ as a computationally + efficient alternative to computing the Fresnel factor in computer + graphics contexts. This implementation is a normalized form of the + equation in [1]_ so that it can be used as a typical IAM model. + Unlike most other IAM models, this model has no parameters to fit + different reflection profiles. + + In PV contexts, the Schlick approximation has been used as an analytically + integrable alternative to the Fresnel equations to estimate IAM + for diffuse irradiance [2]_. + + Parameters + ---------- + aoi : numeric + The angle of incidence (AOI) between the module normal vector and the + sun-beam vector in degrees. Angles of nan will result in nan. + + Returns + ------- + iam : numeric + The incident angle modifier + + References + ---------- + .. [1] Schlick, C. An inexpensive BRDF model for physically-based + rendering. Computer graphics forum 13 (1994). + + .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations' + for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)", + Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022. + :doi:`10.1016/j.rser.2022.112362` + + See Also + -------- + pvlib.iam.fedis + """ + return 1 - (1 - cosd(aoi)) ** 5 + + """ Determine the incidence angle modifiers (IAM) for direct, diffuse sky, and ground-reflected radiation using the FEDIS transmittance model. From 5aa17b55d54900c20bf387f6445a2437edf179e7 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 7 Oct 2022 17:17:15 -0400 Subject: [PATCH 10/28] revise iam.fedis --- pvlib/iam.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index dbedbedea3..d337d0f0a2 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -794,16 +794,14 @@ def schlick(aoi): return 1 - (1 - cosd(aoi)) ** 5 +def fedis(aoi, surface_tilt, n=1.5, n_ref=1.5): """ Determine the incidence angle modifiers (IAM) for direct, diffuse sky, and ground-reflected radiation using the FEDIS transmittance model. The "Fresnel Equations" for Diffuse radiation on Inclined photovoltaic - Surfaces (FEDIS) [1]_ is an analytical solution of diffuse transmission - based on the rigorous integration of an alternate form of the - Fresnel equations. The approach leads to a simple yet accurate - relative transmittance model that reconciles the solar energy - sensed by pyranometers and PV panels. + Surfaces (FEDIS) [1]_ is the result of analytical integration + the Schlick approximation [2]_ to the Fresnel equations. Parameters ---------- @@ -815,12 +813,14 @@ def schlick(aoi): up = 0, surface facing horizon = 90). [degrees] n : float, default 1.5 - Refractive index of the PV front surface material. The default value of 1.5 - was used for an IMT reference cell in [1]_. [unitless] + Refractive index of the PV front surface material. The default value + of 1.5 was used for an IMT reference cell in [1]_. [unitless] - n_ref : float, default 1.4585 - Refractive index of the pyranometer cover. The default value - was used for a fused silica dome over a CMP22 in [1]_. + n_ref : float, default 1.5 + Reference refractive index. In [1]_ this was set to 1.4585 for + was used for a fused silica dome over a CMP22, but in conventional + PV applications it is appropriate to set this to the same value as + ``n``. Returns ------- @@ -842,6 +842,13 @@ def schlick(aoi): for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)", Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022. :doi:`10.1016/j.rser.2022.112362` + + .. [2] Schlick, C. An inexpensive BRDF model for physically-based + rendering. Computer graphics forum 13 (1994). + + See Also + -------- + pvlib.iam.schlick """ # avoid undefined results for horizontal or upside-down surfaces @@ -862,7 +869,7 @@ def schlick(aoi): tan_term = tand(aoi - theta_0tp)**2 / tand(aoi + theta_0tp)**2 / 2 rd = sin_term + tan_term # Eq 3b - # reflectance on pyranometer cover: + # reflectance for normal incidence with reference refractive index: r0 = ((n_ref-1.0)/(n_ref+1.0))**2.0 # Eq 3e # relative transmittance of direct radiation by PV cover: From d35d57df849a712990de9f867a279c02dfd24b1e Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 7 Oct 2022 17:21:15 -0400 Subject: [PATCH 11/28] edits --- docs/sphinx/source/whatsnew/v0.9.4.rst | 4 ++-- pvlib/iam.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index cb68b8a609..d2eb415b57 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -10,8 +10,8 @@ Deprecations Enhancements ~~~~~~~~~~~~ * Multiple code style issues fixed that were reported by LGTM analysis. (:issue:`1275`, :pull:`1559`) -* Added :py:func:`pvlib.iam.fedis`, a model for calculating direct and diffuse IAM - coefficients. (:pull:`1562`) +* Added :py:func:`pvlib.iam.schlick` and :py:func:`pvlib.iam.fedis`, models + for calculating direct and diffuse IAM values. (:pull:`1562`, :issue:`1564`) Bug fixes diff --git a/pvlib/iam.py b/pvlib/iam.py index d337d0f0a2..4a7114579f 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -758,12 +758,12 @@ def schlick(aoi): The Schlick approximation was proposed in [1]_ as a computationally efficient alternative to computing the Fresnel factor in computer graphics contexts. This implementation is a normalized form of the - equation in [1]_ so that it can be used as a typical IAM model. - Unlike most other IAM models, this model has no parameters to fit + equation in [1]_ so that it can be used as a PV IAM model. + Unlike other IAM models, this model has no ability to describe different reflection profiles. In PV contexts, the Schlick approximation has been used as an analytically - integrable alternative to the Fresnel equations to estimate IAM + integrable alternative to the Fresnel equations for estimating IAM for diffuse irradiance [2]_. Parameters From 920305a0a941f224185e228170c3d5b4d8e0d5f3 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 10 Oct 2022 11:03:27 -0400 Subject: [PATCH 12/28] Update pv_modeling.rst --- docs/sphinx/source/reference/pv_modeling.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sphinx/source/reference/pv_modeling.rst b/docs/sphinx/source/reference/pv_modeling.rst index 464e61f430..b898390ef5 100644 --- a/docs/sphinx/source/reference/pv_modeling.rst +++ b/docs/sphinx/source/reference/pv_modeling.rst @@ -29,6 +29,7 @@ Incident angle modifiers iam.marion_diffuse iam.marion_integrate iam.fedis + iam.schlick PV temperature models --------------------- From bb7276ec24e75504076dbda818969cdb66297905 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 10 Oct 2022 13:18:58 -0400 Subject: [PATCH 13/28] clean up --- docs/sphinx/source/whatsnew/v0.9.4.rst | 5 +- pvlib/iam.py | 52 ++++++++++++------ pvlib/tests/test_iam.py | 76 +++++++++++++++++++------- 3 files changed, 94 insertions(+), 39 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index d2eb415b57..1fb964beed 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -10,8 +10,9 @@ Deprecations Enhancements ~~~~~~~~~~~~ * Multiple code style issues fixed that were reported by LGTM analysis. (:issue:`1275`, :pull:`1559`) -* Added :py:func:`pvlib.iam.schlick` and :py:func:`pvlib.iam.fedis`, models - for calculating direct and diffuse IAM values. (:pull:`1562`, :issue:`1564`) +* Added a simple IAM model :py:func:`pvlib.iam.schlick` and made it available + in :py:func:`~pvlib.iam.marion_diffuse` via ``model='schlick'`` (:pull:`1562`, :issue:`1564`) +* Added a direct and diffuse IAM model :py:func:`pvlib.iam.fedis` (:pull:`1562`) Bug fixes diff --git a/pvlib/iam.py b/pvlib/iam.py index 4a7114579f..927d532327 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -541,7 +541,7 @@ def marion_diffuse(model, surface_tilt, **kwargs): ---------- model : str The IAM function to evaluate across solid angle. Must be one of - `'ashrae', 'physical', 'martin_ruiz', 'sapm'`. + `'ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick'`. surface_tilt : numeric Surface tilt angles in decimal degrees. @@ -592,6 +592,7 @@ def marion_diffuse(model, surface_tilt, **kwargs): 'ashrae': ashrae, 'sapm': sapm, 'martin_ruiz': martin_ruiz, + 'schlick': schlick, } try: @@ -791,7 +792,16 @@ def schlick(aoi): -------- pvlib.iam.fedis """ - return 1 - (1 - cosd(aoi)) ** 5 + iam = 1 - (1 - cosd(aoi)) ** 5 + iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam) + + # preserve input type + if np.isscalar(aoi): + iam = iam.item() + elif isinstance(aoi, pd.Series): + iam = pd.Series(iam, aoi.index) + + return iam def fedis(aoi, surface_tilt, n=1.5, n_ref=1.5): @@ -851,29 +861,23 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.5): pvlib.iam.schlick """ - # avoid undefined results for horizontal or upside-down surfaces - zeroang = 1e-06 - surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt) - surface_tilt = np.where(surface_tilt >= 90, 90 - zeroang, surface_tilt) - - # and for aoi: - aoi = np.where(aoi <= 0, zeroang, aoi) - # similar for AOI > 90 - aoi = np.where(aoi >= 90, 90 - zeroang, aoi) - # angle between module normal and refracted ray: theta_0tp = asind(sind(aoi) / n) # Eq 3c # reflectance of direct radiation on PV cover: - sin_term = sind(aoi - theta_0tp)**2 / sind(aoi + theta_0tp)**2 / 2 - tan_term = tand(aoi - theta_0tp)**2 / tand(aoi + theta_0tp)**2 / 2 + with np.errstate(invalid='ignore'): + sin_term = sind(aoi - theta_0tp)**2 / sind(aoi + theta_0tp)**2 / 2 + tan_term = tand(aoi - theta_0tp)**2 / tand(aoi + theta_0tp)**2 / 2 + rd = sin_term + tan_term # Eq 3b + rd = np.where(np.abs(aoi) < 1e-6, ((n-1.0)/(n+1.0))**2.0, rd) # reflectance for normal incidence with reference refractive index: r0 = ((n_ref-1.0)/(n_ref+1.0))**2.0 # Eq 3e # relative transmittance of direct radiation by PV cover: cd = (1 - rd) / (1 - r0) # Eq 3a + cd = np.where(np.abs(aoi) > 90, 0, cd) # weighting function term1 = n*(n_ref+1)**2 / (n_ref*(n+1)**2) @@ -892,10 +896,24 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.5): ) # Eq 4 # relative transmittance of ground-reflected radiation by PV cover: - cug = 40 * w / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk # Eq 6 + with np.errstate(divide='ignore', invalid='ignore'): # Eq 6 + cug = 40 * w / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk + + cug = np.where(surface_tilt < 1e-6, 0, cug) + + # respect input types: + if np.isscalar(aoi): + cd = cd.item() + elif isinstance(aoi, pd.Series): + cd = pd.Series(cd, aoi.index) + + if np.isscalar(surface_tilt): + cuk = cuk.item() + cug = cug.item() + elif isinstance(surface_tilt, pd.Series): + cuk = pd.Series(cuk, surface_tilt.index) + cug = pd.Series(cug, surface_tilt.index) - # handle tilt=0 case correctly: - cug = np.where(surface_tilt == zeroang, 0, cug) out = { 'direct': cd, 'sky': cuk, diff --git a/pvlib/tests/test_iam.py b/pvlib/tests/test_iam.py index 95941bf258..cd708a9cc6 100644 --- a/pvlib/tests/test_iam.py +++ b/pvlib/tests/test_iam.py @@ -324,32 +324,75 @@ def test_marion_integrate_invalid(): _iam.marion_integrate(_iam.ashrae, 0, 'bad', 180) -def test_fedis_defaults(): - surface_tilt = np.array([0, 30, 60, 90]) - aoi = np.array([0, 30, 60, 90]) +def test_schlick(): + idx = pd.date_range('2019-01-01', freq='h', periods=9) + aoi = pd.Series([-180, -135, -90, -45, 0, 45, 90, 135, 180], idx) + expected = pd.Series([0, 0, 0, 0.99784451, 1.0, 0.99784451, 0, 0, 0], idx) + + # scalars + for aoi_scalar, expected_scalar in zip(aoi, expected): + actual = _iam.schlick(aoi_scalar) + assert_allclose(expected_scalar, actual) + + # numpy arrays + actual = _iam.schlick(aoi.values) + assert_allclose(expected.values, actual) + + # pandas Series + actual = _iam.schlick(aoi) + assert_series_equal(expected, actual) + + +def test_fedis_defaults_direct(): + idx = pd.date_range('2019-01-01', freq='h', periods=9) + aoi = pd.Series([-180, -135, -90, -45, 0, 45, 90, 135, 180], idx) + expected = pd.Series([0, 0, 0, 0.989333426, 1, 0.989333426, 0, 0, 0], idx) + + # numpy arrays + actual = _iam.fedis(aoi.values, surface_tilt=0) + assert_allclose(expected, expected, atol=1e-6) + + # scalars + for i in range(len(aoi)): + actual = _iam.fedis(aoi[i], surface_tilt=0) + assert_allclose(expected[i], actual['direct'], atol=1e-6) + + # pandas Series + actual = _iam.fedis(aoi, surface_tilt=0) + assert_series_equal(expected, actual['direct']) + + +def test_fedis_defaults_diffuse(): + idx = pd.date_range('2019-01-01', freq='h', periods=3) + surface_tilt = pd.Series([0, 20, 90], idx) # expected values generated with code from model authors: # https://github.com/NREL/FEDIS/commit/7ae7186caa39aa85848163a39dac46df56fb9819 # noqa: E501 expected = { - 'direct': np.array([0.994592623, 0.99301513, 0.943633517, 0.0]), - 'sky': np.array([0.9410498, 0.95449355, 0.95357626, 0.9410498]), - 'ground': np.array([0.0, 0.75380253, 0.9034704, 0.9410498]), + 'sky': pd.Series([0.946166074, 0.956218435, 0.946166074], idx), + 'ground': pd.Series([0.0, 0.62284759, 0.946166074], idx), } + # numpy arrays - actual = _iam.fedis(aoi, surface_tilt) - assert_allclose(expected['direct'], actual['direct'], atol=1e-6) + actual = _iam.fedis(aoi=0, surface_tilt=surface_tilt.values) assert_allclose(expected['sky'], actual['sky']) - assert_allclose(expected['ground'], actual['ground'], atol=1e-6) + assert_allclose(expected['ground'], actual['ground']) # scalars - for i in range(len(aoi)): - actual = _iam.fedis(aoi[i], surface_tilt[i]) - assert_allclose(expected['direct'][i], actual['direct'], atol=1e-6) + for i in range(len(surface_tilt)): + actual = _iam.fedis(aoi=0, surface_tilt=surface_tilt[i]) assert_allclose(expected['sky'][i], actual['sky']) - assert_allclose(expected['ground'][i], actual['ground'], atol=1e-6) + assert_allclose(expected['ground'][i], actual['ground']) + + # pandas Series + actual = _iam.fedis(aoi=0, surface_tilt=surface_tilt) + assert_series_equal(expected['sky'], actual['sky']) + assert_series_equal(expected['ground'], actual['ground']) def test_fedis_kwargs(): + # custom n and n_ref + surface_tilt = np.array([0, 30, 60, 90]) aoi = np.array([0, 30, 60, 90]) @@ -365,10 +408,3 @@ def test_fedis_kwargs(): assert_allclose(expected['direct'], actual['direct'], atol=1e-6) assert_allclose(expected['sky'], actual['sky']) assert_allclose(expected['ground'], actual['ground'], atol=1e-6) - - # scalars - for i in range(len(aoi)): - actual = _iam.fedis(aoi[i], surface_tilt[i], n=1.7, n_ref=1.3) - assert_allclose(expected['direct'][i], actual['direct'], atol=1e-6) - assert_allclose(expected['sky'][i], actual['sky']) - assert_allclose(expected['ground'][i], actual['ground'], atol=1e-6) From c1bbf3580ef81a9e4ea451cf953274621fd63b8f Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 10 Oct 2022 13:31:30 -0400 Subject: [PATCH 14/28] better default behavior for n_ref --- pvlib/iam.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 927d532327..a04f3a3dd1 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -804,7 +804,7 @@ def schlick(aoi): return iam -def fedis(aoi, surface_tilt, n=1.5, n_ref=1.5): +def fedis(aoi, surface_tilt, n=1.5, n_ref=None): """ Determine the incidence angle modifiers (IAM) for direct, diffuse sky, and ground-reflected radiation using the FEDIS transmittance model. @@ -826,11 +826,11 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.5): Refractive index of the PV front surface material. The default value of 1.5 was used for an IMT reference cell in [1]_. [unitless] - n_ref : float, default 1.5 + n_ref : float, optional Reference refractive index. In [1]_ this was set to 1.4585 for was used for a fused silica dome over a CMP22, but in conventional PV applications it is appropriate to set this to the same value as - ``n``. + ``n`` (the default behavior). Returns ------- @@ -860,6 +860,8 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=1.5): -------- pvlib.iam.schlick """ + if n_ref is None: + n_ref = n # angle between module normal and refracted ray: theta_0tp = asind(sind(aoi) / n) # Eq 3c From b7704c3678bae9d071208d3b6d4e791cdd572c8b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 10 Oct 2022 14:01:06 -0400 Subject: [PATCH 15/28] Apply suggestions from code review Co-authored-by: Cliff Hansen --- pvlib/iam.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index a04f3a3dd1..e224f8fc19 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -828,8 +828,8 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=None): n_ref : float, optional Reference refractive index. In [1]_ this was set to 1.4585 for - was used for a fused silica dome over a CMP22, but in conventional - PV applications it is appropriate to set this to the same value as + a fused silica dome over a CMP22, but in conventional + PV applications it is appropriate to set n_ref to the same value as ``n`` (the default behavior). Returns @@ -838,7 +838,7 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=None): IAM values for each type of irradiance: * 'direct': irradiance from the solar disc - * 'sky': irradiance radiation from the sky dome + * 'sky': diffuse irradiance from the sky dome * 'ground': irradiance reflected from the ground Notes From 5d2246052445c38667985bb8abaa2092d866684d Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 Oct 2022 11:25:10 -0400 Subject: [PATCH 16/28] split up schlick/schlick_diffuse/fedis/fedis_diffuse Co-Authored-By: Anton Driesse <9001027+adriesse@users.noreply.github.com> --- docs/sphinx/source/reference/pv_modeling.rst | 4 +- docs/sphinx/source/whatsnew/v0.9.4.rst | 10 +- pvlib/iam.py | 207 +++++++++++++------ pvlib/tests/test_iam.py | 82 +++++--- 4 files changed, 215 insertions(+), 88 deletions(-) diff --git a/docs/sphinx/source/reference/pv_modeling.rst b/docs/sphinx/source/reference/pv_modeling.rst index b898390ef5..18e7b4e9a9 100644 --- a/docs/sphinx/source/reference/pv_modeling.rst +++ b/docs/sphinx/source/reference/pv_modeling.rst @@ -28,8 +28,10 @@ Incident angle modifiers iam.interp iam.marion_diffuse iam.marion_integrate - iam.fedis iam.schlick + iam.schlick_diffuse + iam.fedis + iam.fedis_diffuse PV temperature models --------------------- diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index 1fb964beed..950436bac5 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -10,9 +10,10 @@ Deprecations Enhancements ~~~~~~~~~~~~ * Multiple code style issues fixed that were reported by LGTM analysis. (:issue:`1275`, :pull:`1559`) -* Added a simple IAM model :py:func:`pvlib.iam.schlick` and made it available - in :py:func:`~pvlib.iam.marion_diffuse` via ``model='schlick'`` (:pull:`1562`, :issue:`1564`) -* Added a direct and diffuse IAM model :py:func:`pvlib.iam.fedis` (:pull:`1562`) +* Added two direct IAM models :py:func:`pvlib.iam.schlick` and :py:func:`pvlib.iam.fedis`, + both of which are also available in :py:func:`~pvlib.iam.marion_diffuse` (:pull:`1562`, :issue:`1564`) +* Added two diffuse IAM models :py:func:`pvlib.iam.schlick_diffuse` and + :py:func:`pvlib.iam.fedis_diffuse` (:pull:`1562`) Bug fixes @@ -40,3 +41,6 @@ Contributors ~~~~~~~~~~~~ * Christian Orner (:ghuser:`chrisorner`) * Yu Xie (:ghuser:`xieyupku`) +* Anton Driesse (:ghuser:`adriesse`) +* Cliff Hansen (:ghuser:`cwhanse`) +* Kevin Anderson (:ghuser:`kanderso-nrel`) diff --git a/pvlib/iam.py b/pvlib/iam.py index e224f8fc19..9060d0d735 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -541,7 +541,7 @@ def marion_diffuse(model, surface_tilt, **kwargs): ---------- model : str The IAM function to evaluate across solid angle. Must be one of - `'ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick'`. + `'ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick', 'fedis'`. surface_tilt : numeric Surface tilt angles in decimal degrees. @@ -593,6 +593,7 @@ def marion_diffuse(model, surface_tilt, **kwargs): 'sapm': sapm, 'martin_ruiz': martin_ruiz, 'schlick': schlick, + 'fedis': fedis, } try: @@ -791,6 +792,7 @@ def schlick(aoi): See Also -------- pvlib.iam.fedis + pvlib.iam.schlick_diffuse """ iam = 1 - (1 - cosd(aoi)) ** 5 iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam) @@ -804,24 +806,88 @@ def schlick(aoi): return iam -def fedis(aoi, surface_tilt, n=1.5, n_ref=None): +def schlick_diffuse(surface_tilt): """ - Determine the incidence angle modifiers (IAM) for direct, diffuse sky, - and ground-reflected radiation using the FEDIS transmittance model. + Determine the incidence angle modifiers (iam) for diffuse sky and + ground-reflected irradiance on a tilted surface using the Schlick + incident angle model. - The "Fresnel Equations" for Diffuse radiation on Inclined photovoltaic - Surfaces (FEDIS) [1]_ is the result of analytical integration - the Schlick approximation [2]_ to the Fresnel equations. + The diffuse iam values are calculated using an analytical integration + of the Schlick equation [1]_ over the portion of an isotropic sky and + isotropic foreground that is visible from the tilted surface [2]_. Parameters ---------- - aoi : numeric - Angle of incidence. [degrees] - surface_tilt : numeric Surface tilt angle measured from horizontal (e.g. surface facing up = 0, surface facing horizon = 90). [degrees] + Returns + ------- + iam_sky : numeric + The incident angle modifier for sky diffuse + + iam_ground : numeric + The incident angle modifier for ground-reflected diffuse + + References + ---------- + .. [1] Schlick, C. An inexpensive BRDF model for physically-based + rendering. Computer graphics forum 13 (1994). + + .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations' + for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)", + Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022. + :doi:`10.1016/j.rser.2022.112362` + + See Also + -------- + pvlib.iam.schlick + pvlib.iam.fedis_diffuse + """ + # these calculations are as in [2]_, but with the refractive index + # weighting coefficient w set to 1.0 (so it is omitted) + + # relative transmittance of sky diffuse radiation by PV cover: + cosB = cosd(surface_tilt) + sinB = sind(surface_tilt) + cuk = (2 / (np.pi * (1 + cosB))) * ( + (30/7)*np.pi - (160/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB + + (160/21)*cosB*sinB - (5/3)*np.pi*cosB*sinB**2 + (20/7)*cosB*sinB**3 + - (5/16)*np.pi*cosB*sinB**4 + (16/105)*cosB*sinB**5 + ) # Eq 4 in [2] + + # relative transmittance of ground-reflected radiation by PV cover: + with np.errstate(divide='ignore', invalid='ignore'): # Eq 6 in [2] + cug = 40 / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk + + cug = np.where(surface_tilt < 1e-6, 0, cug) + + # respect input types: + if np.isscalar(surface_tilt): + cuk = cuk.item() + cug = cug.item() + elif isinstance(surface_tilt, pd.Series): + cuk = pd.Series(cuk, surface_tilt.index) + cug = pd.Series(cug, surface_tilt.index) + + return cuk, cug + + +def fedis(aoi, n=1.5, n_ref=None): + """ + Determine the incidence angle modifier (IAM) for direct irradiance + using the FEDIS transmittance model. + + Note that FEDIS [1]_ calculates direct IAM using the Fresnel equations + without extinction, thus in the default case of ``n_ref=n``, this is + equivalent to calling :py:func:`physical` with ``K=0``. + + Parameters + ---------- + aoi : numeric + Angle of incidence. [degrees] + n : float, default 1.5 Refractive index of the PV front surface material. The default value of 1.5 was used for an IMT reference cell in [1]_. [unitless] @@ -834,17 +900,8 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=None): Returns ------- - iam : dict - IAM values for each type of irradiance: - - * 'direct': irradiance from the solar disc - * 'sky': diffuse irradiance from the sky dome - * 'ground': irradiance reflected from the ground - - Notes - ----- - This implementation corrects a typo in [1]_ regarding the sign - of the last polynomial term in Equation 5. + iam : numeric + The incident angle modifier References ---------- @@ -853,12 +910,9 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=None): Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022. :doi:`10.1016/j.rser.2022.112362` - .. [2] Schlick, C. An inexpensive BRDF model for physically-based - rendering. Computer graphics forum 13 (1994). - See Also -------- - pvlib.iam.schlick + pvlib.iam.physical """ if n_ref is None: n_ref = n @@ -881,45 +935,80 @@ def fedis(aoi, surface_tilt, n=1.5, n_ref=None): cd = (1 - rd) / (1 - r0) # Eq 3a cd = np.where(np.abs(aoi) > 90, 0, cd) - # weighting function - term1 = n*(n_ref+1)**2 / (n_ref*(n+1)**2) - # note: the last coefficient here differs in sign from the reference - polycoeffs = [2.77526e-09, 3.74953, -5.18727, 3.41186, -1.08794, 0.136060] - term2 = np.polynomial.polynomial.polyval(n, polycoeffs) - w = term1 * term2 # Eq 5 - - # relative transmittance of sky diffuse radiation by PV cover: - cosB = cosd(surface_tilt) - sinB = sind(surface_tilt) - cuk = (2*w / (np.pi * (1 + cosB))) * ( - (30/7)*np.pi - (160/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB - + (160/21)*cosB*sinB - (5/3)*np.pi*cosB*sinB**2 + (20/7)*cosB*sinB**3 - - (5/16)*np.pi*cosB*sinB**4 + (16/105)*cosB*sinB**5 - ) # Eq 4 - - # relative transmittance of ground-reflected radiation by PV cover: - with np.errstate(divide='ignore', invalid='ignore'): # Eq 6 - cug = 40 * w / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk - - cug = np.where(surface_tilt < 1e-6, 0, cug) - # respect input types: if np.isscalar(aoi): cd = cd.item() elif isinstance(aoi, pd.Series): cd = pd.Series(cd, aoi.index) - if np.isscalar(surface_tilt): - cuk = cuk.item() - cug = cug.item() - elif isinstance(surface_tilt, pd.Series): - cuk = pd.Series(cuk, surface_tilt.index) - cug = pd.Series(cug, surface_tilt.index) + return cd - out = { - 'direct': cd, - 'sky': cuk, - 'ground': cug, - } - return out +def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): + """ + Determine the incidence angle modifiers (IAM) for diffuse sky and + and ground-reflected radiation using the FEDIS transmittance model. + + The "Fresnel Equations" for Diffuse radiation on Inclined photovoltaic + Surfaces (FEDIS) [1]_ is the result of analytical integration + the Schlick approximation [2]_ to the Fresnel equations. + + Parameters + ---------- + surface_tilt : numeric + Surface tilt angle measured from horizontal (e.g. surface facing + up = 0, surface facing horizon = 90). [degrees] + + n : float, default 1.5 + Refractive index of the PV front surface material. The default value + of 1.5 was used for an IMT reference cell in [1]_. [unitless] + + n_ref : float, optional + Reference refractive index. In [1]_ this was set to 1.4585 for + a fused silica dome over a CMP22, but in conventional + PV applications it is appropriate to set n_ref to the same value as + ``n`` (the default behavior). + + Returns + ------- + iam_sky : numeric + The incident angle modifier for sky diffuse + + iam_ground : numeric + The incident angle modifier for ground-reflected diffuse + + Notes + ----- + This implementation corrects a typo in [1]_ regarding the sign + of the last polynomial term in Equation 5. + + References + ---------- + .. [1] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations' + for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)", + Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022. + :doi:`10.1016/j.rser.2022.112362` + + .. [2] Schlick, C. An inexpensive BRDF model for physically-based + rendering. Computer graphics forum 13 (1994). + + See Also + -------- + pvlib.iam.schlick_diffuse + """ + if n_ref is None: + n_ref = n + + cuk, cug = schlick_diffuse(surface_tilt) + + # weighting function + term1 = n*(n_ref+1)**2 / (n_ref*(n+1)**2) + # note: the last coefficient here differs in sign from the reference + polycoeffs = [2.77526e-09, 3.74953, -5.18727, 3.41186, -1.08794, 0.136060] + term2 = np.polynomial.polynomial.polyval(n, polycoeffs) + w = term1 * term2 # Eq 5 + + cuk = cuk * w + cug = cug * w + + return cuk, cug diff --git a/pvlib/tests/test_iam.py b/pvlib/tests/test_iam.py index cd708a9cc6..323c2dba52 100644 --- a/pvlib/tests/test_iam.py +++ b/pvlib/tests/test_iam.py @@ -343,26 +343,62 @@ def test_schlick(): assert_series_equal(expected, actual) -def test_fedis_defaults_direct(): +def test_schlick_diffuse(): + surface_tilt = np.array([0, 20, 70, 90]) + # expected values calculated with marion_integrate and schlick + expected_sky = np.array([0.95238092, 0.96249934, 0.96228167, 0.95238094]) + expected_ground = np.array([0, 0.62693858, 0.93218737, 0.95238094]) + + # numpy arrays + actual_sky, actual_ground = _iam.schlick_diffuse(surface_tilt) + assert_allclose(expected_sky, actual_sky) + assert_allclose(expected_ground, actual_ground, rtol=1e-6) + + # scalars + for i in range(len(surface_tilt)): + actual_sky, actual_ground = _iam.schlick_diffuse(surface_tilt[i]) + assert_allclose(expected_sky[i], actual_sky) + assert_allclose(expected_ground[i], actual_ground, rtol=1e-6) + + # pandas Series + idx = pd.date_range('2019-01-01', freq='h', periods=len(surface_tilt)) + actual_sky, actual_ground = _iam.schlick_diffuse(pd.Series(surface_tilt, + idx)) + assert_series_equal(pd.Series(expected_sky, idx), actual_sky) + assert_series_equal(pd.Series(expected_ground, idx), actual_ground, + rtol=1e-6) + + +def test_fedis_defaults(): idx = pd.date_range('2019-01-01', freq='h', periods=9) aoi = pd.Series([-180, -135, -90, -45, 0, 45, 90, 135, 180], idx) expected = pd.Series([0, 0, 0, 0.989333426, 1, 0.989333426, 0, 0, 0], idx) # numpy arrays - actual = _iam.fedis(aoi.values, surface_tilt=0) + actual = _iam.fedis(aoi.values) assert_allclose(expected, expected, atol=1e-6) # scalars for i in range(len(aoi)): - actual = _iam.fedis(aoi[i], surface_tilt=0) - assert_allclose(expected[i], actual['direct'], atol=1e-6) + actual = _iam.fedis(aoi[i]) + assert_allclose(expected[i], actual, atol=1e-6) # pandas Series - actual = _iam.fedis(aoi, surface_tilt=0) - assert_series_equal(expected, actual['direct']) + actual = _iam.fedis(aoi) + assert_series_equal(expected, actual) -def test_fedis_defaults_diffuse(): +def test_fedis_kwargs(): + # custom n and n_ref + aoi = np.array([0, 30, 60, 90]) + # expected values generated with code from model authors: + # https://github.com/NREL/FEDIS/commit/7ae7186caa39aa85848163a39dac46df56fb9819 # noqa: E501 + expected = np.array([0.948928986, 0.947089588, 0.894889901, 0.0]) + actual = _iam.fedis(aoi, n=1.7, n_ref=1.3) + assert_allclose(expected, actual, atol=1e-6) + + +def test_fedis_diffuse_defaults(): idx = pd.date_range('2019-01-01', freq='h', periods=3) surface_tilt = pd.Series([0, 20, 90], idx) @@ -374,37 +410,33 @@ def test_fedis_defaults_diffuse(): } # numpy arrays - actual = _iam.fedis(aoi=0, surface_tilt=surface_tilt.values) - assert_allclose(expected['sky'], actual['sky']) - assert_allclose(expected['ground'], actual['ground']) + actual_sky, actual_ground = _iam.fedis_diffuse(surface_tilt.values) + assert_allclose(expected['sky'], actual_sky) + assert_allclose(expected['ground'], actual_ground) # scalars for i in range(len(surface_tilt)): - actual = _iam.fedis(aoi=0, surface_tilt=surface_tilt[i]) - assert_allclose(expected['sky'][i], actual['sky']) - assert_allclose(expected['ground'][i], actual['ground']) + actual_sky, actual_ground = _iam.fedis_diffuse(surface_tilt[i]) + assert_allclose(expected['sky'][i], actual_sky) + assert_allclose(expected['ground'][i], actual_ground) # pandas Series - actual = _iam.fedis(aoi=0, surface_tilt=surface_tilt) - assert_series_equal(expected['sky'], actual['sky']) - assert_series_equal(expected['ground'], actual['ground']) + actual_sky, actual_ground = _iam.fedis_diffuse(surface_tilt) + assert_series_equal(expected['sky'], actual_sky) + assert_series_equal(expected['ground'], actual_ground) -def test_fedis_kwargs(): +def test_fedis_diffuse_kwargs(): # custom n and n_ref - surface_tilt = np.array([0, 30, 60, 90]) - aoi = np.array([0, 30, 60, 90]) - # expected values generated with code from model authors: # https://github.com/NREL/FEDIS/commit/7ae7186caa39aa85848163a39dac46df56fb9819 # noqa: E501 expected = { - 'direct': np.array([0.948928986, 0.947089588, 0.894889901, 0.0]), 'sky': np.array([0.89536659, 0.90815772, 0.90728496, 0.89536659]), 'ground': np.array([0.0, 0.717209232, 0.859611482, 0.895366593]), } # numpy arrays - actual = _iam.fedis(aoi, surface_tilt, n=1.7, n_ref=1.3) - assert_allclose(expected['direct'], actual['direct'], atol=1e-6) - assert_allclose(expected['sky'], actual['sky']) - assert_allclose(expected['ground'], actual['ground'], atol=1e-6) + actual_sky, actual_ground = _iam.fedis_diffuse(surface_tilt, + n=1.7, n_ref=1.3) + assert_allclose(expected['sky'], actual_sky) + assert_allclose(expected['ground'], actual_ground, atol=1e-6) From 70ac60c41adda4181bfee755ff454216d7ce257b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 Oct 2022 16:39:40 -0400 Subject: [PATCH 17/28] Apply suggestions from code review Co-authored-by: Cliff Hansen --- pvlib/iam.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 9060d0d735..567747b830 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -772,7 +772,7 @@ def schlick(aoi): ---------- aoi : numeric The angle of incidence (AOI) between the module normal vector and the - sun-beam vector in degrees. Angles of nan will result in nan. + sun-beam vector. Angles of nan will result in nan. [degrees] Returns ------- @@ -880,7 +880,7 @@ def fedis(aoi, n=1.5, n_ref=None): using the FEDIS transmittance model. Note that FEDIS [1]_ calculates direct IAM using the Fresnel equations - without extinction, thus in the default case of ``n_ref=n``, this is + without extinction, thus in the default case of ``n_ref=None``, this is equivalent to calling :py:func:`physical` with ``K=0``. Parameters @@ -893,10 +893,10 @@ def fedis(aoi, n=1.5, n_ref=None): of 1.5 was used for an IMT reference cell in [1]_. [unitless] n_ref : float, optional - Reference refractive index. In [1]_ this was set to 1.4585 for + Reference refractive index. If None (default), set equal to ``n``. + In [1]_ ``n_ref`` was set to 1.4585 for a fused silica dome over a CMP22, but in conventional - PV applications it is appropriate to set n_ref to the same value as - ``n`` (the default behavior). + PV applications it is appropriate to set ``n_ref=n`` (the default behavior). Returns ------- From 948c8b3c24589be5ef2ae23e6ac14869d67fb63c Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 Oct 2022 16:44:44 -0400 Subject: [PATCH 18/28] follow up --- docs/sphinx/source/whatsnew/v0.9.4.rst | 2 +- pvlib/iam.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index 950436bac5..ebce924bff 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -11,7 +11,7 @@ Enhancements ~~~~~~~~~~~~ * Multiple code style issues fixed that were reported by LGTM analysis. (:issue:`1275`, :pull:`1559`) * Added two direct IAM models :py:func:`pvlib.iam.schlick` and :py:func:`pvlib.iam.fedis`, - both of which are also available in :py:func:`~pvlib.iam.marion_diffuse` (:pull:`1562`, :issue:`1564`) + both of which can be used with :py:func:`~pvlib.iam.marion_diffuse` (:pull:`1562`, :issue:`1564`) * Added two diffuse IAM models :py:func:`pvlib.iam.schlick_diffuse` and :py:func:`pvlib.iam.fedis_diffuse` (:pull:`1562`) diff --git a/pvlib/iam.py b/pvlib/iam.py index 567747b830..92e88ed906 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -808,7 +808,7 @@ def schlick(aoi): def schlick_diffuse(surface_tilt): """ - Determine the incidence angle modifiers (iam) for diffuse sky and + Determine the incidence angle modifiers (IAM) for diffuse sky and ground-reflected irradiance on a tilted surface using the Schlick incident angle model. @@ -896,7 +896,8 @@ def fedis(aoi, n=1.5, n_ref=None): Reference refractive index. If None (default), set equal to ``n``. In [1]_ ``n_ref`` was set to 1.4585 for a fused silica dome over a CMP22, but in conventional - PV applications it is appropriate to set ``n_ref=n`` (the default behavior). + PV applications it is appropriate to set ``n_ref=n`` (the default + behavior). Returns ------- @@ -964,10 +965,11 @@ def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): of 1.5 was used for an IMT reference cell in [1]_. [unitless] n_ref : float, optional - Reference refractive index. In [1]_ this was set to 1.4585 for + Reference refractive index. If None (default), set equal to ``n``. + In [1]_ ``n_ref`` was set to 1.4585 for a fused silica dome over a CMP22, but in conventional - PV applications it is appropriate to set n_ref to the same value as - ``n`` (the default behavior). + PV applications it is appropriate to set ``n_ref=n`` (the default + behavior). Returns ------- From deded0db3e321d32e489f2f2e3e6978da8e743a2 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 17 Oct 2022 14:52:32 -0400 Subject: [PATCH 19/28] use iam.physical in iam.fedis Co-Authored-By: Anton Driesse <9001027+adriesse@users.noreply.github.com> --- pvlib/iam.py | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 92e88ed906..da156c356c 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -915,34 +915,22 @@ def fedis(aoi, n=1.5, n_ref=None): -------- pvlib.iam.physical """ - if n_ref is None: - n_ref = n - - # angle between module normal and refracted ray: - theta_0tp = asind(sind(aoi) / n) # Eq 3c - - # reflectance of direct radiation on PV cover: - with np.errstate(invalid='ignore'): - sin_term = sind(aoi - theta_0tp)**2 / sind(aoi + theta_0tp)**2 / 2 - tan_term = tand(aoi - theta_0tp)**2 / tand(aoi + theta_0tp)**2 / 2 + iam_physical = physical(aoi, n, K=0, L=0) - rd = sin_term + tan_term # Eq 3b - rd = np.where(np.abs(aoi) < 1e-6, ((n-1.0)/(n+1.0))**2.0, rd) + if n_ref is None: + return iam_physical - # reflectance for normal incidence with reference refractive index: - r0 = ((n_ref-1.0)/(n_ref+1.0))**2.0 # Eq 3e + else: + # reflectance for normal incidence with refractive index n: + rd0 = ((n - 1) / (n + 1)) ** 2 - # relative transmittance of direct radiation by PV cover: - cd = (1 - rd) / (1 - r0) # Eq 3a - cd = np.where(np.abs(aoi) > 90, 0, cd) + # reflectance for normal incidence with refractive index n_ref: + r0 = ((n_ref - 1) / (n_ref + 1)) ** 2 - # respect input types: - if np.isscalar(aoi): - cd = cd.item() - elif isinstance(aoi, pd.Series): - cd = pd.Series(cd, aoi.index) + # weighting function + w = (1 - rd0) / (1 - r0) - return cd + return w * iam_physical def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): From ebcf1f65e162b9389eb02c7ac84bed7cb1cf024e Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 28 Oct 2022 15:26:15 -0400 Subject: [PATCH 20/28] Apply suggestions from code review Co-authored-by: Cliff Hansen --- pvlib/iam.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index da156c356c..3f4b1d16bd 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -754,8 +754,8 @@ def marion_integrate(function, surface_tilt, region, num=None): def schlick(aoi): """ - Determine incidence angle modifier (IAM) using the Schlick approximation - to the Fresnel equations. + Determine incidence angle modifier (IAM) for direct irradiance using the + Schlick approximation to the Fresnel equations. The Schlick approximation was proposed in [1]_ as a computationally efficient alternative to computing the Fresnel factor in computer @@ -777,7 +777,7 @@ def schlick(aoi): Returns ------- iam : numeric - The incident angle modifier + The incident angle modifier. References ---------- @@ -825,10 +825,10 @@ def schlick_diffuse(surface_tilt): Returns ------- iam_sky : numeric - The incident angle modifier for sky diffuse + The incident angle modifier for sky diffuse. iam_ground : numeric - The incident angle modifier for ground-reflected diffuse + The incident angle modifier for ground-reflected diffuse. References ---------- @@ -897,12 +897,12 @@ def fedis(aoi, n=1.5, n_ref=None): In [1]_ ``n_ref`` was set to 1.4585 for a fused silica dome over a CMP22, but in conventional PV applications it is appropriate to set ``n_ref=n`` (the default - behavior). + behavior). [unitless] Returns ------- iam : numeric - The incident angle modifier + The incident angle modifier. References ---------- @@ -957,15 +957,15 @@ def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): In [1]_ ``n_ref`` was set to 1.4585 for a fused silica dome over a CMP22, but in conventional PV applications it is appropriate to set ``n_ref=n`` (the default - behavior). + behavior). [unitless] Returns ------- iam_sky : numeric - The incident angle modifier for sky diffuse + The incident angle modifier for sky diffuse. iam_ground : numeric - The incident angle modifier for ground-reflected diffuse + The incident angle modifier for ground-reflected diffuse. Notes ----- From a7d41962fe244ee376296c50fd168bfc77cee697 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 28 Oct 2022 15:30:06 -0400 Subject: [PATCH 21/28] better comments, variable name --- pvlib/iam.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 3f4b1d16bd..c7d235aa19 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -927,10 +927,10 @@ def fedis(aoi, n=1.5, n_ref=None): # reflectance for normal incidence with refractive index n_ref: r0 = ((n_ref - 1) / (n_ref + 1)) ** 2 - # weighting function - w = (1 - rd0) / (1 - r0) + # correction coefficient for different indices of refraction + transmittance_ratio = (1 - rd0) / (1 - r0) - return w * iam_physical + return transmittance_ratio * iam_physical def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): @@ -992,6 +992,8 @@ def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): cuk, cug = schlick_diffuse(surface_tilt) # weighting function + # note that this expression for term1 is algebraically equivalent to + # the "transmittance_ratio" in fedis() term1 = n*(n_ref+1)**2 / (n_ref*(n+1)**2) # note: the last coefficient here differs in sign from the reference polycoeffs = [2.77526e-09, 3.74953, -5.18727, 3.41186, -1.08794, 0.136060] From 1e3b8860203cc6eae344e5fdcc4f64120a2d5aa8 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 31 Oct 2022 13:39:56 -0400 Subject: [PATCH 22/28] revise docstrings --- pvlib/iam.py | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index c7d235aa19..26347d5872 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -893,27 +893,36 @@ def fedis(aoi, n=1.5, n_ref=None): of 1.5 was used for an IMT reference cell in [1]_. [unitless] n_ref : float, optional - Reference refractive index. If None (default), set equal to ``n``. - In [1]_ ``n_ref`` was set to 1.4585 for - a fused silica dome over a CMP22, but in conventional - PV applications it is appropriate to set ``n_ref=n`` (the default - behavior). [unitless] + Refractive index of the surface material for the reference device + measuring irradiance at normal incidence. Mathematically, this is + the value of ``n`` for which IAM=1.0 at normal incidence. + If None (default), set equal to ``n``. See Notes for + possible use cases. [unitless] Returns ------- iam : numeric The incident angle modifier. + See Also + -------- + pvlib.iam.physical + + Notes + ----- + In most PV applications it is appropriate to set ``n_ref=n`` + (the default behavior), but a custom ``n_ref`` may be useful when + using irradiance data from a pyranometer whose calibration does not + account for reflection off its glass dome. It may also be used as + the original index of refraction if the module surface's refractive + index is changing over time (for example a degraded AR coating). + References ---------- .. [1] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations' for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)", Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022. :doi:`10.1016/j.rser.2022.112362` - - See Also - -------- - pvlib.iam.physical """ iam_physical = physical(aoi, n, K=0, L=0) @@ -953,11 +962,11 @@ def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): of 1.5 was used for an IMT reference cell in [1]_. [unitless] n_ref : float, optional - Reference refractive index. If None (default), set equal to ``n``. - In [1]_ ``n_ref`` was set to 1.4585 for - a fused silica dome over a CMP22, but in conventional - PV applications it is appropriate to set ``n_ref=n`` (the default - behavior). [unitless] + Refractive index of the surface material for the reference device + measuring irradiance at normal incidence. Mathematically, this is + the value of ``n`` for which IAM=1.0 at normal incidence. + If None (default), set equal to ``n``. See :py:func:`fedis` for + more information regardings its use. [unitless] Returns ------- @@ -967,6 +976,10 @@ def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): iam_ground : numeric The incident angle modifier for ground-reflected diffuse. + See Also + -------- + pvlib.iam.schlick_diffuse + Notes ----- This implementation corrects a typo in [1]_ regarding the sign @@ -981,10 +994,6 @@ def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): .. [2] Schlick, C. An inexpensive BRDF model for physically-based rendering. Computer graphics forum 13 (1994). - - See Also - -------- - pvlib.iam.schlick_diffuse """ if n_ref is None: n_ref = n From ae1fa18c83e6138e3992989f25ada6d6b4b8e88a Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 31 Oct 2022 13:42:35 -0400 Subject: [PATCH 23/28] clean up 0.9.4 whatsnew, include in whatsnew index --- docs/sphinx/source/whatsnew.rst | 1 + docs/sphinx/source/whatsnew/v0.9.4.rst | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sphinx/source/whatsnew.rst b/docs/sphinx/source/whatsnew.rst index 4830371985..464e59f121 100644 --- a/docs/sphinx/source/whatsnew.rst +++ b/docs/sphinx/source/whatsnew.rst @@ -6,6 +6,7 @@ What's New These are new features and improvements of note in each release. +.. include:: whatsnew/v0.9.4.rst .. include:: whatsnew/v0.9.3.rst .. include:: whatsnew/v0.9.2.rst .. include:: whatsnew/v0.9.1.rst diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index 5d0f678a01..41117e2a61 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -1,7 +1,7 @@ .. _whatsnew_0940: -v0.9.4 (TBD) ------------------------- +v0.9.4 (anticipated December 2022) +---------------------------------- Deprecations ~~~~~~~~~~~~ @@ -14,7 +14,6 @@ Enhancements both of which can be used with :py:func:`~pvlib.iam.marion_diffuse` (:pull:`1562`, :issue:`1564`) * Added two diffuse IAM models :py:func:`pvlib.iam.schlick_diffuse` and :py:func:`pvlib.iam.fedis_diffuse` (:pull:`1562`) - * Add optional ``return_components`` parameter to :py:func:`pvlib.irradiance.haydavies` to return individual diffuse irradiance components (:issue:`1553`, :pull:`1568`) @@ -42,10 +41,10 @@ Requirements Contributors ~~~~~~~~~~~~ * Christian Orner (:ghuser:`chrisorner`) +* Marcus Boumans (:ghuser:`bowie2211`) +* Saurabh Aneja (:ghuser:`spaneja`) +* Marcus Boumans (:ghuser:`bowie2211`) * Yu Xie (:ghuser:`xieyupku`) * Anton Driesse (:ghuser:`adriesse`) * Cliff Hansen (:ghuser:`cwhanse`) * Kevin Anderson (:ghuser:`kanderso-nrel`) -* Marcus Boumans (:ghuser:`bowie2211`) -* Saurabh Aneja (:ghuser:`spaneja`) -* Marcus Boumans (:ghuser:`bowie2211`) From c6f4a3ce9400079eb1c2dda46ecf967f3c0de8bd Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 31 Oct 2022 13:43:34 -0400 Subject: [PATCH 24/28] fix typo --- docs/sphinx/source/whatsnew/v0.9.4.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index 41117e2a61..967c5f71b1 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -41,7 +41,6 @@ Requirements Contributors ~~~~~~~~~~~~ * Christian Orner (:ghuser:`chrisorner`) -* Marcus Boumans (:ghuser:`bowie2211`) * Saurabh Aneja (:ghuser:`spaneja`) * Marcus Boumans (:ghuser:`bowie2211`) * Yu Xie (:ghuser:`xieyupku`) From 0c87df52226c9c6815cb5c589f396becbe8036d0 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 16 Nov 2022 16:07:54 -0500 Subject: [PATCH 25/28] Apply suggestions from code review Co-authored-by: Anton Driesse --- pvlib/iam.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 26347d5872..1d3ec2e97a 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -937,9 +937,9 @@ def fedis(aoi, n=1.5, n_ref=None): r0 = ((n_ref - 1) / (n_ref + 1)) ** 2 # correction coefficient for different indices of refraction - transmittance_ratio = (1 - rd0) / (1 - r0) + normal_transmittance_ratio = (1 - rd0) / (1 - r0) - return transmittance_ratio * iam_physical + return normal_transmittance_ratio * iam_physical def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): @@ -947,9 +947,10 @@ def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): Determine the incidence angle modifiers (IAM) for diffuse sky and and ground-reflected radiation using the FEDIS transmittance model. - The "Fresnel Equations" for Diffuse radiation on Inclined photovoltaic - Surfaces (FEDIS) [1]_ is the result of analytical integration - the Schlick approximation [2]_ to the Fresnel equations. +This model scales the :py:func:`schlick_diffuse` output using a +polynomial to approximate the influence of refractive index n. +An additional scaling factor is applied if ``n_ref`` is not equal to ``n``. + Parameters ---------- @@ -1001,9 +1002,9 @@ def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): cuk, cug = schlick_diffuse(surface_tilt) # weighting function - # note that this expression for term1 is algebraically equivalent to - # the "transmittance_ratio" in fedis() - term1 = n*(n_ref+1)**2 / (n_ref*(n+1)**2) + # note that the following line is algebraically equivalent to + # sequence of calculations for the "normal_transmittance_ratio" found in fedis() + normal_transmittance_ratio = n*(n_ref+1)**2 / (n_ref*(n+1)**2) # note: the last coefficient here differs in sign from the reference polycoeffs = [2.77526e-09, 3.74953, -5.18727, 3.41186, -1.08794, 0.136060] term2 = np.polynomial.polynomial.polyval(n, polycoeffs) From db27aa15dbac746cfa18aeecce12c9678a5d9c35 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 16 Nov 2022 16:11:18 -0500 Subject: [PATCH 26/28] stickler --- pvlib/iam.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 1d3ec2e97a..1fbbdf2048 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -947,9 +947,10 @@ def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): Determine the incidence angle modifiers (IAM) for diffuse sky and and ground-reflected radiation using the FEDIS transmittance model. -This model scales the :py:func:`schlick_diffuse` output using a -polynomial to approximate the influence of refractive index n. -An additional scaling factor is applied if ``n_ref`` is not equal to ``n``. + This model scales the :py:func:`schlick_diffuse` output using a + polynomial to approximate the influence of refractive index n. + An additional scaling factor is applied if ``n_ref`` is not equal + to ``n``. Parameters @@ -1003,7 +1004,8 @@ def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): # weighting function # note that the following line is algebraically equivalent to - # sequence of calculations for the "normal_transmittance_ratio" found in fedis() + # sequence of calculations for the "normal_transmittance_ratio" found + # in fedis() normal_transmittance_ratio = n*(n_ref+1)**2 / (n_ref*(n+1)**2) # note: the last coefficient here differs in sign from the reference polycoeffs = [2.77526e-09, 3.74953, -5.18727, 3.41186, -1.08794, 0.136060] From a6ec2d073672a5bcc907fc582a57ed2da598d7aa Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 16 Nov 2022 16:17:53 -0500 Subject: [PATCH 27/28] fix missed variable rename --- pvlib/iam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 1fbbdf2048..5d5b072b4a 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -1010,7 +1010,7 @@ def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): # note: the last coefficient here differs in sign from the reference polycoeffs = [2.77526e-09, 3.74953, -5.18727, 3.41186, -1.08794, 0.136060] term2 = np.polynomial.polynomial.polyval(n, polycoeffs) - w = term1 * term2 # Eq 5 + w = normal_transmittance_ratio * term2 # Eq 5 cuk = cuk * w cug = cug * w From 39a28e29be77e1d710b9f479f8d9bbfb05b109d6 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 17 Nov 2022 12:12:53 -0500 Subject: [PATCH 28/28] remove fedis functions --- docs/sphinx/source/reference/pv_modeling.rst | 2 - docs/sphinx/source/whatsnew/v0.9.4.rst | 7 +- pvlib/iam.py | 149 +------------------ pvlib/tests/test_iam.py | 73 --------- 4 files changed, 4 insertions(+), 227 deletions(-) diff --git a/docs/sphinx/source/reference/pv_modeling.rst b/docs/sphinx/source/reference/pv_modeling.rst index 18e7b4e9a9..0f33cf8c70 100644 --- a/docs/sphinx/source/reference/pv_modeling.rst +++ b/docs/sphinx/source/reference/pv_modeling.rst @@ -30,8 +30,6 @@ Incident angle modifiers iam.marion_integrate iam.schlick iam.schlick_diffuse - iam.fedis - iam.fedis_diffuse PV temperature models --------------------- diff --git a/docs/sphinx/source/whatsnew/v0.9.4.rst b/docs/sphinx/source/whatsnew/v0.9.4.rst index 8096d17099..93d056e5a7 100644 --- a/docs/sphinx/source/whatsnew/v0.9.4.rst +++ b/docs/sphinx/source/whatsnew/v0.9.4.rst @@ -10,10 +10,9 @@ Deprecations Enhancements ~~~~~~~~~~~~ * Multiple code style issues fixed that were reported by LGTM analysis. (:issue:`1275`, :pull:`1559`) -* Added two direct IAM models :py:func:`pvlib.iam.schlick` and :py:func:`pvlib.iam.fedis`, - both of which can be used with :py:func:`~pvlib.iam.marion_diffuse` (:pull:`1562`, :issue:`1564`) -* Added two diffuse IAM models :py:func:`pvlib.iam.schlick_diffuse` and - :py:func:`pvlib.iam.fedis_diffuse` (:pull:`1562`) +* Added a direct IAM model :py:func:`pvlib.iam.schlick` which can be used with + :py:func:`~pvlib.iam.marion_diffuse`, and a diffuse IAM model + :py:func:`pvlib.iam.schlick_diffuse` (:pull:`1562`, :issue:`1564`) * Added a function to calculate one of GHI, DHI, and DNI from values of the other two. :py:func:`~pvlib.irradiance.complete_irradiance` (:issue:`1565`, :pull:`1567`) diff --git a/pvlib/iam.py b/pvlib/iam.py index 5d5b072b4a..6e60b6f97d 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -541,7 +541,7 @@ def marion_diffuse(model, surface_tilt, **kwargs): ---------- model : str The IAM function to evaluate across solid angle. Must be one of - `'ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick', 'fedis'`. + `'ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick'`. surface_tilt : numeric Surface tilt angles in decimal degrees. @@ -593,7 +593,6 @@ def marion_diffuse(model, surface_tilt, **kwargs): 'sapm': sapm, 'martin_ruiz': martin_ruiz, 'schlick': schlick, - 'fedis': fedis, } try: @@ -791,7 +790,6 @@ def schlick(aoi): See Also -------- - pvlib.iam.fedis pvlib.iam.schlick_diffuse """ iam = 1 - (1 - cosd(aoi)) ** 5 @@ -843,7 +841,6 @@ def schlick_diffuse(surface_tilt): See Also -------- pvlib.iam.schlick - pvlib.iam.fedis_diffuse """ # these calculations are as in [2]_, but with the refractive index # weighting coefficient w set to 1.0 (so it is omitted) @@ -872,147 +869,3 @@ def schlick_diffuse(surface_tilt): cug = pd.Series(cug, surface_tilt.index) return cuk, cug - - -def fedis(aoi, n=1.5, n_ref=None): - """ - Determine the incidence angle modifier (IAM) for direct irradiance - using the FEDIS transmittance model. - - Note that FEDIS [1]_ calculates direct IAM using the Fresnel equations - without extinction, thus in the default case of ``n_ref=None``, this is - equivalent to calling :py:func:`physical` with ``K=0``. - - Parameters - ---------- - aoi : numeric - Angle of incidence. [degrees] - - n : float, default 1.5 - Refractive index of the PV front surface material. The default value - of 1.5 was used for an IMT reference cell in [1]_. [unitless] - - n_ref : float, optional - Refractive index of the surface material for the reference device - measuring irradiance at normal incidence. Mathematically, this is - the value of ``n`` for which IAM=1.0 at normal incidence. - If None (default), set equal to ``n``. See Notes for - possible use cases. [unitless] - - Returns - ------- - iam : numeric - The incident angle modifier. - - See Also - -------- - pvlib.iam.physical - - Notes - ----- - In most PV applications it is appropriate to set ``n_ref=n`` - (the default behavior), but a custom ``n_ref`` may be useful when - using irradiance data from a pyranometer whose calibration does not - account for reflection off its glass dome. It may also be used as - the original index of refraction if the module surface's refractive - index is changing over time (for example a degraded AR coating). - - References - ---------- - .. [1] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations' - for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)", - Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022. - :doi:`10.1016/j.rser.2022.112362` - """ - iam_physical = physical(aoi, n, K=0, L=0) - - if n_ref is None: - return iam_physical - - else: - # reflectance for normal incidence with refractive index n: - rd0 = ((n - 1) / (n + 1)) ** 2 - - # reflectance for normal incidence with refractive index n_ref: - r0 = ((n_ref - 1) / (n_ref + 1)) ** 2 - - # correction coefficient for different indices of refraction - normal_transmittance_ratio = (1 - rd0) / (1 - r0) - - return normal_transmittance_ratio * iam_physical - - -def fedis_diffuse(surface_tilt, n=1.5, n_ref=None): - """ - Determine the incidence angle modifiers (IAM) for diffuse sky and - and ground-reflected radiation using the FEDIS transmittance model. - - This model scales the :py:func:`schlick_diffuse` output using a - polynomial to approximate the influence of refractive index n. - An additional scaling factor is applied if ``n_ref`` is not equal - to ``n``. - - - Parameters - ---------- - surface_tilt : numeric - Surface tilt angle measured from horizontal (e.g. surface facing - up = 0, surface facing horizon = 90). [degrees] - - n : float, default 1.5 - Refractive index of the PV front surface material. The default value - of 1.5 was used for an IMT reference cell in [1]_. [unitless] - - n_ref : float, optional - Refractive index of the surface material for the reference device - measuring irradiance at normal incidence. Mathematically, this is - the value of ``n`` for which IAM=1.0 at normal incidence. - If None (default), set equal to ``n``. See :py:func:`fedis` for - more information regardings its use. [unitless] - - Returns - ------- - iam_sky : numeric - The incident angle modifier for sky diffuse. - - iam_ground : numeric - The incident angle modifier for ground-reflected diffuse. - - See Also - -------- - pvlib.iam.schlick_diffuse - - Notes - ----- - This implementation corrects a typo in [1]_ regarding the sign - of the last polynomial term in Equation 5. - - References - ---------- - .. [1] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations' - for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)", - Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022. - :doi:`10.1016/j.rser.2022.112362` - - .. [2] Schlick, C. An inexpensive BRDF model for physically-based - rendering. Computer graphics forum 13 (1994). - """ - if n_ref is None: - n_ref = n - - cuk, cug = schlick_diffuse(surface_tilt) - - # weighting function - # note that the following line is algebraically equivalent to - # sequence of calculations for the "normal_transmittance_ratio" found - # in fedis() - normal_transmittance_ratio = n*(n_ref+1)**2 / (n_ref*(n+1)**2) - # note: the last coefficient here differs in sign from the reference - polycoeffs = [2.77526e-09, 3.74953, -5.18727, 3.41186, -1.08794, 0.136060] - term2 = np.polynomial.polynomial.polyval(n, polycoeffs) - w = normal_transmittance_ratio * term2 # Eq 5 - - cuk = cuk * w - cug = cug * w - - return cuk, cug diff --git a/pvlib/tests/test_iam.py b/pvlib/tests/test_iam.py index 323c2dba52..df4d9ee877 100644 --- a/pvlib/tests/test_iam.py +++ b/pvlib/tests/test_iam.py @@ -367,76 +367,3 @@ def test_schlick_diffuse(): assert_series_equal(pd.Series(expected_sky, idx), actual_sky) assert_series_equal(pd.Series(expected_ground, idx), actual_ground, rtol=1e-6) - - -def test_fedis_defaults(): - idx = pd.date_range('2019-01-01', freq='h', periods=9) - aoi = pd.Series([-180, -135, -90, -45, 0, 45, 90, 135, 180], idx) - expected = pd.Series([0, 0, 0, 0.989333426, 1, 0.989333426, 0, 0, 0], idx) - - # numpy arrays - actual = _iam.fedis(aoi.values) - assert_allclose(expected, expected, atol=1e-6) - - # scalars - for i in range(len(aoi)): - actual = _iam.fedis(aoi[i]) - assert_allclose(expected[i], actual, atol=1e-6) - - # pandas Series - actual = _iam.fedis(aoi) - assert_series_equal(expected, actual) - - -def test_fedis_kwargs(): - # custom n and n_ref - aoi = np.array([0, 30, 60, 90]) - # expected values generated with code from model authors: - # https://github.com/NREL/FEDIS/commit/7ae7186caa39aa85848163a39dac46df56fb9819 # noqa: E501 - expected = np.array([0.948928986, 0.947089588, 0.894889901, 0.0]) - actual = _iam.fedis(aoi, n=1.7, n_ref=1.3) - assert_allclose(expected, actual, atol=1e-6) - - -def test_fedis_diffuse_defaults(): - idx = pd.date_range('2019-01-01', freq='h', periods=3) - surface_tilt = pd.Series([0, 20, 90], idx) - - # expected values generated with code from model authors: - # https://github.com/NREL/FEDIS/commit/7ae7186caa39aa85848163a39dac46df56fb9819 # noqa: E501 - expected = { - 'sky': pd.Series([0.946166074, 0.956218435, 0.946166074], idx), - 'ground': pd.Series([0.0, 0.62284759, 0.946166074], idx), - } - - # numpy arrays - actual_sky, actual_ground = _iam.fedis_diffuse(surface_tilt.values) - assert_allclose(expected['sky'], actual_sky) - assert_allclose(expected['ground'], actual_ground) - - # scalars - for i in range(len(surface_tilt)): - actual_sky, actual_ground = _iam.fedis_diffuse(surface_tilt[i]) - assert_allclose(expected['sky'][i], actual_sky) - assert_allclose(expected['ground'][i], actual_ground) - - # pandas Series - actual_sky, actual_ground = _iam.fedis_diffuse(surface_tilt) - assert_series_equal(expected['sky'], actual_sky) - assert_series_equal(expected['ground'], actual_ground) - - -def test_fedis_diffuse_kwargs(): - # custom n and n_ref - surface_tilt = np.array([0, 30, 60, 90]) - # expected values generated with code from model authors: - # https://github.com/NREL/FEDIS/commit/7ae7186caa39aa85848163a39dac46df56fb9819 # noqa: E501 - expected = { - 'sky': np.array([0.89536659, 0.90815772, 0.90728496, 0.89536659]), - 'ground': np.array([0.0, 0.717209232, 0.859611482, 0.895366593]), - } - # numpy arrays - actual_sky, actual_ground = _iam.fedis_diffuse(surface_tilt, - n=1.7, n_ref=1.3) - assert_allclose(expected['sky'], actual_sky) - assert_allclose(expected['ground'], actual_ground, atol=1e-6)