Skip to content

Commit 6e9aaf0

Browse files
committed
add linke turbidity formulas module
* add kasten 1996 pyrheliometric formula for linke turbidity factor * move atmosphere into package, just for fun, why not? * make __init__ with same api as before Signed-off-by: Mark Mikofski <[email protected]>
1 parent 730f48e commit 6e9aaf0

File tree

4 files changed

+8921
-0
lines changed

4 files changed

+8921
-0
lines changed

pvlib/atmosphere/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""atmospheric equations"""
2+
3+
from pvlib.atmosphere.core import (
4+
absoluteairmass, alt2pres, first_solar_spectral_correction, gueymard94_pw,
5+
pres2alt, relativeairmass, AIRMASS_MODELS, APPARENT_ZENITH_MODELS,
6+
TRUE_ZENITH_MODELS
7+
)
8+
from pvlib.atmosphere.linke_turb_forms import kasten_96lt
File renamed without changes.

pvlib/atmosphere/linke_turb_forms.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Linke turbidity factor calculated from AOD, Pwat and AM"""
2+
3+
from datetime import datetime
4+
import pandas as pd
5+
import numpy as np
6+
import pvlib
7+
from solar_utils import *
8+
from matplotlib import pyplot as plt
9+
import seaborn as sns
10+
import os
11+
12+
plt.ion()
13+
sns.set_context(rc={'figure.figsize': (12, 8)})
14+
15+
def kasten_96lt(aod, am, pwat, alpha0=1.14, method='Molineaux'):
16+
"""
17+
Calculate Linke turbidity factor using Kasten pyrheliometric formula (1980).
18+
19+
:param aod: aerosol optical depth table or value at 500
20+
:param am: airmass, pressure corrected in atmospheres
21+
:param pwat: precipitable water or total column water vapor in centimeters
22+
:param alpha0: Angstrom turbidity alpha exponent, default is 1.14
23+
:param method: Molineaux (default) or Bird-Huldstrom
24+
:return: Linke turbidity
25+
26+
Aerosol optical depth can be given as a list of tuples with wavelength in
27+
nanometers as the first item in each tuple and values as AOD as the second
28+
item. The list must be in ascending order by wavelength. If ``aod`` is given
29+
as a sequence of floats or as a float then a wavelength of 500[nm] will be
30+
used and alpha will default to 1.14, unless alpha is also given. Otherwise
31+
alpha is calculated from the given wavelength and aod.
32+
33+
Method can be either ``'Molineaux'`` or ``'Bird-Huldstrom'``. Airmass less
34+
than 1 or greater than 6 will return ``NaN``. Precipitable water less than zero
35+
or greater than 5[cm] will also return ``NaN``.
36+
"""
37+
# calculate Angstrom turbidity alpha exponent if not known, from AOD at two
38+
# wavelengths, lambda1 and lambda2
39+
alpha = []
40+
try:
41+
# xrange(0) means iterate zero times, xrange(negative) == xrange(0)
42+
for idx in xrange(len(aod) - 1):
43+
lambda1, aod1 = aod[idx]
44+
lambda2, aod2 = aod[idx + 1]
45+
alpha.append(-np.log(aod1 / aod2) / np.log(lambda1 / lambda2))
46+
except TypeError:
47+
# case 1: aod is a float, so len(aod) raises TypeError
48+
# case 2: aod is an array of float, so (lambda1, aod1) = aod[idx] raises
49+
# TypeError
50+
aod = [(500.0, aod)]
51+
else:
52+
# case 3: len(aod) == 1, then alpha == []
53+
if len(alpha) > 1:
54+
alpha0 = alpha
55+
# make sure alpha can be indexed
56+
try:
57+
alpha = list(alpha0)
58+
except TypeError:
59+
alpha = [alpha0]
60+
# make sure aod has lambda
61+
try:
62+
# case 3: len(aod) == 1 and aod == [aod]
63+
lambda1, aod1 = zip(*aod)
64+
except TypeError:
65+
aod = [(500.0, aod)]
66+
# From numerically integrated spectral simulations done with Modtran (Berk,
67+
# 1996), Molineaux (1998) obtained for the broadband optical depth of a
68+
# clean and dry atmopshere (fictious atmosphere that comprises only the
69+
# effects of Rayleigh scattering and absorption by the atmosphere gases
70+
# other than the water vapor) the following expression where am is airmass.
71+
delta_cda = -0.101 + 0.235 * am ** (-0.16)
72+
# The broadband water vapor optical depth where pwat is the precipitable
73+
# water vapor content of the atmosphere in [cm]. The precision of these fits
74+
# is better than 1% when compared with Modtran simulations in the range
75+
# 1 < am < 6 and 0 < pwat < 5 cm.
76+
delta_w = 0.112 * am ** (-0.55) * pwat ** (0.34)
77+
if method == 'Molineaux':
78+
# get aod at 700[nm] from alpha for Molineaux (1998)
79+
delta_a = get_aod_at_lambda(aod, alpha)
80+
else:
81+
# using (Bird-Hulstrom 1980)
82+
aod380 = get_aod_at_lambda(aod, alpha, 380.0)
83+
aod500 = get_aod_at_lambda(aod, alpha, 500.0)
84+
delta_a = 0.27583 * aod380 + 0.35 * aod500
85+
# the Linke turbidity at am using the Kasten pyrheliometric formula (1980)
86+
lt = -(9.4 + 0.9 * am) * np.log(
87+
np.exp(-am * (delta_cda + delta_w + delta_a))
88+
) / am
89+
# filter out of extrapolated values
90+
filter = (am < 1.0) | (am > 6.0) | (pwat < 0) | (pwat > 5.0)
91+
lt[filter] = np.nan # set out of range to NaN
92+
return lt
93+
94+
95+
def get_aod_at_lambda(aod, alpha, lambda0=700.0):
96+
"""
97+
Get AOD at specified wavelenth.
98+
99+
:param aod: sequence of (wavelength, aod) in ascending order by wavelength
100+
:param alpha: sequence of Angstrom alpha corresponding to wavelength in aod
101+
:param lambda0: desired wavelength in nanometers, defaults to 700[nm]
102+
"""
103+
lambda1, aod = zip(*aod)
104+
# lambda0 is between (idx - 1) and idx
105+
idx = np.searchsorted(lambda1, lambda0)
106+
# unless idx is zero
107+
if idx == 0:
108+
idx = 1
109+
return aod[idx - 1] * ((lambda0 / lambda1[idx - 1]) ** (-alpha[idx - 1]))
110+
111+
112+
def demo_kasten_96lt():
113+
atmos_path = os.path.dirname(os.path.abspath(__file__))
114+
pvlib_path = os.path.dirname(atmos_path)
115+
melbourne_fl = pvlib.tmy.readtmy3(os.path.join(
116+
pvlib_path, 'data', '722040TYA.CSV')
117+
)
118+
aod700 = melbourne_fl[0]['AOD']
119+
pwat_cm = melbourne_fl[0]['Pwat']
120+
press_mbar = melbourne_fl[0]['Pressure']
121+
dry_temp = melbourne_fl[0]['DryBulb']
122+
timestamps = melbourne_fl[0].index
123+
location = (melbourne_fl[1]['latitude'],
124+
melbourne_fl[1]['longitude'],
125+
melbourne_fl[1]['TZ'])
126+
_, airmass = zip(*[solposAM(
127+
location, d.timetuple()[:6], (press_mbar.loc[d], dry_temp.loc[d])
128+
) for d in timestamps])
129+
_, amp = zip(*airmass)
130+
amp = np.array(amp)
131+
filter = amp < 0
132+
amp[filter] = np.nan
133+
lt_molineaux = kasten_96lt(aod=[(700.0, aod700)], am=amp, pwat=pwat_cm)
134+
lt_bird_huldstrom = kasten_96lt(aod=[(700.0, aod700)], am=amp, pwat=pwat_cm,
135+
method='Bird-Huldstrom')
136+
t = pd.DatetimeIndex([datetime.replace(d, year=2016) for d in timestamps])
137+
lt_molineaux.index = t
138+
lt_bird_huldstrom.index = t
139+
pvlib.clearsky.lookup_linke_turbidity(t, *location[:2]).plot()
140+
lt_molineaux.resample('D').mean().plot()
141+
lt_bird_huldstrom.resample('D').mean().plot()
142+
title = ['Linke turbidity factor comparison at Melbourne, FL (722040TYA),',
143+
'calculated using Kasten Pyrheliometric formula']
144+
plt.title(' '.join(title))
145+
plt.ylabel('Linke turbidity factor')
146+
plt.legend(['Linke Turbidity', 'Molineaux', 'Bird-Huldstrom'])
147+
return lt_molineaux, lt_bird_huldstrom
148+
149+
150+
if __name__ == '__main__':
151+
lt_molineaux, lt_bird_huldstrom = demo_kasten_96lt()

0 commit comments

Comments
 (0)