Building a Solar System model with Nasa Horizons API and three.js

Building a Solar System model with Nasa Horizons API and three.js

I love being in constant learning about programming, mathematics, physics, astronomy, etc. I believe I’m far from being an expert in any of these subjects, but I am passionate about learning about them. Something that gives me a lot of satisfaction is when I manage to connect ideas or concepts from different worlds. This time, when I discovered the NASA Horizons API, which I will talk about later, the idea came to me: what if I create a web application with three.js where the user can select a date and see the positions of the planets reflected? You can see the result here:

https://tonicanada.github.io/solar-system-threejs/

In this article, I will talk about the process of creating this app. In one of the data science courses I’m taking, the instructor always says that it’s essential to familiarize yourself with the data, and the mantra is to visualize, visualize, visualize… Creating the app led me to visualize the Solar System in a way I had never seen before. I was surprised that I was unaware of many quite basic concepts.

First, I will write about what I learned about the Solar System, concepts that may be basic for other people, but I was unaware of. Later, I will comment on the code used for the app.

Concepts learned while visualizing the data

Obviously, I knew that the planets orbit around the Sun, and I was aware that Mercury was the closest to the Sun, and Pluto the farthest. However, I used to think that the orbits of the planets were all in different planes, something similar to what is seen in the following image.

Planets orbiting on different planes (unreal depiction)

While visualizing the app, I realized that, in reality, all the planets except Pluto have approximately the same orbital plane, as seen below. I researched this and found a reason behind it: the Solar System formed from a rotating disk of gas and dust.

I was unaware that the planets closest to the Sun have more elliptical orbits than the others, and they also travel at a higher speed. The table shows the number of orbits they complete in a year and the time it takes to complete their orbit.

Another thing I wasn’t aware of is that the group of Mercury, Venus, Earth, and Mars is much closer to each other and to the Sun than the others. Now I understand why the movie ‘2001: A Space Odyssey’ has the chapter ‘Jupiter and Beyond’ 😆!

Planetary Paths: A top-down look at the orbits of our solar system’s planets.

Mars to Sun Orbit View: Zooming in on the dance between Mars and the Sun.

Also, I could detect through visualization that the planets don’t always pass through the same points. In the image, you can see that the lines don’t have the same thickness (Mercury being the clearest case), which means the planet hasn’t always followed the same ellipse over time.

Code Used for the App Development

The Github repository has 2 folders, one called solar-system-data and another called solar-system-threejs. The first one is written in Python and contains the logic to download data from the NASA Horizons API, while the second one is a Vite App where the positions of the planets are represented using the Three.js library.

When I discovered the Nasa Horizons API, I have to admit that I got excited. I always imagined that somewhere I could find the positions of celestial bodies over time, but I never thought it would be so straightforward!

Nasa Horizons has two ways through which you can access the information, one of them is through the terminal, with the following command:

telnet horizons.jpl.nasa.gov 6775

Using this command, with no authentication required, you can query data using commands. Despite this method making you feel like Neo in the Matrix 😎, there’s a more convenient way to access it through an API. Here is the documentation.

In summary, regarding what can be queried with the API, each “object” (could be a planet, star, satellite, asteroid, mission, etc.) has an ID. There are over 600 objects available for querying, and here is a list of them.

Regarding each object, you can inquire about its properties as well as its position over certain dates.

def get_planet_positions_from_sun_csv(start_date, end_date, time_step, planet, output_folder):

    url = 'https://ssd.jpl.nasa.gov/api/horizons.api'

    param = {
        "format": "text",
        "COMMAND": planets[planet],
        "OBJ_DATA": "YES",
        "MAKE_EPHEM": "YES",
        "EPHEM_TYPE": "VECTORS",
        "CENTER": "@sun",
        "START_TIME": f"JD{str(convert_to_juliandate(start_date))}",
        "STOP_TIME": f"JD{str(convert_to_juliandate(end_date))}",
        "STEP_SIZE": time_step,
        "CSV_FORMAT": "YES"
    }

    ephem_exists = check_start_date_ephem_by_planet(start_date, planet)

    if ephem_exists:
        response = requests.get(url, params=param)
        content_txt = response.text

        start = content_txt.find("$$SOE")
        end = content_txt.find("$$EOE")

        data = content_txt[start + len("$$EOE"):end]

        # Remove the comma at the end of each line
        cleaned_data = '\n'.join(line.strip(',') for line in data.split('\n'))

        file_path = Path(output_folder) / f"{planet}_{start_date}_{end_date}.csv"

        with open(file_path, 'w', encoding="utf-8") as file:
            file.write(cleaned_data)
    else:
        print(f"No ephemeris for target '{planet}' for date {start_date}")

