Skip to content

Commit a19b528

Browse files
authored
[ENH] extend iam.physical with optional n_ar parameter for AR coating (#1616)
* [ENH] extend iam.physical with optional n_ar parameter for AR coating * [CLN] resolve stickler-ci failures; [ENH] ensure inputs could be vectors * [DOC] iam.physical: update docstring and add whatsnew entry * [CLN] minor non-functional change to improve readability * revert latest commit * apply suggested changes * update docstring, add contributors
1 parent 55e0577 commit a19b528

File tree

3 files changed

+94
-49
lines changed

3 files changed

+94
-49
lines changed

docs/sphinx/source/whatsnew/v0.9.5.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ Deprecations
1717
Enhancements
1818
~~~~~~~~~~~~
1919

20+
* Added optional ``n_ar`` parameter to :py:func:`pvlib.iam.physical` to
21+
support an anti-reflective coating. (:issue:`1501`, :pull:`1616`)
2022

2123
Bug fixes
2224
~~~~~~~~~
@@ -50,3 +52,7 @@ Contributors
5052
* Will Holmgren (:ghuser:`wholmgren`)
5153
* Adam R. Jensen (:ghuser:`adamrjensen`)
5254
* Pratham Chauhan (:ghuser:`ooprathamm`)
55+
* Karel De Brabandere (:ghuser:`kdebrab`)
56+
* Mark Mikofski (:ghuser:`mikofski`)
57+
* Anton Driesse (:ghuser:`adriesse`)
58+
* Adam R. Jensen (:ghuser:`AdamRJensen`)

pvlib/iam.py

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import numpy as np
1212
import pandas as pd
1313
import functools
14-
from pvlib.tools import cosd, sind, tand, asind
14+
from pvlib.tools import cosd, sind
1515

1616
# a dict of required parameter names for each IAM model
1717
# keys are the function names for the IAM models
@@ -91,21 +91,22 @@ def ashrae(aoi, b=0.05):
9191
return iam
9292

9393

94-
def physical(aoi, n=1.526, K=4., L=0.002):
94+
def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):
9595
r"""
9696
Determine the incidence angle modifier using refractive index ``n``,
97-
extinction coefficient ``K``, and glazing thickness ``L``.
97+
extinction coefficient ``K``, glazing thickness ``L`` and refractive
98+
index ``n_ar`` of an optional anti-reflective coating.
9899
99100
``iam.physical`` calculates the incidence angle modifier as described in
100-
[1]_, Section 3. The calculation is based on a physical model of absorbtion
101+
[1]_, Section 3, with additional support of an anti-reflective coating.
102+
The calculation is based on a physical model of reflections, absorption,
101103
and transmission through a transparent cover.
102104
103105
Parameters
104106
----------
105107
aoi : numeric
106108
The angle of incidence between the module normal vector and the
107-
sun-beam vector in degrees. Angles of 0 are replaced with 1e-06
108-
to ensure non-nan results. Angles of nan will result in nan.
109+
sun-beam vector in degrees. Angles of nan will result in nan.
109110
110111
n : numeric, default 1.526
111112
The effective index of refraction (unitless). Reference [1]_
@@ -121,6 +122,11 @@ def physical(aoi, n=1.526, K=4., L=0.002):
121122
indicates that 0.002 meters (2 mm) is reasonable for most
122123
glass-covered PV panels.
123124
125+
n_ar : numeric, optional
126+
The effective index of refraction of the anti-reflective (AR) coating
127+
(unitless). If n_ar is None (default), no AR coating is applied.
128+
A typical value for the effective index of an AR coating is 1.29.
129+
124130
Returns
125131
-------
126132
iam : numeric
@@ -149,48 +155,65 @@ def physical(aoi, n=1.526, K=4., L=0.002):
149155
pvlib.iam.interp
150156
pvlib.iam.sapm
151157
"""
152-
zeroang = 1e-06
153-
154-
# hold a new reference to the input aoi object since we're going to
155-
# overwrite the aoi reference below, but we'll need it for the
156-
# series check at the end of the function
157-
aoi_input = aoi
158-
159-
aoi = np.where(aoi == 0, zeroang, aoi)
160-
161-
# angle of reflection
162-
thetar_deg = asind(1.0 / n * (sind(aoi)))
163-
164-
# reflectance and transmittance for normal incidence light
165-
rho_zero = ((1-n) / (1+n)) ** 2
166-
tau_zero = np.exp(-K*L)
167-
168-
# reflectance for parallel and perpendicular polarized light
169-
rho_para = (tand(thetar_deg - aoi) / tand(thetar_deg + aoi)) ** 2
170-
rho_perp = (sind(thetar_deg - aoi) / sind(thetar_deg + aoi)) ** 2
171-
172-
# transmittance for non-normal light
173-
tau = np.exp(-K * L / cosd(thetar_deg))
174-
175-
# iam is ratio of non-normal to normal incidence transmitted light
176-
# after deducting the reflected portion of each
177-
iam = ((1 - (rho_para + rho_perp) / 2) / (1 - rho_zero) * tau / tau_zero)
178-
179-
with np.errstate(invalid='ignore'):
180-
# angles near zero produce nan, but iam is defined as one
181-
small_angle = 1e-06
182-
iam = np.where(np.abs(aoi) < small_angle, 1.0, iam)
183-
184-
# angles at 90 degrees can produce tiny negative values,
185-
# which should be zero. this is a result of calculation precision
186-
# rather than the physical model
187-
iam = np.where(iam < 0, 0, iam)
188-
189-
# for light coming from behind the plane, none can enter the module
190-
iam = np.where(aoi > 90, 0, iam)
191-
192-
if isinstance(aoi_input, pd.Series):
193-
iam = pd.Series(iam, index=aoi_input.index)
158+
n1, n3 = 1, n
159+
if n_ar is None or np.allclose(n_ar, n1):
160+
# no AR coating
161+
n2 = n
162+
else:
163+
n2 = n_ar
164+
165+
# incidence angle
166+
costheta = np.maximum(0, cosd(aoi)) # always >= 0
167+
sintheta = np.sqrt(1 - costheta**2) # always >= 0
168+
n1costheta1 = n1 * costheta
169+
n2costheta1 = n2 * costheta
170+
171+
# refraction angle of first interface
172+
sintheta = n1 / n2 * sintheta
173+
costheta = np.sqrt(1 - sintheta**2)
174+
n1costheta2 = n1 * costheta
175+
n2costheta2 = n2 * costheta
176+
177+
# reflectance of s-, p-polarized, and normal light by the first interface
178+
rho12_s = ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2
179+
rho12_p = ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2
180+
rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2
181+
182+
# transmittance through the first interface
183+
tau_s = 1 - rho12_s
184+
tau_p = 1 - rho12_p
185+
tau_0 = 1 - rho12_0
186+
187+
if not np.allclose(n3, n2): # AR coated glass
188+
n3costheta2 = n3 * costheta
189+
# refraction angle of second interface
190+
sintheta = n2 / n3 * sintheta
191+
costheta = np.sqrt(1 - sintheta**2)
192+
n2costheta3 = n2 * costheta
193+
n3costheta3 = n3 * costheta
194+
195+
# reflectance by the second interface
196+
rho23_s = (
197+
(n2costheta2 - n3costheta3) / (n2costheta2 + n3costheta3)
198+
) ** 2
199+
rho23_p = (
200+
(n2costheta3 - n3costheta2) / (n2costheta3 + n3costheta2)
201+
) ** 2
202+
rho23_0 = ((n2 - n3) / (n2 + n3)) ** 2
203+
204+
# transmittance through the coating, including internal reflections
205+
# 1 + rho23*rho12 + (rho23*rho12)^2 + ... = 1/(1 - rho23*rho12)
206+
tau_s *= (1 - rho23_s) / (1 - rho23_s * rho12_s)
207+
tau_p *= (1 - rho23_p) / (1 - rho23_p * rho12_p)
208+
tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)
209+
210+
# transmittance after absorption in the glass
211+
tau_s *= np.exp(-K * L / costheta)
212+
tau_p *= np.exp(-K * L / costheta)
213+
tau_0 *= np.exp(-K * L)
214+
215+
# incidence angle modifier
216+
iam = (tau_s + tau_p) / 2 / tau_0
194217

