Skip to content

Commit 855664c

Browse files
committed
Merge pull request #45 from wholmgren/ephem
fix built in ephemeris
2 parents 32696bc + 8833cfe commit 855664c

File tree

2 files changed

+116
-113
lines changed

2 files changed

+116
-113
lines changed

pvlib/solarposition.py

Lines changed: 96 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -219,39 +219,27 @@ def ephemeris(time, location, pressure=101325, temperature=12):
219219
----------
220220
time : pandas.DatetimeIndex
221221
location : pvlib.Location
222-
223-
Other Parameters
224-
----------------
225-
226-
pressure : float or DataFrame
227-
Ambient pressure (Pascals)
228-
229-
temperature : float or DataFrame
230-
Ambient temperature (C)
222+
pressure : float or Series
223+
Ambient pressure (Pascals)
224+
temperature : float or Series
225+
Ambient temperature (C)
231226
232227
Returns
233228
-------
234229
235-
DataFrame with the following columns
236-
237-
SunEl : float of DataFrame
238-
Actual elevation (not accounting for refraction)of the sun
239-
in decimal degrees, 0 = on horizon. The complement of the True Zenith
240-
Angle.
241-
242-
SunAz : Azimuth of the sun in decimal degrees from North.
243-
0 = North, 90 = West, 180 = South, 270 = West
244-
245-
SunZen : Solar zenith angle
246-
247-
ApparentSunEl : float or DataFrame
248-
249-
Apparent sun elevation accounting for atmospheric
250-
refraction. This is the complement of the Apparent Zenith Angle.
251-
252-
SolarTime : fload or DataFrame
253-
Solar time in decimal hours (solar noon is 12.00).
230+
DataFrame with the following columns:
254231
232+
* apparent_elevation : apparent sun elevation accounting for
233+
atmospheric refraction.
234+
* elevation : actual elevation (not accounting for refraction)
235+
of the sun in decimal degrees, 0 = on horizon.
236+
The complement of the zenith angle.
237+
* azimuth : Azimuth of the sun in decimal degrees East of North.
238+
This is the complement of the apparent zenith angle.
239+
* apparent_zenith : apparent sun zenith accounting for atmospheric
240+
refraction.
241+
* zenith : Solar zenith angle
242+
* solar_time : Solar time in decimal hours (solar noon is 12.00).
255243
256244
References
257245
-----------
@@ -262,133 +250,129 @@ def ephemeris(time, location, pressure=101325, temperature=12):
262250
See also
263251
--------
264252
pyephem, spa
265-
266253
'''
267254

268255
# Added by Rob Andrews (@Calama-Consulting), Calama Consulting, 2014
269256
# Edited by Will Holmgren (@wholmgren), University of Arizona, 2014
270-
271-
pvl_logger.warning('Using solarposition.ephemeris is discouraged. '
272-
'solarposition.pyephem and solarposition.spa are '
273-
'faster and more accurate.')
257+
258+
# Most comments in this function are from PVLIB_MATLAB or from
259+
# pvlib-python's attempt to understand and fix problems with the
260+
# algorithm. The comments are *not* based on the reference material.
261+
# This helps a little bit:
262+
# http://www.cv.nrao.edu/~rfisher/Ephemerides/times.html
274263

275264
pvl_logger.debug('location={}, temperature={}, pressure={}'.format(
276265
location, temperature, pressure))
277266

267+
# the inversion of longitude is due to the fact that this code was
268+
# originally written for the convention that positive longitude were for
269+
# locations west of the prime meridian. However, the correct convention (as
270+
# of 2009) is to use negative longitudes for locations west of the prime
271+
# meridian. Therefore, the user should input longitude values under the
272+
# correct convention (e.g. Albuquerque is at -106 longitude), but it needs
273+
# to be inverted for use in the code.
274+
278275
Latitude = location.latitude
279-
''' the inversion of longitude is due to the fact that this code was
280-
originally written for the convention that positive longitude were for
281-
locations west of the prime meridian. However, the correct convention (as
282-
of 2009) is to use negative longitudes for locations west of the prime
283-
meridian. Therefore, the user should input longitude values under the
284-
correct convention (e.g. Albuquerque is at -106 longitude), but it needs
285-
to be inverted for use in the code.
286-
'''
287-
Latitude = location.latitude
288-
Longitude = 1 * location.longitude
289-
Year = time.year
290-
Hour = time.hour
291-
Minute = time.minute
292-
Second = time.second
293-
DayOfYear = time.dayofyear
294-
295-
DecHours = Hour + Minute / float(60) + Second / float(3600)
296-
297-
Abber = 20 / float(3600)
276+
Longitude = -1 * location.longitude
277+
278+
Abber = 20 / 3600.
298279
LatR = np.radians(Latitude)
299-
300-
# this code is needed to handle the new location.tz format string
301-
try:
302-
utc_offset = pytz.timezone(location.tz).utcoffset(
303-
time[0]).total_seconds() / 3600.
304-
except ValueError:
305-
utc_offset = time.tzinfo.utcoffset(time[0]).total_seconds() / 3600.
306-
pvl_logger.debug('utc_offset={}'.format(utc_offset))
280+
281+
# the SPA algorithm needs time to be expressed in terms of
282+
# decimal UTC hours of the day of the year.
283+
284+
# first convert to utc
285+
time_utc = localize_to_utc(time, location)
286+
287+
# strip out the day of the year and calculate the decimal hour
288+
DayOfYear = time_utc.dayofyear
289+
DecHours = (time_utc.hour + time_utc.minute/60. + time_utc.second/3600. +
290+
time_utc.microsecond/3600.e6)
307291

308292
UnivDate = DayOfYear
293+
UnivHr = DecHours
309294

310-
# ToDo: Will H: surprised to see the 0.5 here, but moving on...
311-
UnivHr = DecHours + utc_offset - .5
312-
313-
Yr = Year - 1900
314-
315-
YrBegin = 365 * Yr + np.floor((Yr - 1) / float(4)) - 0.5
295+
Yr = time_utc.year - 1900
296+
YrBegin = 365 * Yr + np.floor((Yr - 1) / 4.) - 0.5
316297

317298
Ezero = YrBegin + UnivDate
318-
T = Ezero / float(36525)
319-
GMST0 = 6 / float(24) + 38 / float(1440) + (
320-
45.836 + 8640184.542 * T + 0.0929 * T ** 2) / float(86400)
299+
T = Ezero / 36525.
300+
301+
# Calculate Greenwich Mean Sidereal Time (GMST)
302+
GMST0 = 6 / 24. + 38 / 1440. + (
303+
45.836 + 8640184.542 * T + 0.0929 * T ** 2) / 86400.
321304
GMST0 = 360 * (GMST0 - np.floor(GMST0))
322-
GMSTi = np.mod(GMST0 + 360 * (1.0027379093 * UnivHr / float(24)), 360)
323-
305+
GMSTi = np.mod(GMST0 + 360 * (1.0027379093 * UnivHr / 24.), 360)
306+
307+
# Local apparent sidereal time
324308
LocAST = np.mod((360 + GMSTi - Longitude), 360)
325-
EpochDate = Ezero + UnivHr / float(24)
326-
T1 = EpochDate / float(36525)
309+
310+
EpochDate = Ezero + UnivHr / 24.
311+
T1 = EpochDate / 36525.
312+
327313
ObliquityR = np.radians(
328314
23.452294 - 0.0130125 * T1 - 1.64e-06 * T1 ** 2 + 5.03e-07 * T1 ** 3)
329315
MlPerigee = 281.22083 + 4.70684e-05 * EpochDate + 0.000453 * T1 ** 2 + (
330316
3e-06 * T1 ** 3)
331317
MeanAnom = np.mod((358.47583 + 0.985600267 * EpochDate - 0.00015 *
332-
T1 ** 2 - 3e-06 * T1 ** 3), 360)
318+
T1 ** 2 - 3e-06 * T1 ** 3), 360)
333319
Eccen = 0.01675104 - 4.18e-05 * T1 - 1.26e-07 * T1 ** 2
334320
EccenAnom = MeanAnom
335321
E = 0
336322

337323
while np.max(abs(EccenAnom - E)) > 0.0001:
338324
E = EccenAnom
339-
EccenAnom = MeanAnom + np.degrees(Eccen) * (np.sin(np.radians(E)))
325+
EccenAnom = MeanAnom + np.degrees(Eccen)*np.sin(np.radians(E))
340326

341-
# pdb.set_trace()
342327
TrueAnom = (
343328
2 * np.mod(np.degrees(np.arctan2(((1 + Eccen) / (1 - Eccen)) ** 0.5 *
344-
np.tan(np.radians(EccenAnom) / float(2)), 1)), 360))
329+
np.tan(np.radians(EccenAnom) / 2.), 1)), 360))
345330
EcLon = np.mod(MlPerigee + TrueAnom, 360) - Abber
346331
EcLonR = np.radians(EcLon)
347-
DecR = np.arcsin(np.sin(ObliquityR)*(np.sin(EcLonR)))
332+
DecR = np.arcsin(np.sin(ObliquityR)*np.sin(EcLonR))
348333

349-
RtAscen = np.degrees(np.arctan2(np.cos(ObliquityR) * ((np.sin(EcLonR))),
350-
np.cos(EcLonR)))
334+
RtAscen = np.degrees(np.arctan2(np.cos(ObliquityR)*np.sin(EcLonR),
335+
np.cos(EcLonR) ))
351336

352337
HrAngle = LocAST - RtAscen
353338
HrAngleR = np.radians(HrAngle)
354-
355339
HrAngle = HrAngle - (360 * ((abs(HrAngle) > 180)))
356-
SunAz = np.degrees(np.arctan2(- 1 * np.sin(HrAngleR), np.cos(LatR) *
357-
(np.tan(DecR)) - np.sin(LatR)*(np.cos(HrAngleR))))
358-
SunAz = SunAz + (SunAz < 0) * 360
359-
# potential error in the following line
360-
SunEl = np.degrees(np.arcsin((np.cos(LatR) * (np.cos(DecR)) *
361-
(np.cos(HrAngleR)) + np.sin(LatR)*(np.sin(DecR)))))
362-
SolarTime = (180 + HrAngle) / float(15)
363-
364-
Refract = []
365-
366-
for Elevation in SunEl:
367-
TanEl = np.tan(np.radians(Elevation))
368-
if Elevation > 5 and Elevation <= 85:
369-
Refract.append((58.1 / float(TanEl) - 0.07 / float(TanEl ** 3) +
370-
8.6e-05 / float(TanEl ** 5)))
371-
elif Elevation > -0.575 and Elevation <= 5:
372-
Refract.append((Elevation * ((- 518.2 + Elevation *
373-
((103.4 + Elevation * ((- 12.79 + Elevation *
374-
(0.711))))))) + 1735))
375-
elif Elevation > -1 and Elevation <= -0.575:
376-
Refract.append(- 20.774 / float(TanEl))
377-
else:
378-
Refract.append(0)
379-
380-
Refract = (np.array(Refract) * ((283 / float(273 + temperature))) *
381-
pressure / float(101325) / float(3600))
382-
383-
SunZen = 90 - SunEl
340+
341+
SunAz = np.degrees(np.arctan2(-np.sin(HrAngleR),
342+
np.cos(LatR)*np.tan(DecR) - np.sin(LatR)*np.cos(HrAngleR) ))
343+
SunAz[SunAz < 0] += 360
344+
345+
SunEl = np.degrees(np.arcsin(
346+
np.cos(LatR) * np.cos(DecR) * np.cos(HrAngleR) +
347+
np.sin(LatR) * np.sin(DecR) ))
348+
349+
SolarTime = (180 + HrAngle) / 15.
350+
351+
# Calculate refraction correction
352+
Elevation = SunEl
353+
TanEl = pd.Series(np.tan(np.radians(Elevation)), index=time_utc)
354+
Refract = pd.Series(0, index=time_utc)
355+
356+
Refract[(Elevation > 5) & (Elevation <= 85)] = (
357+
58.1/TanEl - 0.07/(TanEl**3) + 8.6e-05/(TanEl**5) )
358+
359+
Refract[(Elevation > -0.575) & (Elevation <= 5)] = ( Elevation *
360+
(-518.2 + Elevation*(103.4 + Elevation*(-12.79 + Elevation*0.711))) +
361+
1735 )
362+
363+
Refract[(Elevation > -1) & (Elevation <= -0.575)] = -20.774 / TanEl
364+
365+
Refract *= (283/(273. + temperature)) * (pressure/101325.) / 3600.
384366

385367
ApparentSunEl = SunEl + Refract
386368

387-
DFOut = pd.DataFrame({'elevation': SunEl}, index=time)
388-
DFOut['azimuth'] = SunAz
389-
DFOut['zenith'] = SunZen
369+
# make output DataFrame
370+
DFOut = pd.DataFrame(index=time_utc).tz_convert(location.tz)
390371
DFOut['apparent_elevation'] = ApparentSunEl
372+
DFOut['elevation'] = SunEl
373+
DFOut['azimuth'] = SunAz
391374
DFOut['apparent_zenith'] = 90 - ApparentSunEl
375+
DFOut['zenith'] = 90 - SunEl
392376
DFOut['solar_time'] = SolarTime
393377

394378
return DFOut

pvlib/test/test_solarposition.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,23 @@ def test_ephemeris_functional():
117117
time=times, location=golden_mst, method='ephemeris')
118118

119119

120-
# add tests for daylight savings time?
120+
def test_ephemeris_physical():
121+
times = pd.date_range(datetime.datetime(2003,10,17,12,30,30),
122+
periods=1, freq='D')
123+
ephem_data = solarposition.ephemeris(times, golden_mst, pressure=82000,
124+
temperature=11).ix[0]
125+
126+
assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 2)
127+
assert_almost_equals(194.340241, ephem_data['azimuth'], 2)
128+
assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 2)
129+
130+
131+
def test_ephemeris_physical_dst():
132+
times = pd.date_range(datetime.datetime(2003,10,17,13,30,30),
133+
periods=1, freq='D')
134+
ephem_data = solarposition.ephemeris(times, golden, pressure=82000,
135+
temperature=11).ix[0]
136+
137+
assert_almost_equals(50.111622, ephem_data['apparent_zenith'], 2)
138+
assert_almost_equals(194.340241, ephem_data['azimuth'], 2)
139+
assert_almost_equals(39.888378, ephem_data['apparent_elevation'], 2)

0 commit comments

Comments
 (0)