Skip to content

Satellite Data Sources

For many modeling tasks it is useful to access satellite ephemeris (orbit) data. This data is made available by a number of sources, including public sources such as Celestrak, Space-Track, and Starlink's own public ephemeris mirror. Brahe provides functions for accessing satellite data from all three sources, as well as initializing SGP4 propagators from data.

All three clients have integrated, default rate-limiting and caching to ensure efficient and responsible access to the data. For more information on the configuration of the clients, see the respective language API documentation.

Moving Beyond TLEs

While TLEs have been historically used for satellite ephemeris data, we will soon encounter the problem of catalog number exhaustion. Brahe supports both alpha-5 and GP Record formats that are being adopted to address the issue.

Celestrak

The celestrack client

import brahe as bh

# Initialize EOP
bh.initialize_eop()

# Get the latest TLE for the ISS (NORAD ID 25544) from Celestrak
client = bh.celestrak.CelestrakClient()
propagator = client.get_sgp_propagator(catnr=25544, step_size=60.0)

# Configure propagation window
epoch_start = propagator.current_epoch()
epoch_end = epoch_start + 7.0 * 86400.0

# Step propagator forward by 1 hour
propagator.propagate_to(epoch_end)

# Get final epoch and state
final_epoch = propagator.current_epoch()
final_state = propagator.current_state()
print(f"Initial epoch: {epoch_start}")
print(f"Final epoch:   {final_epoch}")
print(
    f"Position (km): [{final_state[0] / 1e3:.3f}, {final_state[1] / 1e3:.3f}, {final_state[2] / 1e3:.3f}]"
)
print(
    f"Velocity (m/s): [{final_state[3]:.3f}, {final_state[4]:.3f}, {final_state[5]:.3f}]"
)
#![allow(unused_imports)]
use brahe as bh;
use brahe::traits::SStatePropagator;

fn main() {
    // Initialize EOP
    bh::initialize_eop().unwrap();

    // Get the latest TLE for the ISS (NORAD ID 25544) from Celestrak
    let client = bh::celestrak::CelestrakClient::new();
    let mut propagator = client.get_sgp_propagator_by_catnr(25544, 60.0).unwrap();

    // Configure propagation window
    let epoch_start = propagator.current_epoch();
    let epoch_end = epoch_start + 7.0 * 86400.0;  

    // Step propagator forward by 1 hour
    propagator.propagate_to(epoch_end).unwrap();

    // Get final epoch and state
    let final_epoch = propagator.current_epoch();
    let final_state = propagator.current_state();
    println!("Initial epoch: {}", epoch_start);
    println!("Final epoch:   {}", final_epoch);
    println!(
        "Position (km): [{:.3}, {:.3}, {:.3}]",
        final_state[0] / 1e3,
        final_state[1] / 1e3,
        final_state[2] / 1e3
    );
    println!(
        "Velocity (m/s): [{:.3}, {:.3}, {:.3}]",
        final_state[3],
        final_state[4],
        final_state[5]
    );
}
Output
1
2
3
4
Initial epoch: 2026-08-30 04:13:16.870 UTC
Final epoch:   2026-09-06 04:13:16.870 UTC
Position (km): [1281.354, 6675.094, -38.906]
Velocity (m/s): [-4688.230, 875.247, -5996.567]
1
2
3
4
Initial epoch: 2026-08-30 04:13:16.870 UTC
Final epoch:   2026-09-06 04:13:16.870 UTC
Position (km): [1281.354, 6675.094, -38.906]
Velocity (m/s): [-4688.230, 875.247, -5996.567]

Space-Track

The space-track client requires a user account to access the data. Once authenticated, the client provides access to a variety of satellite data, including TLEs, GP Records, and other records.

import os

import brahe as bh

# Initialize EOP
bh.initialize_eop()

