22import pandas as pd
33import io
44
5- URL = " https://ssd.jpl.nasa.gov/api/horizons.api"
5+ URL = ' https://ssd.jpl.nasa.gov/api/horizons.api'
66
77
8- def nasa_horizons (
9- latitude , longitude , start , end , elevation = 0.0 , * , time_step = "1h" , url = URL
10- ):
8+ def nasa_horizons (latitude , longitude , start , end , elevation = 0. , * ,
9+ time_step = '1h' , refraction_correction = False , url = URL ):
1110 """
1211 Retrieve solar positions from NASA's Horizons web service.
1312
@@ -42,30 +41,39 @@ def nasa_horizons(
4241 Time step size of the requested time series. '1m' for minutes,
4342 '1h' for hours, '1d' for days, '1mo' for months, and '1y' for years.
4443 The default is '1h'.
44+ refraction_correction : bool, optional
45+ Whether to return apparent solar position angles corrected for
46+ atmospheric refraction. The defalt is False.
4547 url : str, optional
4648 API endpoint. The default is
4749 ``'https://ssd.jpl.nasa.gov/api/horizons.api'``.
4850
4951 Returns
5052 -------
5153 pandas.DataFrame
52- DataFrame with the following columns (all values in degrees):
54+ DataFrame with the following columns in degrees (note that all columns
55+ except azimuth are prefixed with `'apparent_'` if
56+ ``refraction_correction=True``.)
5357
54- - uncertainty_right_ascension
55- - uncertainty_declination
5658 - right_ascension
5759 - declination
58- - apparent_right_ascesion
59- - apparent_declination
60- - apparent_azimuth
61- - apparent_elevation
60+ - azimuth
61+ - elevation
62+
63+ Notes
64+ -----
65+ The refraction correction algorithm is the same as
66+ :py:func:`solposx.refraction.spa`. See [3]_ for more
67+ information.
6268
6369 References
6470 ----------
6571 .. [1] `NASA Horizons Systems
6672 <https://ssd.jpl.nasa.gov/horizons/>`_
6773 .. [2] `NASA Horizons API
6874 <https://ssd-api.jpl.nasa.gov/doc/horizons.html>`_
75+ .. [3] `NASA Horizons Manual
76+ <https://ssd.jpl.nasa.gov/horizons/manual.html>`_
6977 """
7078 params = {
7179 "MAKE_EPHEM" : "YES" , # generate ephemeris
@@ -74,62 +82,75 @@ def nasa_horizons(
7482 "CENTER" : "coord@399" , # input coordinates for Earth (399)
7583 "COORD_TYPE" : "GEODETIC" , # latitude, longitude, elevation in degrees
7684 "SITE_COORD" : f"{ longitude } ,{ latitude } ,{ elevation / 1000 } " ,
77- "START_TIME" : pd .Timestamp (start ).strftime (" %Y-%m-%d %H:%M" ),
78- "STOP_TIME" : pd .Timestamp (end ).strftime (" %Y-%m-%d %H:%M" ),
85+ "START_TIME" : pd .Timestamp (start ).strftime (' %Y-%m-%d %H:%M' ),
86+ "STOP_TIME" : pd .Timestamp (end ).strftime (' %Y-%m-%d %H:%M' ),
7987 "STEP_SIZE" : time_step ,
8088 "QUANTITIES" : "2,4" , # corrected angles
8189 "REF_SYSTEM" : "ICRF" ,
8290 "CAL_FORMAT" : "CAL" , # output date format
8391 "CAL_TYPE" : "MIXED" , # Gregorian or mixed Julian/Gregorian calendar
8492 "TIME_DIGITS" : "FRACSEC" , # output time precision
8593 "ANG_FORMAT" : "DEG" , # output angles in degrees
86- "APPARENT" : "AIRLESS" , # no refraction
87- # "RANGE_UNITS": "AU",
88- # "SUPPRESS_RANGE_RATE": "NO",
8994 "SKIP_DAYLT" : "NO" , # include daylight periods
9095 "SOLAR_ELONG" : "0,180" ,
9196 "EXTRA_PREC" : "NO" , # toggle additional digits on some angles (RA/DEC)
9297 "CSV_FORMAT" : "NO" ,
93- "OBJ_DATA" : "NO" , # whether to return summary data
98+ "OBJ_DATA" : "NO" # whether to return summary data
9499 }
95100
101+ if refraction_correction :
102+ params ["APPARENT" ] = "REFRACTED" # account for refraction
103+ else :
104+ params ["APPARENT" ] = "AIRLESS" # do not account for refraction
105+
96106 # manual formatting of the url as all parameters except format shall be
97- # in enclosed in single quotes
107+ # enclosed in single quotes
98108 url_formatted = (
99- url + "?" + "format=text&" + "&" .join ([f"{ k } ='{ v } '" for k , v in params .items ()])
109+ url
110+ + '?'
111+ + "format=text&"
112+ + '&' .join ([f"{ k } ='{ v } '" for k , v in params .items ()])
100113 )
101114
102115 res = requests .get (url_formatted )
103116
104117 fbuf = io .StringIO (res .content .decode ())
105118
106119 lines = fbuf .readlines ()
107- first_line = lines .index (" $$SOE\n " ) + 1
108- last_line = lines .index (" $$EOE\n " , first_line )
120+ first_line = lines .index (' $$SOE\n ' ) + 1
121+ last_line = lines .index (' $$EOE\n ' , first_line )
109122 header_line = lines [first_line - 3 ]
110- data_str = [header_line ] + lines [first_line :last_line ]
123+ data_str = [header_line ] + lines [first_line : last_line ]
111124
112- data = pd .read_fwf (
113- io .StringIO ("\n " .join (data_str )), index_col = [0 ], na_values = ["n.a." ]
114- )
115- data .index = pd .to_datetime (data .index , format = "%Y-%b-%d %H:%M:%S.%f" )
116- data .index = data .index .tz_localize ("UTC" )
125+ data = pd .read_fwf (io .StringIO ('\n ' .join (data_str )),
126+ index_col = [0 ], na_values = ['n.a.' ])
127+ data .index = pd .to_datetime (data .index , format = '%Y-%b-%d %H:%M:%S.%f' )
128+ data .index = data .index .tz_localize ('UTC' )
117129
118130 # split columns as several params have a shared header name for two params
119- column_name_split_map = {
120- "R.A._(a-appar)_DEC." : ["right_ascension" , "declination" ],
121- "Azi____(a-app)___Elev" : ["azimuth" , "elevation" ],
122- }
131+ if refraction_correction :
132+ column_name_split_map = {
133+ # "r" prefix stands for "refracted" aka. refracted/apparent angles
134+ 'R.A._(r-appar)__DEC' : ['apparent_right_ascension' , 'apparent_declination' ],
135+ 'Azi____(r-app)___Elev' : ['azimuth' , 'apparent_elevation' ],
136+ }
137+
138+ else :
139+ column_name_split_map = {
140+ # "a" prefix stands for "airless" aka. unrefracted
141+ 'R.A._(a-appar)_DEC.' : ['right_ascension' , 'declination' ],
142+ 'Azi____(a-app)___Elev' : ['azimuth' , 'elevation' ],
143+ }
123144
124145 for old_name , new_names in column_name_split_map .items ():
125- data [new_names ] = data [old_name ].str .split (r"\s+" , expand = True ).astype (float )
146+ data [new_names ] = \
147+ data [old_name ].str .split (r'\s+' , expand = True ).astype (float )
126148
127149 data = data .drop (columns = list (column_name_split_map .keys ()))
128150
129- data .index .name = " time"
151+ data .index .name = ' time'
130152 try :
131- del data [" Unnamed: 1" ]
153+ del data [' Unnamed: 1' ]
132154 except KeyError :
133155 pass
134-
135156 return data
0 commit comments