308.1. Solar System orbit classifications#
308.1. Solar System orbit classifications¶
For the Rubin Science Platform at data.lsst.cloud.
Data Release: Data Preview 2
Container Size: Large
LSST Science Pipelines version: r30.0.9
Last verified to run: 2026-07-20
Repository: github.com/lsst/tutorial-notebooks
DOI: 10.11578/rubin/dc.20250909.20
Learning objective: An overview of the Data Preview 2 (DP2) Solar System object orbits.
LSST data products: mpc_orbits, SSObject
Packages: lsst.rsp
Credit: Originally developed by the Rubin Community Science team. Please consider acknowledging them if this notebook is used for the preparation of journal articles, software releases, or other notebooks.
Get Support: Everyone is encouraged to ask questions or raise issues in the Support Category of the Rubin Community Forum. Rubin staff will respond to all questions posted there.
1. Introduction¶
This notebook examines the orbital classifications in the DP2 Solar System object tables. The DP2 data release mpc_orbits table contains 1,505,545 Solar System objects, with 299,343 objects detected by Rubin (accessible via the SSObject table).
Related tutorials: The 100-level tutorials demonstrate how to use the TAP service. The 200-level tutorials introduce the types of catalog data.
1.1. Import packages¶
Import numpy, a fundamental package for scientific computing with arrays in Python
(numpy.org), and
matplotlib, a comprehensive library for data visualization
(matplotlib.org;
matplotlib gallery).
From the lsst package, import the module for accessing the Table Access Protocol (TAP) service from the LSST Science Pipelines (pipelines.lsst.io).
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from lsst.rsp import RSPDiscovery
1.2. Define parameters and functions¶
Create an instance of the TAP service.
discovery = RSPDiscovery("dp2")
service = discovery.get_tap_client()
Define parameters to use colorblind-friendly colors with matplotlib.
plt.style.use('seaborn-v0_8-colorblind')
prop_cycle = plt.rcParams['axes.prop_cycle']
colors = prop_cycle.by_key()['color']
The orbit_type_int field in the mpc_orbits catalog contains a set of integers used by the IAU Minor Planet Center (MPC) to define Solar System object orbit types based on the object's semimajor axis $a$, eccentricity $e$, and inclination $i$. For the orbit type definitions, see the Minor Planet Center website.
The orbit type and their MPC-assigned integer values are as follows:
Orbit type | Assigned value
----------------------------
Atira NEO | 0
Aten NEO | 1
Apollo NEO | 2
Amor NEO | 3
Other - Inner Solar System | 9
Mars Crosser | 10
Main Belt | 11
Jupiter Trojan | 12
Other - Middle Solar System | 19
Jupiter Coupled | 20
Neptune Trojan | 21
Centaur | 22
Transneptunian object | 23
Hyperbolic | 30
Parabolic | 31
Other - unusual | 99
None | None
Define a function to assign orbit classification labels based on the MPC-assigned orbit type, where ATI = Atira NEO, ATE = Aten NEO, APO = Apollo NEO, AMO = Amor NEO, O-I = Other-Inner Solar System, MCA = Mars-crosser asteroid, MBA = main-belt asteroid, TJN = Jupiter Trojan, O-M = Other-Middle Solar System, J-C = Jupiter coupled, NTR = Neptune Trojan, CEN = Centaur, TNO = transneptunian object, HYA = hyperbolic asteroid, PAA = parabolic asteroid, O-U = Other-unusual, and NAN = None, following the MPC orbit type definitions above.
Notice: Many objects in the
mpc_orbitstable have no value in theirorbit_type_intfields. A search onorbit_type_int == 0thus returns both Atiras and all objects with noorbit_type_intvalues. To separate out the Atiras, use the perihelion distance ($q$) and eccentricity ($e$) to restrict objects to those with aphelion distance 0 au < $Q$ < 0.983 au (orbital definition for Atiras, i.e., objects with aphelia less than Earth's perihelion distance). The orbital element relationship for aphelion distance is defined by $Q = q * (1 + e) / (1 - e)$. The pericenter distance $q$ is used instead of the semimajor axis $a$, because many semimajor axis entries are missing from thempc_orbitstable (see Section 2 for more details). All objects with noorbit_type_intvalues are assigned theAST(asteroid) classification label.
def assign_orbit_class(Q, orbit_type_int):
orbit_class = np.full(len(orbit_type_int), "AST")
orbit_class_dict = {
"ATI": np.where((Q < 0.983) & (Q > 0)),
"ATE": np.where((orbit_type_int == 1)),
"APO": np.where((orbit_type_int == 2)),
"AMO": np.where((orbit_type_int == 3)),
"O-I": np.where((orbit_type_int == 9)),
"MCA": np.where((orbit_type_int == 10)),
"MBA": np.where((orbit_type_int == 11)),
"TJN": np.where((orbit_type_int == 12)),
"O-M": np.where((orbit_type_int == 19)),
"J-C": np.where((orbit_type_int == 20)),
"NTR": np.where((orbit_type_int == 21)),
"CEN": np.where((orbit_type_int == 22)),
"TNO": np.where((orbit_type_int == 23)),
"HYA": np.where((orbit_type_int == 30)),
"PAA": np.where((orbit_type_int == 31)),
"O-U": np.where((orbit_type_int == 99)),
"NAN": np.where((orbit_type_int == 'None')),
}
for c, v in orbit_class_dict.items():
orbit_class[v] = c
return orbit_class
2. Classify orbits¶
Query the mpc_orbits table for the orbital parameters for all 1,505,545 Solar System objects in this DP2 table, including those detected by Rubin.
query = "SELECT mpc.designation, "\
"mpc.q, mpc.e, mpc.i, "\
"mpc.orbit_type_int "\
"FROM dp2.mpc_orbits as mpc "\
"ORDER BY mpc.designation ASC"
job = service.submit_job(query)
job.run()
job.wait(phases=['COMPLETED', 'ERROR'])
print('Job phase is', job.phase)
if job.phase == 'ERROR':
job.raise_if_error()
Job phase is COMPLETED
Fetch the job results and assign to an astropy result table.
assert job.phase == 'COMPLETED'
result = job.fetch_result()
len(result)
1505545
Convert the result astropy table to a pandas dataframe.
result_df = pd.DataFrame(result)
Calculate the aphelion distance $Q$ from the perihelion distance ($q$) and eccentricity ($e$) using the relationship $Q = q * (1 + e) / (1 - e)$ and add as a new column in the result_df dataframe.
result_df['Q'] = result_df['q'] * (1.0 + result_df['e']) / (1.0 - result_df['e'])
Add a column to result_df with each object's orbit_class.
result_df['orbit_class'] = assign_orbit_class(result_df['Q'], result_df['orbit_type_int'])
Option to print the results
# result_df
Print the number of objects in each orbit class.
print("atira: ", len(result_df[result_df['orbit_class'] == 'ATI']))
print("aten: ", len(result_df[result_df['orbit_class'] == 'ATE']))
print("apollo: ", len(result_df[result_df['orbit_class'] == 'APO']))
print("amor: ", len(result_df[result_df['orbit_class'] == 'AMO']))
print("other_inner_ss: ", len(result_df[result_df['orbit_class'] == 'O-I']))
print("mars_crosser: ", len(result_df[result_df['orbit_class'] == 'MCA']))
print("mba: ", len(result_df[result_df['orbit_class'] == 'MBA']))
print("jupiter_trojan: ", len(result_df[result_df['orbit_class'] == 'TJN']))
print("other_middle_ss: ", len(result_df[result_df['orbit_class'] == 'O-M']))
print("jupiter_coupled: ", len(result_df[result_df['orbit_class'] == 'J-C']))
print("neptune_trojan: ", len(result_df[result_df['orbit_class'] == 'NTR']))
print("centaur: ", len(result_df[result_df['orbit_class'] == 'CEN']))
print("tno: ", len(result_df[result_df['orbit_class'] == 'TNO']))
print("hyperbolic: ", len(result_df[result_df['orbit_class'] == 'HYA']))
print("parabolic: ", len(result_df[result_df['orbit_class'] == 'PAA']))
print("other_unusual: ", len(result_df[result_df['orbit_class'] == 'O-U']))
print("none: ", len(result_df[result_df['orbit_class'] == 'NAN']))
print("ast: ", len(result_df[result_df['orbit_class'] == 'AST']))
atira: 39 aten: 710 apollo: 4438 amor: 3253
other_inner_ss: 0 mars_crosser: 12639 mba: 968235
jupiter_trojan: 13077 other_middle_ss: 8084 jupiter_coupled: 2485 neptune_trojan: 19
centaur: 269 tno: 2072 hyperbolic: 37 parabolic: 0
other_unusual: 0 none: 0 ast: 490188
The semimajor axis is included in the mpc_orbits table, but many entries are missing, as described in Section 1. For further information on the known issues with the mpc_orbits table, see the MPC documentation. The semimajor axis ($a$) can be computed from the perihelion distance ($q$) and eccentricity ($e$) using the relationship $a$ = $q$ / (1.0-$e$).
Add a column to result_df with the calculated semimajor axis.
result_df['a_calc'] = result_df['q'] / (1.0-result_df['e'])
Define a dataframe splitting the inner Solar System (with semimajor axes 0 < $a$ <= 10 au) from result_df. Remove all orbital classes with no objects from object counts above and the large number of objects classified as 'AST'.
inner_ss = result_df[result_df['a_calc'] <= 10.0]
inner_ss = inner_ss[inner_ss['a_calc'] > 0.0]
inner_ss.drop(inner_ss[inner_ss['orbit_class'] == 'O-I'].index, inplace=True)
inner_ss.drop(inner_ss[inner_ss['orbit_class'] == 'HYA'].index, inplace=True)
inner_ss.drop(inner_ss[inner_ss['orbit_class'] == 'PAA'].index, inplace=True)
inner_ss.drop(inner_ss[inner_ss['orbit_class'] == 'O-U'].index, inplace=True)
inner_ss.drop(inner_ss[inner_ss['orbit_class'] == 'NAN'].index, inplace=True)
inner_ss.drop(inner_ss[inner_ss['orbit_class'] == 'AST'].index, inplace=True)
Define a dataframe splitting the outer Solar System (with semimajor axes 10 au < $a$ < 100 au) from result_df. Remove all orbital classes with no objects from object counts above from object counts above and the large number of objects classified as 'AST'.
outer_ss = result_df[result_df['a_calc'] > 10.0]
outer_ss = outer_ss[outer_ss['a_calc'] < 100.0]
outer_ss.drop(outer_ss[outer_ss['orbit_class'] == 'O-I'].index, inplace=True)
outer_ss.drop(outer_ss[outer_ss['orbit_class'] == 'HYA'].index, inplace=True)
outer_ss.drop(outer_ss[outer_ss['orbit_class'] == 'PAA'].index, inplace=True)
outer_ss.drop(outer_ss[outer_ss['orbit_class'] == 'O-U'].index, inplace=True)
outer_ss.drop(outer_ss[outer_ss['orbit_class'] == 'NAN'].index, inplace=True)
outer_ss.drop(outer_ss[outer_ss['orbit_class'] == 'AST'].index, inplace=True)
Plot the semimajor axes, eccentricities, and inclinations of the 1,505,545 Solar System objects in the DP2 mpc_orbits table, distinguishing each orbital class.
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
for ax, oo in zip(axes, (inner_ss, outer_ss)):
for label, orb in oo.groupby('orbit_class'):
if label == 'ATI':
marker = "D"
color = colors[0]
elif label == 'ATE':
marker = "^"
color = colors[2]
elif label == 'APO':
marker = "*"
color = colors[1]
elif label == 'AMO':
marker = "D"
color = colors[3]
elif label == 'MCA':
marker = "x"
color = colors[4]
elif label == 'MBA':
marker = "D"
color = colors[5]
elif label == 'TJN':
marker = "h"
color = colors[1]
elif label == 'O-M':
marker = "D"
color = colors[0]
elif label == 'J-C':
marker = "^"
color = colors[2]
elif label == 'NTR':
marker = "*"
color = colors[3]
elif label == 'CEN':
marker = "D"
color = colors[4]
elif label == 'TNO':
marker = "x"
color = colors[5]
ax.scatter(orb["a_calc"], orb["i"],
s=0.3, marker=marker, color=color,
label=f"{label} ({len(orb)})")
for handle in ax.legend(fontsize="small", loc="upper right").legend_handles:
handle.set_sizes([10])
ax.set_xlabel("semimajor axis (au)")
ax.set_ylabel("inclination (deg)")
plt.subplots_adjust(wspace=0.3)
fig.suptitle("Inner (left) and outer (right) Solar System objects in the DP2 `mpc_orbits` table")
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
for ax, oo in zip(axes, (inner_ss, outer_ss)):
for label, orb in oo.groupby('orbit_class'):
if label == 'ATI':
marker = "D"
color = colors[0]
elif label == 'ATE':
marker = "^"
color = colors[2]
elif label == 'APO':
marker = "*"
color = colors[1]
elif label == 'AMO':
marker = "D"
color = colors[3]
elif label == 'MCA':
marker = "x"
color = colors[4]
elif label == 'MBA':
marker = "D"
color = colors[5]
elif label == 'TJN':
marker = "h"
color = colors[1]
elif label == 'O-M':
marker = "D"
color = colors[0]
elif label == 'J-C':
marker = "^"
color = colors[2]
elif label == 'NTR':
marker = "*"
color = colors[3]
elif label == 'CEN':
marker = "D"
color = colors[4]
elif label == 'TNO':
marker = "x"
color = colors[5]
ax.scatter(orb["a_calc"], orb["e"],
s=0.3, marker=marker, color=color,
label=f"{label} ({len(orb)})")
for handle in ax.legend(fontsize="small", loc="upper right").legend_handles:
handle.set_sizes([10])
ax.set_xlabel("semimajor axis (au)")
ax.set_ylabel("eccentricity")
plt.subplots_adjust(wspace=0.3)
Figure 1: Semimajor axes, eccentricities, and inclinations of the 1,505,545 Solar System objects in the DP2
mpc_orbitstable, with the colors distinguishing each orbital class. Left: inner Solar System (semimajor axis $a$ < 10 au). Right: outer Solar System (truncated at semimajor axis 10 au > $a$ > 100 au). The number of objects in each orbital class is indicated in the figure legend.
Clean up.
del query, result
3. Rubin detections¶
The DP2 mpc_orbits and SSObject tables are too large (1,505,545, and 299,343 rows, respectively) to query both for the orbital parameters of all Rubin-detected DP2 Solar System objects without long query times. To avoid long query times on joined tables, query the SSObject table separately and merge the results as a dataframe with result_df from the query above on the mpc_orbits table.
query = "SELECT designation "\
"FROM dp2.SSObject "\
"ORDER BY designation ASC"
job = service.submit_job(query)
job.run()
job.wait(phases=['COMPLETED', 'ERROR'])
print('Job phase is', job.phase)
if job.phase == 'ERROR':
job.raise_if_error()
Job phase is COMPLETED
Fetch the job results and assign to an astropy result table.
assert job.phase == 'COMPLETED'
result = job.fetch_result()
Convert the result astropy table to a pandas dataframe.
result_df_rubin = pd.DataFrame(result)
Option to print the results
# result_df_rubin
Merge the result_df dataframe from the mpc_orbits query above with the result_df_rubin dataframe containing only Rubin-detected objects. Use an inner merge to only keep matching rows present in both dataframes.
merged_df = pd.merge(result_df, result_df_rubin, on='designation', how='inner')
len(merged_df)
299343
Print the number of objects detected by Rubin in each orbit class.
print("atira: ", len(merged_df[merged_df['orbit_class'] == 'ATI']))
print("aten: ", len(merged_df[merged_df['orbit_class'] == 'ATE']))
print("apollo: ", len(merged_df[merged_df['orbit_class'] == 'APO']))
print("amor: ", len(merged_df[merged_df['orbit_class'] == 'AMO']))
print("other_inner_ss: ", len(merged_df[merged_df['orbit_class'] == 'O-I']))
print("mars_crosser: ", len(merged_df[merged_df['orbit_class'] == 'MCA']))
print("mba: ", len(merged_df[merged_df['orbit_class'] == 'MBA']))
print("jupiter_trojan: ", len(merged_df[merged_df['orbit_class'] == 'TJN']))
print("other_middle_ss: ", len(merged_df[merged_df['orbit_class'] == 'O-M']))
print("jupiter_coupled: ", len(merged_df[merged_df['orbit_class'] == 'J-C']))
print("neptune_trojan: ", len(merged_df[merged_df['orbit_class'] == 'NTR']))
print("centaur: ", len(merged_df[merged_df['orbit_class'] == 'CEN']))
print("tno: ", len(merged_df[merged_df['orbit_class'] == 'TNO']))
print("hyperbolic: ", len(merged_df[merged_df['orbit_class'] == 'HYA']))
print("parabolic: ", len(merged_df[merged_df['orbit_class'] == 'PAA']))
print("other_unusual: ", len(merged_df[merged_df['orbit_class'] == 'O-U']))
print("none: ", len(merged_df[merged_df['orbit_class'] == 'NAN']))
print("ast: ", len(merged_df[merged_df['orbit_class'] == 'AST']))
atira: 0 aten: 44 apollo: 376 amor: 494 other_inner_ss: 0 mars_crosser: 2380 mba: 229552 jupiter_trojan: 378
other_middle_ss: 2168 jupiter_coupled: 646 neptune_trojan: 3 centaur: 38 tno: 670 hyperbolic: 3 parabolic: 0 other_unusual: 0 none: 0 ast: 62591
Similar to above, define a dataframe splitting the inner Solar System (with semimajor axes 0 < $a$ <= 10 au) from result_df_rubin. Remove all orbital classes with no objects from object counts above from object counts above and the large number of objects classified as 'AST'.
inner_ss_rubin = merged_df[merged_df['a_calc'] <= 10.0]
inner_ss_rubin = inner_ss_rubin[inner_ss_rubin['a_calc'] > 0.0]
inner_ss_rubin.drop(inner_ss_rubin[inner_ss_rubin['orbit_class'] == 'ATI'].index, inplace=True)
inner_ss_rubin.drop(inner_ss_rubin[inner_ss_rubin['orbit_class'] == 'O-I'].index, inplace=True)
inner_ss_rubin.drop(inner_ss_rubin[inner_ss_rubin['orbit_class'] == 'HYA'].index, inplace=True)
inner_ss_rubin.drop(inner_ss_rubin[inner_ss_rubin['orbit_class'] == 'PAA'].index, inplace=True)
inner_ss_rubin.drop(inner_ss_rubin[inner_ss_rubin['orbit_class'] == 'O-U'].index, inplace=True)
inner_ss_rubin.drop(inner_ss_rubin[inner_ss_rubin['orbit_class'] == 'NAN'].index, inplace=True)
inner_ss_rubin.drop(inner_ss_rubin[inner_ss_rubin['orbit_class'] == 'AST'].index, inplace=True)
Define a dataframe splitting the outer Solar System (with semimajor axes 10 au < $a$ < 700 au) from result_df_rubin. Remove all orbital classes with no objects from object counts above from object counts above and the large number of objects classified as 'AST'.
outer_ss_rubin = merged_df[merged_df['a_calc'] > 10.0]
outer_ss_rubin = outer_ss_rubin[outer_ss_rubin['a_calc'] < 700.0]
outer_ss_rubin.drop(outer_ss_rubin[outer_ss_rubin['orbit_class'] == 'ATI'].index, inplace=True)
outer_ss_rubin.drop(outer_ss_rubin[outer_ss_rubin['orbit_class'] == 'O-I'].index, inplace=True)
outer_ss_rubin.drop(outer_ss_rubin[outer_ss_rubin['orbit_class'] == 'HYA'].index, inplace=True)
outer_ss_rubin.drop(outer_ss_rubin[outer_ss_rubin['orbit_class'] == 'PAA'].index, inplace=True)
outer_ss_rubin.drop(outer_ss_rubin[outer_ss_rubin['orbit_class'] == 'O-U'].index, inplace=True)
outer_ss_rubin.drop(outer_ss_rubin[outer_ss_rubin['orbit_class'] == 'NAN'].index, inplace=True)
outer_ss_rubin.drop(outer_ss_rubin[outer_ss_rubin['orbit_class'] == 'AST'].index, inplace=True)
Plot the semimajor axes, eccentricities, and inclinations of the 299,343 Solar System objects detected by Rubin in DP2, distinguishing each orbital class.
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
for ax, oo in zip(axes, (inner_ss_rubin, outer_ss_rubin)):
for label, orb in oo.groupby('orbit_class'):
if label == 'ATE':
marker = "^"
color = colors[2]
elif label == 'APO':
marker = "*"
color = colors[1]
elif label == 'AMO':
marker = "D"
color = colors[3]
elif label == 'MCA':
marker = "x"
color = colors[4]
elif label == 'MBA':
marker = "D"
color = colors[5]
elif label == 'TJN':
marker = "h"
color = colors[1]
elif label == 'O-M':
marker = "D"
color = colors[0]
elif label == 'J-C':
marker = "^"
color = colors[2]
elif label == 'NTR':
marker = "*"
color = colors[3]
elif label == 'CEN':
marker = "D"
color = colors[4]
elif label == 'TNO':
marker = "x"
color = colors[5]
ax.scatter(orb["a_calc"], orb["i"],
s=0.3, marker=marker, color=color,
label=f"{label} ({len(orb)})")
for handle in ax.legend(fontsize="small").legend_handles:
handle.set_sizes([10])
ax.set_xlabel("semimajor axis (au)")
ax.set_ylabel("inclination (deg)")
plt.subplots_adjust(wspace=0.3)
fig.suptitle("Inner (left) and outer (right) Solar System objects detected by Rubin in DP2")
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
for ax, oo in zip(axes, (inner_ss_rubin, outer_ss_rubin)):
for label, orb in oo.groupby('orbit_class'):
if label == 'ATE':
marker = "^"
color = colors[2]
elif label == 'APO':
marker = "*"
color = colors[1]
elif label == 'AMO':
marker = "D"
color = colors[3]
elif label == 'MCA':
marker = "x"
color = colors[4]
elif label == 'MBA':
marker = "D"
color = colors[5]
elif label == 'TJN':
marker = "h"
color = colors[1]
elif label == 'O-M':
marker = "D"
color = colors[0]
elif label == 'J-C':
marker = "^"
color = colors[2]
elif label == 'NTR':
marker = "*"
color = colors[3]
elif label == 'CEN':
marker = "D"
color = colors[4]
elif label == 'TNO':
marker = "x"
color = colors[5]
ax.scatter(orb["a_calc"], orb["e"],
s=0.3, marker=marker, color=color,
label=f"{label} ({len(orb)})")
for handle in ax.legend(fontsize="small").legend_handles:
handle.set_sizes([10])
ax.set_xlabel("semimajor axis (au)")
ax.set_ylabel("eccentricity")
plt.subplots_adjust(wspace=0.3)
Figure 2: Semimajor axes, eccentricities, and inclinations of the 299,343 Rubin-detected DP2 Solar System objects, with the colors distinguishing each orbital class. Left: inner Solar System (semimajor axis $a$ < 10 au). Right: outer Solar System (truncated at semimajor axis 10 au > $a$ > 700 au). The number of objects in each orbital class is indicated in the figure legend.
Clean up.
del query, result, result_df, inner_ss, outer_ss
del result_df_rubin, merged_df, inner_ss_rubin, outer_ss_rubin