Skip to content

Commit 51d7ecc

Browse files
authored
add 'nrel' spa option for ET irradiance, fix doy - 1 bug (#215)
* use extraradiation function in disc. creates doy-1 offset problem. add invalid model to extrarad * switch to doy-1. need new tests * add numba-capable nrel_earthsun_distance * add nrel option to extraradiation * update irradiance notebook with new dni_extra method * fix docstrings * add check_less_precise=2 to failing comparisons * remove unnecessary and slow date conversion * fix tests * fix bad rebase * comment * update whatsnew
1 parent 2279add commit 51d7ecc

12 files changed

+330
-83
lines changed

docs/sphinx/source/whatsnew/v0.4.0.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,21 @@ Enhancements
2828
* Adds the PVWatts DC, AC, and system losses model. (:issue:`195`)
2929
* Improve PEP8 conformity in irradiance module. (:issue:`214`)
3030
* irradiance.disc is up to 10x faster. (:issue:`214`)
31+
* Add solarposition.nrel_earthsun_distance function and option to
32+
calculate extraterrestrial radiation using the NREL solar position
33+
algorithm. (:issue:`211`, :issue:`215`)
3134

3235

3336
Bug fixes
3437
~~~~~~~~~
3538

3639
* dirint function yielded the wrong results for non-sea-level pressures.
3740
Fixed. (:issue:`212`)
41+
* Fixed a bug in the day angle calculation used by the 'spencer' and 'asce'
42+
extraterrestrial radiation options. Most modeling results will be changed
43+
by less than 1 part in 1000. (:issue:`211`)
44+
* irradiance.extraradiation now raises a ValueError for invalid method
45+
input. It previously failed silently. (:issue:`215`)
3846

3947

4048
Documentation

docs/tutorials/irradiance.ipynb

Lines changed: 108 additions & 19 deletions
Large diffs are not rendered by default.

pvlib/irradiance.py

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,32 +35,35 @@
3535
'dirty steel': 0.08}
3636

3737

