Skip to content

Incidence Angle Modifier zero instead of np.nan (#338) #339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Aug 1, 2017
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ def ashraeiam(aoi, b=0.05):
The incident angle modifier calculated as 1-b*(sec(aoi)-1) as
described in [2,3].

Returns nan for all abs(aoi) >= 90 and for all IAM values that
Returns zeros for all abs(aoi) >= 90 and for all IAM values that
would be less than 0.

References
Expand All @@ -732,7 +732,7 @@ def ashraeiam(aoi, b=0.05):

iam = 1 - b*((1/np.cos(np.radians(aoi)) - 1))

iam = np.where(np.abs(aoi) >= 90, np.nan, iam)
iam = np.where(np.abs(aoi) >= 90, 0, iam)
iam = np.maximum(0, iam)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the np.maximum line still necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is, because for angles close to +/-90° the function would return a negative value for the iam (the intercept for b=0.05 is at 87.21°: https://www.desmos.com/calculator/y8wvzrx3b9).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, the np.maximum line is needed


if isinstance(iam, pd.Series):
Expand Down Expand Up @@ -761,7 +761,8 @@ def physicaliam(aoi, n=1.526, K=4., L=0.002):
----------
aoi : numeric
The angle of incidence between the module normal vector and the
sun-beam vector in degrees.
sun-beam vector in degrees. Angles of 0 are replaced with 1e-06
to ensure non-nan results.

n : numeric
The effective index of refraction (unitless). Reference [1]
Expand Down Expand Up @@ -811,6 +812,10 @@ def physicaliam(aoi, n=1.526, K=4., L=0.002):
spa
ashraeiam
'''
zeroang = 1e-06

aoi = np.where(aoi == 0, zeroang, aoi)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you add this line? I understand what it does, but not why we need it now when we didn't previously.

It's not clear to me that we actually need zeroang at all. I think its only purpose is to avoid infs, but maybe those be more cleanly handled with np.where or similar.

Copy link
Contributor Author

@jkfm jkfm Jun 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understood it, if the input angle is 0, tau will be nan, which is why we substitute it with zeroang.
As iam is defined as tau(theta) / tau(0°), we need zeroang anyway to calculate tau(0°). That's why I moved the variable definition to the beginning of the function.

I guess we could define tau0 as a constant in the function and handle aoi == 0 with np.where after the calculations are done, but I feel like the current function is more intuitive to understand.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should keep zeroang and for input theta < zeroang, physicaliam should return tau0. Passing theta=0 through will return nan rather than zero. Line 821 computes tau with the leading factor exp(-KL/cos(theta)) which is why we should return tau0 for small theta.

tau0 is computed awkwardly. I'd replace line 829 - 833 with the limit value which we should have implemented in Matlab:
tau0 = exp(-KL)*(1 - ((1-n)/(1+n))^2)
We used zeroang instead in Matlab which is probably why its in pvlib-python

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, now I see that you've changed the test so that an input of 0 returns 1 instead of np.nan. I should have caught this when I wrote the test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am surprised that the model would not work for an angle of exactly zero degrees. Could this have something to do with the equation discrepancy mentioned in the comments further above?

Copy link
Contributor Author

@jkfm jkfm Jul 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it has to do with the function discrepancy. The unmodified function for theta_r is not defined for angles > ~40°.

The tau(theta)-function in turn simply has a discontinuity for theta = 0. It seems this was ignored in the paper...


thetar_deg = tools.asind(1.0 / n*(tools.sind(aoi)))

tau = (np.exp(- 1.0 * (K*L / tools.cosd(thetar_deg))) *
Expand All @@ -819,8 +824,6 @@ def physicaliam(aoi, n=1.526, K=4., L=0.002):
((tools.tand(thetar_deg - aoi)) ** 2) /
((tools.tand(thetar_deg + aoi)) ** 2))))))

zeroang = 1e-06

thetar_deg0 = tools.asind(1.0 / n*(tools.sind(zeroang)))

tau0 = (np.exp(- 1.0 * (K*L / tools.cosd(thetar_deg0))) *
Expand All @@ -831,7 +834,7 @@ def physicaliam(aoi, n=1.526, K=4., L=0.002):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use
tau0 = (np.exp(- 1.0 * (K*L)) * (1 - (1 - n)^2/(1 + n)^2)
instead of the expression on line 826. I'd switch to tau0 when thetar_deg<zeroing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your suggestions, especially after having consulted my D&B book. I would suggest calculating reflectance and absorption separately before combining them in order to clarify the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I applied both your suggestions correctly. Could you take a look?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't quite correct, so I thought it might be easier to communicate it this way:

    # angle of refraction
    thetar_deg = tools.asind(tools.sind(aoi) / n)

    # reflectance and transmittance for normal incidence light
    rho_zero = ((1-n) / (1+n)) ** 2
    tau_zero = np.exp(-K*L)

    # reflectance for parallel and perpendicular polarized light
    rho_para = (tools.tand(thetar_deg - aoi) /
                tools.tand(thetar_deg + aoi)) ** 2
    rho_perp = (tools.sind(thetar_deg - aoi) /
                tools.sind(thetar_deg + aoi)) ** 2

    # transmittance for non-normal light
    tau = np.exp(-K*L / tools.cosd(thetar_deg))

    # iam is ratio of non-normal to normal incidence transmitted light
    # after deducting the reflected portion of each
    iam = ((1 - (rho_para + rho_perp) / 2) / (1 - rho_zero)
          * tau / tau_zero )


    # angles near zero produce nan, but iam is defined as one
    small_angle = 1e-06
    iam = np.where(np.abs(aoi) < small_angle, 1.0, iam)

    # angles at 90 degrees can produce tiny negative values, which should be zero
    # this is a result of calculation precision rather than the physical model
    iam = np.where(iam <  0, 0, iam)    

    # for light coming from behind the plane, none can enter the module
    iam = np.where(aoi > 90, 0, iam)

    # negative angles are not logical
    iam = np.where(aoi <  0, np.nan, iam)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation! So does this refer to reference 2 in the Docstring (I don't have access so cannot check)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref [1] simply describes what is in ref [2].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented your suggestion except for setting negative angles to np.nan, to keep consistency with the other IAM functions for now...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistency has some appeal...

iam = tau / tau0

iam = np.where((np.abs(aoi) >= 90) | (iam < 0), np.nan, iam)
iam = np.where((np.abs(aoi) >= 90) | (iam < 0), 0, iam)

if isinstance(aoi, pd.Series):
iam = pd.Series(iam, index=aoi.index)
Expand Down Expand Up @@ -1462,7 +1465,7 @@ def sapm_aoi_loss(aoi, module, upper=None):
----------
aoi : numeric
Angle of incidence in degrees. Negative input angles will return
nan values.
zeros.

module : dict-like
A dict, Series, or DataFrame defining the SAPM performance
Expand Down Expand Up @@ -1504,7 +1507,7 @@ def sapm_aoi_loss(aoi, module, upper=None):

aoi_loss = np.polyval(aoi_coeff, aoi)
aoi_loss = np.clip(aoi_loss, 0, upper)
aoi_loss = np.where(aoi < 0, np.nan, aoi_loss)
aoi_loss = np.where(aoi < 0, 0, aoi_loss)

if isinstance(aoi, pd.Series):
aoi_loss = pd.Series(aoi_loss, aoi.index)
Expand Down
18 changes: 9 additions & 9 deletions pvlib/test/test_pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ def test_systemdef_dict():
def test_ashraeiam():
thetas = np.linspace(-90, 90, 9)
iam = pvsystem.ashraeiam(thetas, .05)
expected = np.array([ nan, 0.9193437 , 0.97928932, 0.99588039, 1. ,
0.99588039, 0.97928932, 0.9193437 , nan])
expected = np.array([ 0, 0.9193437 , 0.97928932, 0.99588039, 1. ,
0.99588039, 0.97928932, 0.9193437 , 0])
assert_allclose(iam, expected, equal_nan=True)


Expand All @@ -107,17 +107,17 @@ def test_PVSystem_ashraeiam():
system = pvsystem.PVSystem(module_parameters=module_parameters)
thetas = np.linspace(-90, 90, 9)
iam = system.ashraeiam(thetas)
expected = np.array([ nan, 0.9193437 , 0.97928932, 0.99588039, 1. ,
0.99588039, 0.97928932, 0.9193437 , nan])
expected = np.array([ 0, 0.9193437 , 0.97928932, 0.99588039, 1. ,
0.99588039, 0.97928932, 0.9193437 , 0])
assert_allclose(iam, expected, equal_nan=True)


@needs_numpy_1_10
def test_physicaliam():
thetas = np.linspace(-90, 90, 9)
iam = pvsystem.physicaliam(thetas, 1.526, 0.002, 4)
expected = np.array([ nan, 0.8893998 , 0.98797788, 0.99926198, nan,
0.99926198, 0.98797788, 0.8893998 , nan])
expected = np.array([ 0, 0.8893998 , 0.98797788, 0.99926198, 1,
0.99926198, 0.98797788, 0.8893998 , 0])
assert_allclose(iam, expected, equal_nan=True)


Expand All @@ -127,8 +127,8 @@ def test_PVSystem_physicaliam():
system = pvsystem.PVSystem(module_parameters=module_parameters)
thetas = np.linspace(-90, 90, 9)
iam = system.physicaliam(thetas)
expected = np.array([ nan, 0.8893998 , 0.98797788, 0.99926198, nan,
0.99926198, 0.98797788, 0.8893998 , nan])
expected = np.array([ 0, 0.8893998 , 0.98797788, 0.99926198, 1,
0.99926198, 0.98797788, 0.8893998 , 0])
assert_allclose(iam, expected, equal_nan=True)


Expand Down Expand Up @@ -239,7 +239,7 @@ def test_PVSystem_sapm_spectral_loss(sapm_module_params):
@pytest.mark.parametrize('aoi,expected', [
(45, 0.9975036250000002),
(np.array([[-30, 30, 100, np.nan]]),
np.array([[np.nan, 1.007572, 0, np.nan]])),
np.array([[0, 1.007572, 0, np.nan]])),
(pd.Series([80]), pd.Series([0.597472]))
])
def test_sapm_aoi_loss(sapm_module_params, aoi, expected):
Expand Down