# Authenticate with Space-Track using account credentials
client = bh.spacetrack.SpaceTrackClient(
    os.environ["SPACETRACK_USERNAME"], os.environ["SPACETRACK_PASSWORD"]
)

# Query the latest GP record for the ISS (NORAD ID 25544)
query = (
    bh.SpaceTrackQuery(bh.RequestClass.GP)
    .filter("NORAD_CAT_ID", "25544")
    .order_by("EPOCH", bh.SortOrder.DESC)
    .limit(1)
)
records = client.query_gp(query)

# Create an SGP4 propagator from the GP record
propagator = records[0].to_sgp_propagator(step_size=60.0)

# Configure propagation window
epoch_start = propagator.current_epoch()
epoch_end = epoch_start + 7.0 * 86400.0

# Propagate forward 7 days
propagator.propagate_to(epoch_end)

# Get final epoch and state
final_epoch = propagator.current_epoch()
final_state = propagator.current_state()
print(f"Initial epoch: {epoch_start}")
print(f"Final epoch:   {final_epoch}")
print(
    f"Position (km): [{final_state[0] / 1e3:.3f}, {final_state[1] / 1e3:.3f}, {final_state[2] / 1e3:.3f}]"
)
print(
    f"Velocity (m/s): [{final_state[3]:.3f}, {final_state[4]:.3f}, {final_state[5]:.3f}]"
)
#![allow(unused_imports)]
use brahe as bh;
use bh::spacetrack::{RequestClass, SortOrder, SpaceTrackClient, SpaceTrackQuery};
use brahe::traits::SStatePropagator;

fn main() {
    // Initialize EOP
    bh::initialize_eop().unwrap();

    // Authenticate with Space-Track using account credentials
    let username = std::env::var("SPACETRACK_USERNAME").unwrap();
    let password = std::env::var("SPACETRACK_PASSWORD").unwrap();
    let client = SpaceTrackClient::new(&username, &password);

    // Query the latest GP record for the ISS (NORAD ID 25544)
    let query = SpaceTrackQuery::new(RequestClass::GP)
        .filter("NORAD_CAT_ID", "25544")
        .order_by("EPOCH", SortOrder::Desc)
        .limit(1);
    let records = client.query_gp(&query).unwrap();

    // Create an SGP4 propagator from the GP record
    let mut propagator = bh::SGPPropagator::from_gp_record(&records[0], 60.0).unwrap();

    // Configure propagation window
    let epoch_start = propagator.current_epoch();
    let epoch_end = epoch_start + 7.0 * 86400.0;

    // Propagate forward 7 days
    propagator.propagate_to(epoch_end).unwrap();

    // Get final epoch and state
    let final_epoch = propagator.current_epoch();
    let final_state = propagator.current_state();
    println!("Initial epoch: {}", epoch_start);
    println!("Final epoch:   {}", final_epoch);
    println!(
        "Position (km): [{:.3}, {:.3}, {:.3}]",
        final_state[0] / 1e3,
        final_state[1] / 1e3,
        final_state[2] / 1e3
    );
    println!(
        "Velocity (m/s): [{:.3}, {:.3}, {:.3}]",
        final_state[3],
        final_state[4],
        final_state[5]
    );
}
Output
1
2
3
4
Initial epoch: 2026-06-09 18:30:49.921 UTC
Final epoch:   2026-06-16 18:30:49.921 UTC
Position (km): [-4582.532, 4686.441, -1786.235]
Velocity (m/s): [-2461.644, -4551.849, -5652.779]
1
2
3
4
Initial epoch: 2026-06-09 18:30:49.921 UTC
Final epoch:   2026-06-16 18:30:49.921 UTC
Position (km): [-4582.532, 4686.441, -1786.235]
Velocity (m/s): [-2461.644, -4551.849, -5652.779]

Starlink publishes the Modified ITC ephemerides it submits to Space-Track at a public, unauthenticated mirror. The StarlinkClient reads them directly and returns ITC messages and OrbitTrajectory objects, rather than the GPRecord returned by the CelesTrak and Space-Track clients above.