38-
def extraradiation(datetime_or_doy, solar_constant=1366.1, method='spencer'):
38+
def extraradiation(datetime_or_doy, solar_constant=1366.1, method='spencer',
39+
**kwargs):
3940
"""
4041
Determine extraterrestrial radiation from day of year.
4142
4243
Parameters
4344
----------
4445
datetime_or_doy : int, float, array, pd.DatetimeIndex
45-
Day of year, array of days of year e.g. pd.DatetimeIndex.dayofyear,
46-
or pd.DatetimeIndex.
46+
Day of year, array of days of year e.g.
47+
pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex.
4748
4849
solar_constant : float
4950
The solar constant.
5051
5152
method : string
5253
The method by which the ET radiation should be calculated.
53-
Options include ``'pyephem', 'spencer', 'asce'``.
54+
Options include ``'pyephem', 'spencer', 'asce', 'nrel'``.
55+
56+
kwargs :
57+
Passed to solarposition.nrel_earthsun_distance
5458
5559
Returns
5660
-------
57-
float or Series
58-
61+
dni_extra : float, array, or Series
5962
The extraterrestrial radiation present in watts per square meter
60-
on a surface which is normal to the sun. Ea is of the same size as the
61-
input doy.
63+
on a surface which is normal to the sun. Ea is of the same size
64+
as the input doy.
6265
63-
'pyephem' always returns a series.
66+
'pyephem' and 'nrel' always return a Series.
6467
6568
Notes
6669
-----
@@ -69,8 +72,8 @@ def extraradiation(datetime_or_doy, solar_constant=1366.1, method='spencer'):
6972
7073
References
7174
----------
72-
[1] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance Clear
73-
Sky Models: Implementation and Analysis", Sandia National
75+
[1] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance
76+
Clear Sky Models: Implementation and Analysis", Sandia National
7477
Laboratories, SAND2012-2389, 2012.
7578
7679
[2] <http://solardat.uoregon.edu/SolarRadiationBasics.html>,
@@ -102,8 +105,9 @@ def extraradiation(datetime_or_doy, solar_constant=1366.1, method='spencer'):
102105
doy = datetime_or_doy
103106
input_to_datetimeindex = _array_to_datetimeindex
104107

105-
B = (2 * np.pi / 365) * doy
108+
B = (2. * np.pi / 365.) * (doy - 1)
106109

110+
method = method.lower()
107111
if method == 'asce':
108112
pvl_logger.debug('Calculating ET rad using ASCE method')
109113
RoverR0sqrd = 1 + 0.033 * np.cos(B)
@@ -115,6 +119,12 @@ def extraradiation(datetime_or_doy, solar_constant=1366.1, method='spencer'):
115119
pvl_logger.debug('Calculating ET rad using pyephem method')
116120
times = input_to_datetimeindex(datetime_or_doy)
117121
RoverR0sqrd = solarposition.pyephem_earthsun_distance(times) ** (-2)
122+
elif method == 'nrel':
123+
times = input_to_datetimeindex(datetime_or_doy)
124+
RoverR0sqrd = \
125+
solarposition.nrel_earthsun_distance(times, **kwargs) ** (-2)
126+
else:
127+
raise ValueError('Invalid method: %s', method)
118128

119129
Ea = solar_constant * RoverR0sqrd
120130

@@ -1075,7 +1085,7 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
10751085
return sky_diffuse
10761086

10771087

1078-
def disc(ghi, zenith, times, pressure=101325):
1088+
def disc(ghi, zenith, datetime_or_doy, pressure=101325):
10791089
"""
10801090
Estimate Direct Normal Irradiance from Global Horizontal Irradiance
10811091
using the DISC model.
@@ -1093,7 +1103,9 @@ def disc(ghi, zenith, times, pressure=101325):
10931103
True (not refraction-corrected) solar zenith angles in decimal
10941104
degrees.
10951105
1096-
times : DatetimeIndex
1106+
datetime_or_doy : int, float, array, pd.DatetimeIndex
1107+
Day of year or array of days of year e.g.
1108+
pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex.
10971109
10981110
pressure : numeric
10991111
Site pressure in Pascal.
@@ -1128,18 +1140,8 @@ def disc(ghi, zenith, times, pressure=101325):
11281140
dirint
11291141
"""
11301142

1131-
# in principle, the dni_extra calculation could be done by
1132-
# pvlib's function. However, this is the algorithm used in
1133-
# the DISC paper
1134-
1135-
doy = times.dayofyear
1136-
1137-
dayangle = 2. * np.pi*(doy - 1) / 365
1138-
1139-
re = (1.00011 + 0.034221*np.cos(dayangle) + 0.00128*np.sin(dayangle) +
1140-
0.000719*np.cos(2.*dayangle) + 7.7e-5*np.sin(2.*dayangle))
1141-
1142-
I0 = re * 1370.
1143+
# this is the I0 calculation from the reference
1144+
I0 = extraradiation(datetime_or_doy, 1370, 'spencer')
11431145
I0h = I0 * np.cos(np.radians(zenith))
11441146

11451147
am = atmosphere.relativeairmass(zenith, model='kasten1966')
@@ -1176,8 +1178,8 @@ def disc(ghi, zenith, times, pressure=101325):
11761178
output['kt'] = kt
11771179
output['airmass'] = am
11781180

1179-
if isinstance(times, pd.DatetimeIndex):
1180-
output = pd.DataFrame(output, index=times)
1181+
if isinstance(datetime_or_doy, pd.DatetimeIndex):
1182+
output = pd.DataFrame(output, index=datetime_or_doy)
11811183

11821184
return output
11831185

pvlib/solarposition.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,3 +771,54 @@ def pyephem_earthsun_distance(time):
771771
earthsun.append(sun.earth_distance)
772772

773773
return pd.Series(earthsun, index=time)
774+
775+
776+
def nrel_earthsun_distance(time, how='numpy', delta_t=None, numthreads=4):
777+
"""
778+
Calculates the distance from the earth to the sun using the
779+
NREL SPA algorithm described in [1].
780+
781+
Parameters
782+
----------
783+
time : pd.DatetimeIndex
784+
785+
how : str, optional
786+
Options are 'numpy' or 'numba'. If numba >= 0.17.0
787+
is installed, how='numba' will compile the spa functions
788+
to machine code and run them multithreaded.
789+
790+
delta_t : float, optional
791+
Difference between terrestrial time and UT1.
792+
By default, use USNO historical data and predictions
793+
794+
numthreads : int, optional
795+
Number of threads to use if how == 'numba'.
796+
797+
Returns
798+
-------
799+
R : pd.Series
800+
Earth-sun distance in AU.
801+
802+
References
803+
----------
804+
[1] Reda, I., Andreas, A., 2003. Solar position algorithm for solar
805+
radiation applications. Technical report: NREL/TP-560- 34302. Golden,
806+
USA, http://www.nrel.gov.
807+
"""
808+
delta_t = delta_t or 67.0
809+
810+
if not isinstance(time, pd.DatetimeIndex):
811+
try:
812+
time = pd.DatetimeIndex(time)
813+
except (TypeError, ValueError):
814+
time = pd.DatetimeIndex([time, ])
815+
816+
unixtime = time.astype(np.int64)/10**9
817+
818+
spa = _spa_python_import(how)
819+
820+
R = spa.earthsun_distance(unixtime, delta_t, numthreads)
821+
822+
R = pd.Series(R, index=time)
823+
824+
return R

pvlib/spa.py

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,7 @@ def solar_position_loop(unixtime, loc_args, out):
907907
delta_t = loc_args[5]
908908
atmos_refract = loc_args[6]
909909
sst = loc_args[7]
910+
esd = loc_args[8]
910911

911912
for i in range(unixtime.shape[0]):
912913
utime = unixtime[i]
@@ -915,9 +916,12 @@ def solar_position_loop(unixtime, loc_args, out):
915916
jc = julian_century(jd)
916917
jce = julian_ephemeris_century(jde)
917918
jme = julian_ephemeris_millennium(jce)
919+
R = heliocentric_radius_vector(jme)
920+
if esd:
921+
out[0, i] = R
922+
continue
918923
L = heliocentric_longitude(jme)
919924
B = heliocentric_latitude(jme)
920-
R = heliocentric_radius_vector(jme)
921925
Theta = geocentric_longitude(L)
922926
beta = geocentric_latitude(B)
923927
x0 = mean_elongation(jce)
@@ -970,14 +974,24 @@ def solar_position_loop(unixtime, loc_args, out):
970974

971975

972976
def solar_position_numba(unixtime, lat, lon, elev, pressure, temp, delta_t,
973-
atmos_refract, numthreads, sst=False):
977+
atmos_refract, numthreads, sst=False, esd=False):
974978
"""Calculate the solar position using the numba compiled functions
975979
and multiple threads. Very slow if functions are not numba compiled.
976980
"""
981+
# these args are the same for each thread
977982
loc_args = np.array([lat, lon, elev, pressure, temp, delta_t,
978-
atmos_refract, sst])
983+
atmos_refract, sst, esd])
984+
985+
# construct dims x ulength array to put the results in
979986
ulength = unixtime.shape[0]
980-
result = np.empty((6, ulength), dtype=np.float64)
987+
if sst:
988+
dims = 3
989+
elif esd:
990+
dims = 1
991+
else:
992+
dims = 6
993+
result = np.empty((dims, ulength), dtype=np.float64)
994+
981995
if unixtime.dtype != np.float64:
982996
unixtime = unixtime.astype(np.float64)
983997

@@ -992,6 +1006,7 @@ def solar_position_numba(unixtime, lat, lon, elev, pressure, temp, delta_t,
9921006
solar_position_loop(unixtime, loc_args, result)
9931007
return result
9941008

1009+
# split the input and output arrays into numthreads chunks
9951010
split0 = np.array_split(unixtime, numthreads)
9961011
split2 = np.array_split(result, numthreads, axis=1)
9971012
chunks = [[a0, loc_args, split2[i]] for i, a0 in enumerate(split0)]
@@ -1006,7 +1021,7 @@ def solar_position_numba(unixtime, lat, lon, elev, pressure, temp, delta_t,
10061021

10071022

10081023
def solar_position_numpy(unixtime, lat, lon, elev, pressure, temp, delta_t,
1009-
atmos_refract, numthreads, sst=False):
1024+
atmos_refract, numthreads, sst=False, esd=False):
10101025
"""Calculate the solar position assuming unixtime is a numpy array. Note
10111026
this function will not work if the solar position functions were
10121027
compiled with numba.
@@ -1017,9 +1032,11 @@ def solar_position_numpy(unixtime, lat, lon, elev, pressure, temp, delta_t,
10171032
jc = julian_century(jd)
10181033
jce = julian_ephemeris_century(jde)
10191034
jme = julian_ephemeris_millennium(jce)
1035+
R = heliocentric_radius_vector(jme)
1036+
if esd:
1037+
return (R, )
10201038
L = heliocentric_longitude(jme)
10211039
B = heliocentric_latitude(jme)
1022-
R = heliocentric_radius_vector(jme)
10231040
Theta = geocentric_longitude(L)
10241041
beta = geocentric_latitude(B)
10251042
x0 = mean_elongation(jce)
@@ -1063,7 +1080,7 @@ def solar_position_numpy(unixtime, lat, lon, elev, pressure, temp, delta_t,
10631080

10641081

10651082
def solar_position(unixtime, lat, lon, elev, pressure, temp, delta_t,
1066-
atmos_refract, numthreads=8, sst=False):
1083+
atmos_refract, numthreads=8, sst=False, esd=False):
10671084

10681085
"""
10691086
Calculate the solar position using the
@@ -1100,6 +1117,11 @@ def solar_position(unixtime, lat, lon, elev, pressure, temp, delta_t,
11001117
numthreads: int, optional
11011118
Number of threads to use for computation if numba>=0.17
11021119
is installed.
1120+
sst : bool
1121+
If True, return only data needed for sunrise, sunset, and transit
1122+
calculations.
1123+
esd : bool
1124+
If True, return only Earth-Sun distance in AU
11031125
11041126
Returns
11051127
-------
@@ -1126,7 +1148,7 @@ def solar_position(unixtime, lat, lon, elev, pressure, temp, delta_t,
11261148

11271149
result = do_calc(unixtime, lat, lon, elev, pressure,
11281150
temp, delta_t, atmos_refract, numthreads,
1129-
sst)
1151+
sst, esd)
11301152

11311153
if not isinstance(result, np.ndarray):
11321154
try:
@@ -1241,3 +1263,37 @@ def transit_sunrise_sunset(dates, lat, lon, delta_t, numthreads):
12411263
sunset = S + utday
12421264

12431265
return transit, sunrise, sunset
1266+
1267+
1268+
def earthsun_distance(unixtime, delta_t, numthreads):
1269+
"""
1270+
Calculates the distance from the earth to the sun using the
1271+
NREL SPA algorithm described in [1].
1272+
1273+
Parameters
1274+
----------
1275+
unixtime : numpy array
1276+
Array of unix/epoch timestamps to calculate solar position for.
1277+
Unixtime is the number of seconds since Jan. 1, 1970 00:00:00 UTC.
1278+
A pandas.DatetimeIndex is easily converted using .astype(np.int64)/10**9
1279+
delta_t : float
1280+
Difference between terrestrial time and UT. USNO has tables.
1281+
numthreads : int
1282+
Number to threads to use for calculation (if using numba)
1283+
1284+
Returns
1285+
-------
1286+
R : array
1287+
Earth-Sun distance in AU.
1288+
1289+
References
1290+
----------
1291+
[1] Reda, I., Andreas, A., 2003. Solar position algorithm for solar
1292+
radiation applications. Technical report: NREL/TP-560- 34302. Golden,
1293+
USA, http://www.nrel.gov.
1294+
"""
1295+
1296+
R = solar_position(unixtime, 0, 0, 0, 0, 0, delta_t,
1297+
0, numthreads, esd=True)[0]
1298+
1299+
return R

0 commit comments

Comments
 (0)