Skip to content

Commit 0d43d99

Browse files
committed
added get_time function to calculate time for a given solar position
1 parent 3b751e1 commit 0d43d99

File tree

8 files changed

+194
-50
lines changed

8 files changed

+194
-50
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ __pycache__/
44

55
# emacs temp files
66
*~
7+
#*#
78

89
# C extensions
910
*.so
@@ -68,6 +69,6 @@ coverage.xml
6869
# vi
6970
*.swp
7071

71-
#Ignore notebooks
72+
#Ignore some notebooks
7273
*.ipynb
7374
!docs/tutorials/*.ipynb

LICENSE

100755100644
File mode changed.

MANIFEST.in

100755100644
File mode changed.

README.md

100755100644
+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ import pvlib.solarposition
7474
import pvlib.clearsky
7575
7676
# make a location
77-
tus = Location(32.2, -111, 700, 'MST')
77+
tus = Location(32.2, -111, 'MST', 700)
7878
7979
# make a pandas DatetimeIndex for some day
8080
times = pd.date_range(start=datetime.datetime(2014,6,24), end=datetime.datetime(2014,6,25), freq='1Min')

pvlib/__init__.py

100755100644
File mode changed.

pvlib/pvl_tools.py

100755100644
+84
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import logging
55
pvl_logger = logging.getLogger('pvlib')
66

7+
import datetime as dt
78
import pdb
89
import ast
910
import re
1011

1112
import numpy as np
13+
import pytz
1214

1315

1416
class repack(): #repack a dict as a struct
@@ -303,4 +305,86 @@ def asind(number):
303305
return res
304306

305307

308+
def localize_to_utc(time, location):
309+
"""
310+
Converts or localizes a time series to UTC.
311+
312+
Parameters
313+
----------
314+
time : datetime.datetime, pandas.DatetimeIndex,
315+
or pandas.Series/DataFrame with a DatetimeIndex.
316+
location : pvlib.Location object
317+
318+
Returns
319+
-------
320+
pandas object localized to UTC.
321+
"""
322+
import datetime as dt
323+
import pytz
324+
325+
if isinstance(time, dt.datetime):
326+
if time.tzinfo is None:
327+
time = pytz.timezone(location.tz).localize(time)
328+
time_utc = time.astimezone(pytz.utc)
329+
else:
330+
try:
331+
time_utc = time.tz_convert('UTC')
332+
pvl_logger.debug('tz_convert to UTC')
333+
except TypeError:
334+
time_utc = time.tz_localize(location.tz).tz_convert('UTC')
335+
pvl_logger.debug('tz_localize to {} and then tz_convert to UTC'
336+
.format(location.tz))
337+
338+
339+
return time_utc
340+
341+
342+
def datetime_to_djd(time):
343+
"""
344+
Converts a datetime to the Dublin Julian Day
345+
346+
Parameters
347+
----------
348+
time : datetime.datetime
349+
time to convert
350+
351+
Returns
352+
-------
353+
float
354+
fractional days since 12/31/1899+0000
355+
"""
356+
357+
if time.tzinfo is None:
358+
time_utc = pytz.utc.localize(time)
359+
else:
360+
time_utc = time.astimezone(pytz.utc)
361+
362+
djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))
363+
djd = (time_utc - djd_start).total_seconds() * 1.0/(60 * 60 * 24)
364+
365+
return djd
366+
367+
368+
def djd_to_datetime(djd, tz='UTC'):
369+
"""
370+
Converts a Dublin Julian Day float to a datetime.datetime object
371+
372+
Parameters
373+
----------
374+
djd : float
375+
fractional days since 12/31/1899+0000
376+
tz : str
377+
timezone to localize the result to
378+
379+
Returns
380+
-------
381+
datetime.datetime
382+
The resultant datetime localized to tz
383+
"""
384+
385+
djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))
306386

387+
utc_time = djd_start + dt.timedelta(days=djd)
388+
return utc_time.astimezone(pytz.timezone(tz))
389+
390+

pvlib/solarposition.py

+83-46
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,20 @@
77
# Will Holmgren (@wholmgren), University of Arizona, 2014
88

99
from __future__ import division
10-
1110
import logging
1211
pvl_logger = logging.getLogger('pvlib')
12+
import datetime as dt
1313

14-
import datetime
1514

1615
import numpy as np
1716
import pandas as pd
1817
import pytz
1918

20-
try:
21-
from .spa_c_files.spa_py import spa_calc
22-
except ImportError as e:
23-
pvl_logger.exception('Could not import built-in SPA calculator. You may need to recompile the SPA code.')
24-
25-
try:
26-
import ephem
27-
except ImportError as e:
28-
pvl_logger.warning('PyEphem not found.')
2919

20+
from .pvl_tools import localize_to_utc, datetime_to_djd, djd_to_datetime
3021

3122

32-
def get_solarposition(time, location, method='spa', pressure=101325,
23+
def get_solarposition(time, location, method='pyephem', pressure=101325,
3324
temperature=12):
3425
"""
3526
A convenience wrapper for the solar position calculators.
@@ -48,6 +39,8 @@ def get_solarposition(time, location, method='spa', pressure=101325,
4839
"""
4940

5041
method = method.lower()
42+
if isinstance(time, dt.datetime):
43+
time = pd.DatetimeIndex([time,])
5144

5245
if method == 'spa':
5346
ephem_df = spa(time, location)
@@ -59,7 +52,6 @@ def get_solarposition(time, location, method='spa', pressure=101325,
5952
return ephem_df
6053

6154

62-
6355
def spa(time, location, raw_spa_output=False):
6456
'''
6557
Calculate the solar position using the C implementation of the NREL
@@ -90,10 +82,16 @@ def spa(time, location, raw_spa_output=False):
9082

9183
# Added by Rob Andrews (@Calama-Consulting), Calama Consulting, 2014
9284
# Edited by Will Holmgren (@wholmgren), University of Arizona, 2014
85+
86+
try:
87+
from .spa_c_files.spa_py import spa_calc
88+
except ImportError as e:
89+
raise ImportError('Could not import built-in SPA calculator. '+
90+
'You may need to recompile the SPA code.')
9391

9492
pvl_logger.debug('using built-in spa code to calculate solar position')
9593

96-
time_utc = _localize_to_utc(time, location)
94+
time_utc = localize_to_utc(time, location)
9795

9896
spa_out = []
9997

@@ -120,6 +118,20 @@ def spa(time, location, raw_spa_output=False):
120118
return dfout
121119

122120

121+
def _ephem_setup(location, pressure, temperature):
122+
import ephem
123+
# initialize a PyEphem observer
124+
obs = ephem.Observer()
125+
obs.lat = str(location.latitude)
126+
obs.lon = str(location.longitude)
127+
obs.elevation = location.altitude
128+
obs.pressure = pressure / 100. # convert to mBar
129+
obs.temp = temperature
130+
131+
# the PyEphem sun
132+
sun = ephem.Sun()
133+
return obs, sun
134+
123135

124136
def pyephem(time, location, pressure=101325, temperature=12):
125137
"""
@@ -144,24 +156,17 @@ def pyephem(time, location, pressure=101325, temperature=12):
144156
"""
145157

146158
# Written by Will Holmgren (@wholmgren), University of Arizona, 2014
147-
159+
160+
import ephem
161+
148162
pvl_logger.debug('using PyEphem to calculate solar position')
149163

150-
time_utc = _localize_to_utc(time, location)
164+
time_utc = localize_to_utc(time, location)
151165

152166
sun_coords = pd.DataFrame(index=time_utc)
153167

154-
# initialize a PyEphem observer
155-
obs = ephem.Observer()
156-
obs.lat = str(location.latitude)
157-
obs.lon = str(location.longitude)
158-
obs.elevation = location.altitude
159-
obs.pressure = pressure / 100. # convert to mBar
160-
obs.temp = temperature
161-
162-
# the PyEphem sun
163-
sun = ephem.Sun()
164-
168+
obs, sun = _ephem_setup(location, pressure, temperature)
169+
165170
# make and fill lists of the sun's altitude and azimuth
166171
# this is the pressure and temperature corrected apparent alt/az.
167172
alts = []
@@ -197,8 +202,7 @@ def pyephem(time, location, pressure=101325, temperature=12):
197202
return sun_coords.tz_convert(location.tz)
198203
except TypeError:
199204
return sun_coords.tz_localize(location.tz)
200-
201-
205+
202206

203207
def ephemeris(time, location, pressure=101325, temperature=12):
204208
'''
@@ -217,7 +221,7 @@ def ephemeris(time, location, pressure=101325, temperature=12):
217221
pressure : float or DataFrame
218222
Ambient pressure (Pascals)
219223
220-
tempreature: float or DataFrame
224+
temperature : float or DataFrame
221225
Ambient temperature (C)
222226
223227
Returns
@@ -374,30 +378,63 @@ def ephemeris(time, location, pressure=101325, temperature=12):
374378
DFOut['solar_time'] = SolarTime
375379

376380
return DFOut
377-
378-
379-
380-
def _localize_to_utc(time, location):
381+
382+
383+
def calc_time(lower_bound, upper_bound, location, attribute, value,
384+
pressure=101325, temperature=12, xtol=1.0e-12):
381385
"""
382-
Converts or localizes a time series to UTC.
386+
Calculate the time between lower_bound and upper_bound
387+
where the attribute is equal to value. Uses PyEphem for
388+
solar position calculations.
383389
384390
Parameters
385391
----------
386-
time : pandas.DatetimeIndex or pandas.Series/DataFrame with a DatetimeIndex.
392+
lower_bound : datetime.datetime
393+
upper_bound : datetime.datetime
387394
location : pvlib.Location object
395+
attribute : str
396+
The attribute of a pyephem.Sun object that
397+
you want to solve for. Likely options are 'alt'
398+
and 'az' (which must be given in radians).
399+
value : int or float
400+
The value of the attribute to solve for
401+
pressure : int or float, optional
402+
Air pressure in Pascals. Set to 0 for no
403+
atmospheric correction.
404+
temperature : int or float, optional
405+
Air temperature in degrees C.
406+
xtol : float, optional
407+
The allowed error in the result from value
388408
389409
Returns
390410
-------
391-
pandas object localized to UTC.
411+
datetime.datetime
412+
413+
Raises
414+
------
415+
ValueError
416+
If the value is not contained between the bounds.
417+
AttributeError
418+
If the given attribute is not an attribute of a
419+
PyEphem.Sun object.
392420
"""
393-
421+
394422
try:
395-
time_utc = time.tz_convert('UTC')
396-
pvl_logger.debug('tz_convert to UTC')
397-
except TypeError:
398-
time_utc = time.tz_localize(location.tz).tz_convert('UTC')
399-
pvl_logger.debug('tz_localize to {} and then tz_convert to UTC'
400-
.format(location.tz))
423+
import scipy.optimize as so
424+
except ImportError as e:
425+
raise ImportError('The calc_time function requires scipy')
426+
427+
obs, sun = _ephem_setup(location, pressure, temperature)
428+
429+
def compute_attr(thetime, target, attr):
430+
obs.date = thetime
431+
sun.compute(obs)
432+
return getattr(sun, attr) - target
401433

402-
return time_utc
403-
434+
lb = datetime_to_djd(lower_bound)
435+
ub = datetime_to_djd(upper_bound)
436+
437+
djd_root = so.brentq(compute_attr, lb, ub,
438+
(value, attribute), xtol=xtol)
439+
440+
return djd_to_datetime(djd_root, location.tz)

pvlib/test/test_solarposition.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,28 @@ def test_pyephem_physical_dst():
7676
def test_pyephem_localization():
7777
assert_frame_equal(solarposition.pyephem(times, tus), solarposition.pyephem(times_localized, tus))
7878

79+
80+
def test_calc_time():
81+
import pytz
82+
import math
83+
# validation from USNO solar position calculator online
84+
85+
epoch = datetime.datetime(1970,1,1)
86+
epoch_dt = pytz.utc.localize(epoch)
87+
88+
loc = tus
89+
loc.pressure = 0
90+
actual_time = pytz.timezone(loc.tz).localize(datetime.datetime(2014, 10, 10, 8, 30))
91+
lb = pytz.timezone(loc.tz).localize(datetime.datetime(2014, 10, 10, 6))
92+
ub = pytz.timezone(loc.tz).localize(datetime.datetime(2014, 10, 10, 10))
93+
alt = solarposition.calc_time(lb, ub, loc, 'alt', math.radians(24.7))
94+
az = solarposition.calc_time(lb, ub, loc, 'az', math.radians(116.3))
95+
actual_timestamp = (actual_time - epoch_dt).total_seconds()
96+
97+
assert_almost_equals((alt.replace(second=0, microsecond=0) -
98+
epoch_dt).total_seconds(), actual_timestamp)
99+
assert_almost_equals((az.replace(second=0, microsecond=0) -
100+
epoch_dt).total_seconds(), actual_timestamp)
101+
79102

80-
81-
# add tests for daylight savings time?
103+
# add tests for daylight savings time?

0 commit comments

Comments
 (0)