Skip to content

Commit da453db

Browse files
committed
implement NREL SPA code in numpy or compiled numba
1 parent a4a4df6 commit da453db

File tree

10 files changed

+1694
-79
lines changed

10 files changed

+1694
-79
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ before_install:
2626
install:
2727
- echo "install"
2828
- conda install --yes python=$TRAVIS_PYTHON_VERSION numpy scipy pandas=$PD_VERSION nose pytz ephem
29+
- test $PD_VERSION != 0.13.1 && conda install --yes "numba>=0.17" || echo "Not installing numba"
2930
- conda install --yes coverage
3031
- pip install coveralls
3132
- python setup.py install

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ New features
2424
* Library is Python 3.3 and 3.4 compatible
2525
* Add What's New section to docs (:issue:`10`)
2626
* Add PyEphem option to solar position calculations.
27+
* Add a Python translation of NREL's SPA algorithm.
2728
* ``irradiance.py`` has more AOI, projection, and irradiance sum and calculation functions
2829
* TMY data import has a ``coerce_year`` option
2930
* TMY data can be loaded from a url (:issue:`5`)

pvlib/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
from pvlib import tmy
1111
from pvlib import tracking
1212
from pvlib import pvsystem
13+
from pvlib import spa

pvlib/solarposition.py

Lines changed: 150 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,20 @@
55
# Contributors:
66
# Rob Andrews (@Calama-Consulting), Calama Consulting, 2014
77
# Will Holmgren (@wholmgren), University of Arizona, 2014
8+
# Tony Lorenzo (@alorenzo175), University of Arizona, 2015
89

910
from __future__ import division
11+
import os
1012
import logging
1113
pvl_logger = logging.getLogger('pvlib')
1214
import datetime as dt
15+
try:
16+
from importlib import reload
17+
except ImportError:
18+
try:
19+
from imp import reload
20+
except ImportError:
21+
pass
1322

1423