As you can see, we are requesting from the API the information about a specific planet, within a range of dates, along with its position relative to the Sun. It’s interesting to note that you can inquire about positions relative to an object other than the Sun.

The parameters start-time and end-time can also be written in the date format we are accustomed to (yyyy-mm-dd). However, to work with BC dates, I found it more convenient to express them in terms of “Julian time”, a concept I was entirely unfamiliar with and which also became part of the learning experience during this project. The year 0 JD is equivalent to 01-01-4713 BC.

It’s essential to consider that the Horizons API doesn’t have information for some planets before a certain date. That’s why in the app, before the year 1600 AD, only Mercury, Venus, and Earth appear. Below is the information on starting from which date there is data available.

Another discovery while working on this project was the ephem library in Python. I haven’t explored it in detail, but I believe it has some very interesting functionalities.

The purpose of the Python code is to generate a JSON file that will be later used by the Three.js App. The format of this JSON has the following structure:

{
  "1751-01-01": {
    "Jupiter": [567870505.4387926, 480018874.0493847, -14677624.64057758],
    "Pluto": [-1659845741.404341, -4084639570.690031, 917754510.7325],
    "Earth": [-35105686.89957103, 142841146.0037219, 78731.25164704025],
    "Mercury": [28381752.76056007, -59184524.46478384, -7438604.424046163],
    "Uranus": [2551258418.697556, -1569295438.133674, -39231201.01378703],
    "Venus": [11102748.79937587, -108229322.7695824, -2047611.735988766],
    "Saturn": [-533303296.2827991, -1399738414.371662, 46102475.71358705],
    "Mars": [-211074066.189805, -112754985.9349335, 2949618.42168083],
    "Neptune": [-2367922191.058414, 3812768397.208968, -23965999.51064825]
  },
  "1751-01-06": {
    "Jupiter": [564137011.158541, 484586781.3215852, -14611742.24325112],
    "Pluto": [-1657611697.348931, -4085879026.118621, 917243104.6679108],
    "Earth": [-47660843.28146335, 139168197.3731036, 76127.27410317957],
    "Mercury": [41660692.40613102, -46515751.51663856, -7635150.727781702],
    "Uranus": [2552772489.484175, -1566920187.910375, -39241992.34731734],
    "Venus": [25894979.39666522, -105708287.6503585, -2871586.917012662],
    "Saturn": [-529639015.5941727, -1401233985.269415, 45984713.75110489],
    "Mars": [-205543380.1672792, -120999328.002768, 2638743.832780249],
    "Neptune": [-2369932753.068549, 3811547211.91458, -23894575.02123165]
  },
  "1751-01-11": {
    "Jupiter": [560369831.7803667, 489125487.5742517, -14544978.99913225],
    "Pluto": [-1655379598.629253, -4087119800.464841, 916731714.0704236],
    "Earth": [-59840515.57991014, 134416350.484194, 73357.95135698467],
    "Mercury": [50772514.64782426, -29272594.4568694, -7074363.546100765],
    "Uranus": [2554284127.146801, -1564543500.78105, -39252721.40614557],
    "Venus": [40191050.86081873, -101161595.8786273, -3640538.164681114],
    "Saturn": [-525970430.4813625, -1402718692.159617, 45866284.8403725],
    "Mars": [-199637655.1509095, -129022859.8676484, 2323055.78080599],
    "Neptune": [-2371942596.010034, 3810325122.270596, -23823110.77476954]
  },
...
}

Three.js App

The app is very simple; it’s built with Vite and essentially has three main files, in addition to the JSON files where the collected data is stored.

The script.js file is the main one where planets and the sun are created as points (Float32Array), and orbits are drawn. The updatePositions function is responsible for updating the positions of the planets based on the date.

One detail that took me a bit to implement is ensuring that the points (planets and sun) appear the same size regardless of how close or far the camera is. However, this can be achieved by making the point size relative to its distance from the camera.

The app uses the tweakpane menu, allowing users to choose the date to view the positions of the planets. It’s a very user-friendly library, and its implementation is in the menu.js file.

Here’s the link to view the online website. You might be curious about how the planets were aligned when you were born — now you can find out!

I hope you found it interesting! Please give it a 👏 and share it! You can follow me on my blog, LinkedIn, Twitter, and Facebook.