195218
return iam
196219

pvlib/tests/test_iam.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def test_physical():
4242
expected = np.array([0, 0.8893998, 0.98797788, 0.99926198, 1, 0.99926198,
4343
0.98797788, 0.8893998, 0, np.nan])
4444
iam = _iam.physical(aoi, 1.526, 0.002, 4)
45-
assert_allclose(iam, expected, equal_nan=True)
45+
assert_allclose(iam, expected, atol=1e-7, equal_nan=True)
4646

4747
# GitHub issue 397
4848
aoi = pd.Series(aoi)
@@ -51,6 +51,22 @@ def test_physical():
5151
assert_series_equal(iam, expected)
5252

5353

54+
def test_physical_ar():
55+
aoi = np.array([0, 22.5, 45, 67.5, 90, 100, np.nan])
56+
expected = np.array([1, 0.99944171, 0.9917463, 0.91506158, 0, 0, np.nan])
57+
iam = _iam.physical(aoi, n_ar=1.29)
58+
assert_allclose(iam, expected, atol=1e-7, equal_nan=True)
59+
60+
61+
def test_physical_noar():
62+
aoi = np.array([0, 22.5, 45, 67.5, 90, 100, np.nan])
63+
expected = _iam.physical(aoi)
64+
iam0 = _iam.physical(aoi, n_ar=1)
65+
iam1 = _iam.physical(aoi, n_ar=1.526)
66+
assert_allclose(iam0, expected, equal_nan=True)
67+
assert_allclose(iam1, expected, equal_nan=True)
68+
69+
5470
def test_physical_scalar():
5571
aoi = -45.
5672
iam = _iam.physical(aoi, 1.526, 0.002, 4)

0 commit comments

Comments
 (0)