1524
import numpy as np
@@ -32,7 +41,11 @@ def get_solarposition(time, location, method='pyephem', pressure=101325,
3241
method : string
3342
'pyephem' uses the PyEphem package (default): :func:`pyephem`
3443
35-
'spa' uses the spa code: :func:`spa`
44+
'spa' or 'spa_c' uses the spa code: :func:`spa`
45+
46+
'spa_numpy' uses SPA code translated to python: :func: `spa_python`
47+
48+
'spa_numba' uses SPA code translated to python: :func: `spa_python`
3649
3750
'ephemeris' uses the pvlib ephemeris code: :func:`ephemeris`
3851
pressure : float
@@ -45,10 +58,16 @@ def get_solarposition(time, location, method='pyephem', pressure=101325,
4558
if isinstance(time, dt.datetime):
4659
time = pd.DatetimeIndex([time, ])
4760

48-
if method == 'spa':
49-
ephem_df = spa(time, location)
61+
if method == 'spa' or method == 'spa_c':
62+
ephem_df = spa(time, location, pressure, temperature)
5063
elif method == 'pyephem':
5164
ephem_df = pyephem(time, location, pressure, temperature)
65+
elif method == 'spa_numpy':
66+
ephem_df = spa_python(time, location, pressure, temperature,
67+
how='numpy')
68+
elif method == 'spa_numba':
69+
ephem_df = spa_python(time, location, pressure, temperature,
70+
how='numba')
5271
elif method == 'ephemeris':
5372
ephem_df = ephemeris(time, location, pressure, temperature)
5473
else:
@@ -57,7 +76,8 @@ def get_solarposition(time, location, method='pyephem', pressure=101325,
5776
return ephem_df
5877

5978

60-
def spa(time, location, raw_spa_output=False):
79+
def spa(time, location, pressure=101325, temperature=12, delta_t=67.0,
80+
raw_spa_output=False):
6181
'''
6282
Calculate the solar position using the C implementation of the NREL
6383
SPA code
@@ -69,6 +89,13 @@ def spa(time, location, raw_spa_output=False):
6989
----------
7090
time : pandas.DatetimeIndex
7191
location : pvlib.Location object
92+
pressure : float
93+
Pressure in Pascals
94+
temperature : float
95+
Temperature in C
96+
delta_t : float
97+
Difference between terrestrial time and UT1.
98+
USNO has previous values and predictions.
7299
raw_spa_output : bool
73100
If true, returns the raw SPA output.
74101
@@ -78,15 +105,19 @@ def spa(time, location, raw_spa_output=False):
78105
The DataFrame will have the following columns:
79106
elevation,
80107
azimuth,
81-
zenith.
108+
zenith,
109+
apparent_elevation,
110+
apparent_zenith.
82111
83112
References
84113
----------
85114
NREL SPA code: http://rredc.nrel.gov/solar/codesandalgorithms/spa/
115+
USNO delta T: http://www.usno.navy.mil/USNO/earth-orientation/eo-products/long-term
86116
'''
87117

88118
# Added by Rob Andrews (@Calama-Consulting), Calama Consulting, 2014
89119
# Edited by Will Holmgren (@wholmgren), University of Arizona, 2014
120+
# Edited by Tony Lorenzo (@alorenzo175), University of Arizona, 2015
90121

91122
try:
92123
from pvlib.spa_c_files.spa_py import spa_calc
@@ -102,27 +133,129 @@ def spa(time, location, raw_spa_output=False):
102133

103134
for date in time_utc:
104135
spa_out.append(spa_calc(year=date.year,
105-
month=date.month,
106-
day=date.day,
107-
hour=date.hour,
108-
minute=date.minute,
109-
second=date.second,
110-
timezone=0, # timezone corrections handled above
111-
latitude=location.latitude,
112-
longitude=location.longitude,
113-
elevation=location.altitude))
136+
month=date.month,
137+
day=date.day,
138+
hour=date.hour,
139+
minute=date.minute,
140+
second=date.second,
141+
timezone=0, # timezone corrections handled above
142+
latitude=location.latitude,
143+
longitude=location.longitude,
144+
elevation=location.altitude,
145+
pressure=pressure / 100,
146+
temperature=temperature,
147+
delta_t=delta_t
148+
))
114149

115150
spa_df = pd.DataFrame(spa_out, index=time_utc).tz_convert(location.tz)
116151

117152
if raw_spa_output:
118153
return spa_df
119154
else:
120-
dfout = spa_df[['zenith', 'azimuth']]
121-
dfout['elevation'] = 90 - dfout.zenith
122-
155+
dfout = pd.DataFrame({'azimuth':spa_df['azimuth'],
156+
'apparent_zenith':spa_df['zenith'],
157+
'apparent_elevation':spa_df['e'],
158+
'elevation':spa_df['e0'],
159+
'zenith': 90 - spa_df['e0']})
160+
123161
return dfout
124162

125163

164+
def spa_python(time, location, pressure=101325, temperature=12, delta_t=None,
165+
atmos_refract=None, how='numpy', numthreads=4):
166+
"""
167+
Calculate the solar position using a python translation of the
168+
NREL SPA code.
169+
170+
If numba is installed, the functions can be compiled to
171+
machine code and the function can be multithreaded.
172+
Without numba, the function evaluates via numpy with
173+
a slight performance hit.
174+
175+
Parameters
176+
----------
177+
time : pandas.DatetimeIndex
178+
location : pvlib.Location object
179+
pressure : int or float, optional
180+
avg. yearly air pressure in Pascals.
181+
temperature : int or float, optional
182+
avg. yearly air temperature in degrees C.
183+
delta_t : float, optional
184+
Difference between terrestrial time and UT1.
185+
By default, use USNO historical data and predictions
186+
how : str, optional
187+
If numba >= 0.17.0 is installed, how='numba' will
188+
compile the spa functions to machine code and
189+
run them multithreaded. Otherwise, numpy calculations
190+
are used.
191+
numthreads : int, optional
192+
Number of threads to use if how == 'numba'.
193+
194+
Returns
195+
-------
196+
DataFrame
197+
The DataFrame will have the following columns:
198+
apparent_zenith, zenith,
199+
apparent_elevation, elevation,
200+
azimuth.
201+
202+
203+
References
204+
----------
205+
[1] I. Reda and A. Andreas, Solar position algorithm for solar radiation applications. Solar Energy, vol. 76, no. 5, pp. 577-589, 2004.
206+
[2] I. Reda and A. Andreas, Corrigendum to Solar position algorithm for solar radiation applications. Solar Energy, vol. 81, no. 6, p. 838, 2007.
207+
"""
208+
209+
# Added by Tony Lorenzo (@alorenzo175), University of Arizona, 2015
210+
211+
from pvlib import spa
212+
pvl_logger.debug('Calculating solar position with spa_python code')
213+
214+
lat = location.latitude
215+
lon = location.longitude
216+
elev = location.altitude
217+
pressure = pressure / 100 # pressure must be in millibars for calculation
218+
delta_t = delta_t or 67.0
219+
atmos_refract = atmos_refract or 0.5667
220+
221+
if not isinstance(time, pd.DatetimeIndex):
222+
try:
223+
time = pd.DatetimeIndex(time)
224+
except (TypeError, ValueError):
225+
time = pd.DatetimeIndex([time,])
226+
227+
unixtime = localize_to_utc(time, location).astype(int)/10**9
228+
229+
using_numba = spa.USE_NUMBA
230+
if how == 'numpy' and using_numba:
231+
os.environ['PVLIB_USE_NUMBA'] = '0'
232+
pvl_logger.debug('Reloading spa module without compiling')
233+
spa = reload(spa)
234+
del os.environ['PVLIB_USE_NUMBA']
235+
elif how == 'numba' and not using_numba:
236+
os.environ['PVLIB_USE_NUMBA'] = '1'
237+
pvl_logger.debug('Reloading spa module, compiling with numba')
238+
spa = reload(spa)
239+
del os.environ['PVLIB_USE_NUMBA']
240+
elif how != 'numba' and how != 'numpy':
241+
raise ValueError("how must be either 'numba' or 'numpy'")
242+
243+
app_zenith, zenith, app_elevation, elevation, azimuth = spa.solar_position(
244+
unixtime, lat, lon, elev, pressure, temperature, delta_t, atmos_refract,
245+
numthreads)
246+
result = pd.DataFrame({'apparent_zenith':app_zenith, 'zenith':zenith,
247+
'apparent_elevation':app_elevation,
248+
'elevation':elevation, 'azimuth':azimuth},
249+
index = time)
250+
251+
try:
252+
result = result.tz_convert(location.tz)
253+
except TypeError:
254+
result = result.tz_localize(location.tz)
255+
256+
return result
257+
258+
126259
def _ephem_setup(location, pressure, temperature):
127260
import ephem
128261
# initialize a PyEphem observer

0 commit comments

Comments
 (0)