import brahe as bh

bh.initialize_eop()

client = bh.StarlinkClient(cache_max_age=7 * 86400.0)
manifest = client.get_manifest()
print(f"Manifest lists {len(manifest)} satellites (retrieved {manifest.retrieved})")

entry = manifest.find_by_object_name("STARLINK-38128")
print(f"{entry.object_name}: NORAD {entry.norad_cat_id}, {entry.category}")
print(f"  ephemeris {entry.ephemeris_start} to {entry.ephemeris_stop}")
print(f"  file {entry.file_name_string()}")

trajectory = client.get_trajectory(entry.norad_cat_id)
print(f"Trajectory: {len(trajectory)} samples in {trajectory.frame}")
print(f"Cached files: {len(client.cached_files())}")

df = manifest.to_dataframe()
print(df.select(["norad_cat_id", "object_name", "ephemeris_start"]).head(3))
use brahe as bh;
use brahe::starlink::StarlinkClient;
use brahe::traits::*;

fn main() {
    bh::initialize_eop().unwrap();

    let client = StarlinkClient::with_cache_age(7.0 * 86400.0);
    let manifest = client.get_manifest().unwrap();
    println!("Manifest lists {} satellites (retrieved {})", manifest.len(), manifest.retrieved);

    let entry = manifest.find_by_object_name("STARLINK-38128").unwrap();
    println!("{}: NORAD {}, {}", entry.object_name, entry.norad_cat_id, entry.category);
    println!("  ephemeris {} to {}", entry.ephemeris_start, entry.ephemeris_stop.map(|e| e.to_string()).unwrap_or_default());
    println!("  file {}", entry.file_name_string());

    let trajectory = client.get_trajectory(entry.norad_cat_id).unwrap();
    println!("Trajectory: {} samples in {}", trajectory.len(), trajectory.frame);
    println!("Cached files: {}", client.cached_files().unwrap().len());

    let df = manifest.to_dataframe().unwrap();
    let head = df.select(["norad_cat_id", "object_name", "ephemeris_start"]).unwrap().head(Some(3));
    println!("Manifest table shape: {:?}", head.shape());
    println!("Columns: {:?}", head.get_column_names());
}
Output
Manifest lists 5 satellites (retrieved 2026-10-22 06:47:02.440 UTC)
STARLINK-38128: NORAD 100001, Operational
  ephemeris 2026-09-11 01:42:00.000 UTC to None
  file MEME_100001_STARLINK-38128_2540142_Operational_1473385380_UNCLASSIFIED.txt
Trajectory: 4321 samples in EME2000
Cached files: 2
shape: (3, 3)
┌──────────────┬────────────────┬─────────────────────┐
│ norad_cat_id ┆ object_name    ┆ ephemeris_start     │
│ ---          ┆ ---            ┆ ---                 │
│ u32          ┆ str            ┆ datetime[ms]        │
╞══════════════╪════════════════╪═════════════════════╡
│ 100001       ┆ STARLINK-38128 ┆ 2026-09-11 01:42:00 │
│ 100002       ┆ STARLINK-37711 ┆ 2026-09-11 01:49:00 │
│ 100003       ┆ STARLINK-38123 ┆ 2026-09-11 01:41:00 │
└──────────────┴────────────────┴─────────────────────┘
1
2
3
4
5
6
7
8
Manifest lists 5 satellites (retrieved 2026-10-22 06:47:02.440 UTC)
STARLINK-38128: NORAD 100001, Operational
  ephemeris 2026-09-11 01:42:00.000 UTC to 
  file MEME_100001_STARLINK-38128_2540142_Operational_1473385380_UNCLASSIFIED.txt
Trajectory: 4321 samples in EME2000
Cached files: 2
Manifest table shape: (3, 3)
Columns: ["norad_cat_id", "object_name", "ephemeris_start"]