|
| 1 | +# # Downloading ephemeris data from JPL Horizons |
| 2 | +# |
| 3 | +# This example shows how to download ephemeris data from JPL Horizons and use it in STK. A custom function is used to connect to the [NASA JPL Horizons System](https://ssd.jpl.nasa.gov/horizons/app.html) by using its [API service](https://ssd-api.jpl.nasa.gov/doc/horizons.html). After downloading the ephemerides file from the server, the file is adapted to STK ephemerides format by adding custom headers. |
| 4 | + |
| 5 | +# ## What are ephemerides? |
| 6 | +# |
| 7 | +# Ephemerides files, often simply called ephemerides, are datasets containing information about the positions and movements of celestial objects such as planets, moons, asteroids, and comets. These files provide detailed information about the positions of these celestial bodies at specific points in time, typically given in coordinates such as right ascension and declination for celestial objects or ecliptic coordinates for objects within the solar system. |
| 8 | +# |
| 9 | +# In this example, the ephemerides of ['Oumuamua](https://ssd.jpl.nasa.gov/tools/sbdb_lookup.html#/?sstr=1I) are the ones studied. |
| 10 | + |
| 11 | +# ## Downloading ephemerides from JPL Horizons System |
| 12 | +# |
| 13 | +# The JPL Horizons System provides a public API deployed in https://ssd.jpl.nasa.gov/api/horizons.api. Users can perform a ``GET`` request to previous endpoint and include different parameters in the query. Parameters allow to request desired data or indicate the output format. A complete list of the supported parameters can be found in the [official JPL Horizons System API reference](https://ssd-api.jpl.nasa.gov/doc/horizons.html). |
| 14 | + |
| 15 | +JPL_HORIZONS_API = "https://ssd.jpl.nasa.gov/api/horizons.api" |
| 16 | + |
| 17 | +# ### Defining the parameters |
| 18 | +# |
| 19 | +# STK ephemerides support a variety of parameters, see the [official STK ephemerides specification](https://help.agi.com/stk/12.8.0/index.htm#stk/importfiles-02.htm). |
| 20 | +# |
| 21 | +# To match previous specification, the parameters required for the JPL Horizons query are: |
| 22 | +# |
| 23 | +# - A ``Vector Table`` containing the data |
| 24 | +# - ``State Vector`` data |
| 25 | +# - CSV format |
| 26 | +# |
| 27 | +# In addition to previous ones, users can request a custom time span and coordinates center. |
| 28 | +# |
| 29 | +# Since this tutorial focuses on 1I/ʻOumuamua interstellar object, the JPL query parameters are the following: |
| 30 | + |
| 31 | +# + |
| 32 | +common_parameters = { |
| 33 | + "format": "text", |
| 34 | + "COMMAND": "'DES=A/2017 U1;'", # Official indicator for Oumuamua |
| 35 | + "OBJ_DATA": "NO", # Do not include header data |
| 36 | + "MAKE_EPHEM": "YES", # Generate ephemerides for the query |
| 37 | + "EPHEM_TYPE": "VECTOR", # Request only vector data |
| 38 | +} |
| 39 | + |
| 40 | +ephemerides_parameters = { |
| 41 | + "CENTER": "500@10", # Generate ephemerides w.r.t Sun's center |
| 42 | + "START_TIME": "2017-01-01", # Start time for the ephemerides |
| 43 | + "STOP_TIME": "2018-01-01", # Stop time for the ephemerides |
| 44 | + "STEP_SIZE": "1d", # Step size is 1 day |
| 45 | + "REF_SYSTEM": "ICRF", # Reference system is ICRF |
| 46 | + "VEC_TABLE": "2", # Request only state vector |
| 47 | + "OUT_UNITS": "KM-S", # Output units in Kilometers and seconds |
| 48 | + "CAL_FORMAT": "JD", # Dates must be in Julian Date |
| 49 | + "CSV_FORMAT": "YES", # Apply CSV formatting |
| 50 | +} |
| 51 | + |
| 52 | +parameters = common_parameters | ephemerides_parameters |
| 53 | +# - |
| 54 | + |
| 55 | +# ### Querying JPL Horizons System |
| 56 | +# |
| 57 | +# Once the query parameters are defined, an asynchronous function can be implemented for performing the query: |
| 58 | + |
| 59 | +# + |
| 60 | +import httpx |
| 61 | + |
| 62 | + |
| 63 | +async def query_jpl_horizons(params: dict) -> str: |
| 64 | + """Query JPL Horizons System with desired parameters. |
| 65 | +
|
| 66 | + Parameters |
| 67 | + ---------- |
| 68 | + params : dict |
| 69 | + Dictionary relating parameters and their values. |
| 70 | +
|
| 71 | + Returns |
| 72 | + ------- |
| 73 | + str |
| 74 | + Ephemerides data in string format. |
| 75 | +
|
| 76 | + Notes |
| 77 | + ----- |
| 78 | + For a complete list of valid parameters refer to https://ssd-api.jpl.nasa.gov/doc/horizons.html. |
| 79 | + |
| 80 | + """ |
| 81 | + async with httpx.AsyncClient() as client: |
| 82 | + response = await client.get(JPL_HORIZONS_API, params=params) |
| 83 | + if response.status_code == 200: |
| 84 | + return response.text |
| 85 | + else: |
| 86 | + raise RuntimeError("Failed to retrieve data.") |
| 87 | +# - |
| 88 | + |
| 89 | +# Finally, it is possible to query the JPL Horizons System: |
| 90 | + |
| 91 | +oumuamua_ephem = await query_jpl_horizons(parameters) |
| 92 | + |
| 93 | +# Previous data can be stored in a file, similarly to what JPL Horizons web application allows: |
| 94 | + |
| 95 | +with open("horizons_results.txt", "w") as file: |
| 96 | + file.write(oumuamua_ephem) |
| 97 | + |
| 98 | +# ## Cleaning ephemerides data |
| 99 | +# |
| 100 | +# The ephemerides file downloaded contains the following data: |
| 101 | + |
| 102 | +# # !heat -n 50 horizons_results.txt |
| 103 | + |
| 104 | +# This format is not supported by STK. The ephemerides data lies between the ``$$SOE`` (Start Of Ephemeris) and ``$EOE`` (End Of Ephemeris) keywords. In addition, no commas or headers must be present. Finally, some STK metadata needs to be included in the file, following [STK Ephemerides specification](https://help.agi.com/stk/12.8.0/index.htm#stk/importfiles-02.htm). |
| 105 | +# |
| 106 | +# The function below converts JPL Horizons ephemerides results to STK format: |
| 107 | + |
| 108 | +# + |
| 109 | +import re |
| 110 | + |
| 111 | + |
| 112 | +def jpl_to_stk_ephem(jplfile: str, stkfile: str, version: str, metadata: dict) -> str: |
| 113 | + """Convert from JPL Horizons Service ephemerides format into STK format. |
| 114 | +
|
| 115 | + Parameters |
| 116 | + ---------- |
| 117 | + jplfile : str |
| 118 | + Path string to JPL ephemerides file. |
| 119 | + stkfile: str |
| 120 | + Path string to STK ephemerides file. |
| 121 | + version: str |
| 122 | + STK version formatted as MAJOR.MINOR |
| 123 | + metadata: dict |
| 124 | + Desired metadata to be included in the STK ephemerides file. |
| 125 | +
|
| 126 | + Raises |
| 127 | + ------ |
| 128 | + ValueError |
| 129 | + If ``$$SEO`` and ``$$EOE`` guards are not found. |
| 130 | +
|
| 131 | + """ |
| 132 | + # Look for ephemerides from JPL Horizons results |
| 133 | + with open(jplfile, "r") as file: |
| 134 | + EPHEM_PATTERN = r'\$\$SOE(.*?)\$\$EOE' |
| 135 | + match = re.search(EPHEM_PATTERN, file.read(), re.DOTALL) |
| 136 | + if not match: |
| 137 | + raise ValueError("Could not find ephemerides guards $$SEO and $$EOE.") |
| 138 | + |
| 139 | + # Extract JPL ephemerides data, remove empty lines, and split by commas |
| 140 | + jpl_ephem_data = match.group(1).strip().split(",") |
| 141 | + |
| 142 | + # Drop calendar dates as they are not required in STK ephemerides |
| 143 | + ignore_indices = set(range(1, len(jpl_ephem_data), 8)) |
| 144 | + desired_values = lambda value: jpl_ephem_data.index(value) not in ignore_indices |
| 145 | + stk_ephem_data = "".join(list(filter(desired_values, jpl_ephem_data))) |
| 146 | + |
| 147 | + # Save data in STK ephemerides format |
| 148 | + with open(stkfile, "w") as file: |
| 149 | + file.write(f"stk.v.{version}\nBEGIN Ephemeris\n") |
| 150 | + |
| 151 | + max_key_length = max(len(key) for key in metadata.keys()) |
| 152 | + for key, value in metadata.items(): |
| 153 | + file.write(f"\t{key.ljust(max_key_length)}\t\t{value}\n") |
| 154 | + |
| 155 | + file.write(f"{stk_ephem_data}\nEND Ephemeris") |
| 156 | + |
| 157 | + |
| 158 | +# - |
| 159 | + |
| 160 | +# Metadata needs to be consistent with the query. In this example, the following metadata applies: |
| 161 | + |
| 162 | +metadata = { |
| 163 | + "InterpolationMethod": "Lagrange", |
| 164 | + "InterpolationOrder": "5", |
| 165 | + "DistanceUnit": "Kilometers", |
| 166 | + "CentralBody": "Sun", |
| 167 | + "CoordinateSystem": "ICRF", |
| 168 | + "TimeFormat": "JDate", |
| 169 | + "EphemerisTimePosVel": "", |
| 170 | +} |
| 171 | + |
| 172 | +# Finally, it is possible to convert the ``horizons_result.txt`` file to an ephemerides file named ``oumuamua.e``: |
| 173 | + |
| 174 | +jpl_to_stk_ephem( |
| 175 | + jplfile="horizons_results.txt", |
| 176 | + stkfile="oumuamua.e", |
| 177 | + version="12.9", |
| 178 | + metadata=metadata, |
| 179 | +) |
| 180 | + |
| 181 | +# ## Using the ephemerides in STK |
| 182 | +# |
| 183 | +# Once the ephemerides have been downloaded, cleaned, and formatted as needed, it is possible to load them in STK. |
| 184 | +# |
| 185 | +# Start by launching an instance of STK by running: |
| 186 | + |
| 187 | +# + |
| 188 | +from ansys.stk.core.stkengine import STKEngine |
| 189 | + |
| 190 | + |
| 191 | +stk = STKEngine.start_application(noGraphics=False) |
| 192 | +print(f"Using {stk.version}") |
| 193 | +# - |
| 194 | + |
| 195 | +# ### Create a new scenario |
| 196 | +# |
| 197 | +# Start by creating a new scenario in STK. The scenario for simulating Oumuamua is set to have a time period that matches the one requested during the ephemerides query. In addition, the central body of this new scenario must be the Sun: |
| 198 | + |
| 199 | +# + |
| 200 | +from ansys.stk.core.stkobjects import STK_OBJECT_TYPE |
| 201 | + |
| 202 | + |
| 203 | +root = stk.new_object_root() |
| 204 | +scenario = root.children.new_on_central_body(STK_OBJECT_TYPE.SCENARIO, "JPLHorizonsEphem", "Sun") |
| 205 | +scenario.set_time_period("1 Jan 2017", "1 Jan 2018") |
| 206 | +# - |
| 207 | + |
| 208 | +# Make sure to rewind the scenario once created: |
| 209 | + |
| 210 | +root.rewind() |
| 211 | + |
| 212 | +# Next, you can display the scenario. Since the orbit of Oumuamua is very distant from the Sun, it is required to updated the far plane of the camera. This ensures that any object up to this distance is rendered on the scene: |
| 213 | + |
| 214 | +# + |
| 215 | +from ansys.stk.core.stkengine.experimental.jupyterwidgets import GlobeWidget |
| 216 | + |
| 217 | + |
| 218 | +plotter = GlobeWidget(root, 640, 480) |
| 219 | +plotter.camera.far_plane = 1E12 |
| 220 | +plotter.show() |
| 221 | +# - |
| 222 | + |
| 223 | +# ### Visualizing the inner planets |
| 224 | +# |
| 225 | +# To better understand the orbit of Oumuamua and its trajectory through the solar system, the orbits of inner planets can be simulated. The default ephemerides provided by STK are used in this case: |
| 226 | + |
| 227 | +# + |
| 228 | +from ansys.stk.core.stkobjects import STK_OBJECT_TYPE, PLANET_POSITION_SOURCE_TYPE, EPHEM_SOURCE_TYPE |
| 229 | +from ansys.stk.core.utilities.colors import Colors |
| 230 | + |
| 231 | + |
| 232 | +planets = ["Mercury", "Venus", "Earth", "Mars"] |
| 233 | +colors = [Colors.Gray, Colors.Orange, Colors.RoyalBlue, Colors.Red] |
| 234 | + |
| 235 | +for planet_name, color in zip(planets, colors): |
| 236 | + planet = scenario.children.new(STK_OBJECT_TYPE.PLANET, planet_name) |
| 237 | + planet.common_tasks.set_position_source_central_body(planet_name, EPHEM_SOURCE_TYPE.DEFAULT) |
| 238 | + planet.graphics.color = color |
| 239 | +# - |
| 240 | + |
| 241 | +# Ensure that the following configuration is declared to visualize the orbits and labels for vehicles and planets: |
| 242 | + |
| 243 | +# + |
| 244 | +# General graphics configuration |
| 245 | +scenario.graphics.labels_visible = True |
| 246 | + |
| 247 | +# Vehicle specific graphics |
| 248 | +scenario.graphics.orbits_visible = True |
| 249 | + |
| 250 | +# Planet specific graphics |
| 251 | +scenario.graphics.planet_orbits_visible = True |
| 252 | +scenario.graphics.inertial_position_labels_visible = True |
| 253 | +scenario.graphics.inertial_position_visible = True |
| 254 | +scenario.graphics.sub_planet_points_visible = False |
| 255 | +scenario.graphics.sub_planet_labels_visible = False |
| 256 | +# - |
| 257 | + |
| 258 | +# Next, display the scene to visualize the inner planets. The camera position is updated to have a detailed view of the scene: |
| 259 | + |
| 260 | +plotter.camera.position = [-39909210.2975278, -717257963.8657558, -85235906.80896328] |
| 261 | +plotter.show() |
| 262 | + |
| 263 | +# ### Simulating Oumuamua's orbit |
| 264 | +# |
| 265 | +# A satellite object can be used to simulate Omumuamua's orbit. The central body for this new object must be the Sun: |
| 266 | + |
| 267 | +oumuamua = scenario.children.new_on_central_body(STK_OBJECT_TYPE.SATELLITE, "Oumuamua", "Sun") |
| 268 | + |
| 269 | +# The ephemerides file created in the early stages of this notebook is used together with an external STK propagator: |
| 270 | + |
| 271 | +# + |
| 272 | +from ansys.stk.core.stkobjects import VEHICLE_PROPAGATOR_TYPE, STK_EXTERNAL_EPHEMERIS_FORMAT |
| 273 | + |
| 274 | + |
| 275 | +oumuamua.set_propagator_type(VEHICLE_PROPAGATOR_TYPE.PROPAGATOR_STK_EXTERNAL) |
| 276 | +oumuamua.propagator.file_format = STK_EXTERNAL_EPHEMERIS_FORMAT.STK |
| 277 | +oumuamua.propagator.filename = "oumuamua.e" |
| 278 | +# - |
| 279 | + |
| 280 | +# A yellow color is used for visualizing the orbit of Oumuamua. Make sure to increase the detail threshold up to the maximum so the orbit renders in the scene: |
| 281 | + |
| 282 | +oumuamua.graphics.attributes.color = Colors.Yellow |
| 283 | +oumuamua.graphics_3d.model.detail_threshold.all = 1E12 |
| 284 | + |
| 285 | +# Next, propagate the orbit by running: |
| 286 | + |
| 287 | +oumuamua.propagator.propagate() |
| 288 | + |
| 289 | +# Finally, visualize the complete scene: |
| 290 | + |
| 291 | +plotter.show() |
0 commit comments