Skip to content

Parallel Orbit Propagation

When working with multiple satellites (constellations, Monte Carlo simulations, etc.), propagating each satellite sequentially can be slow. The par_propagate_to function enables efficient parallel propagation by utilizing multiple CPU cores. The parallel propagation function uses Rayon's work-stealing thread pool, configured via brahe.set_num_threads().

When to Use Parallel Propagation

  • Propagating constellations (10s to 1000s of satellites)
  • Running Monte Carlo simulations
  • Batch processing orbital predictions
  • You have multiple CPU cores available

See the threading documentation for more details on configuring threading in Brahe.

Basic Example

This example creates a constellation of 10 satellites and propagates them 24 hours forward in parallel:

import brahe as bh
import numpy as np
import time

bh.initialize_eop()

# Create initial epoch
epoch = bh.Epoch.from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, bh.TimeSystem.UTC)

# Create multiple propagators for a constellation
num_sats = 10
propagators = []

for i in range(num_sats):
    # Vary semi-major axis slightly for each satellite
    a = bh.R_EARTH + 500e3 + i * 10e3
    oe = np.array([a, 0.001, 98.0, i * 36.0, 0.0, i * 36.0])
    state = bh.state_koe_to_eci(oe, bh.AngleFormat.DEGREES)
    prop = bh.KeplerianPropagator.from_eci(epoch, state, 60.0)
    propagators.append(prop)

# Target epoch: 24 hours later
target = epoch + 86400.0

# Propagate all satellites in parallel
start = time.time()
bh.par_propagate_to(propagators, target)
parallel_time = time.time() - start

print(f"Propagated {num_sats} satellites in parallel: {parallel_time:.4f} seconds")
print("\nFinal states:")
for i, prop in enumerate(propagators):
    state = prop.current_state()
    print(f"  Satellite {i}: r = {np.linalg.norm(state[:3]) / 1e3:.1f} km")

# Output:
# Propagated 10 satellites in parallel: 0.0026 seconds

# Final states:
#   Satellite 0: r = 6876.8 km
#   Satellite 1: r = 6889.7 km
#   Satellite 2: r = 6902.3 km
#   Satellite 3: r = 6914.2 km
#   Satellite 4: r = 6925.0 km
#   Satellite 5: r = 6934.7 km
#   Satellite 6: r = 6943.1 km
#   Satellite 7: r = 6950.7 km
#   Satellite 8: r = 6957.8 km
#   Satellite 9: r = 6965.0 km
use brahe as bh;
use brahe::traits::SStatePropagator;
use nalgebra as na;

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

    // Create initial epoch
    let epoch = bh::Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, bh::TimeSystem::UTC);

    // Create multiple propagators for a constellation
    let num_sats = 10;
    let mut propagators = Vec::new();

    for i in 0..num_sats {
        // Vary semi-major axis slightly for each satellite
        let a = bh::R_EARTH + 500e3 + (i as f64) * 10e3;
        let oe = na::SVector::<f64, 6>::new(
            a,
            0.001,
            98.0,
            (i as f64) * 36.0,
            0.0,
            (i as f64) * 36.0,
        );
        let state = bh::state_koe_to_eci(oe, bh::AngleFormat::Degrees);
        let prop = bh::KeplerianPropagator::from_eci(epoch, state, 60.0);
        propagators.push(prop);
    }

    // Target epoch: 24 hours later
    let target = epoch + 86400.0;

    // Propagate all satellites in parallel
    let start = std::time::Instant::now();
    bh::par_propagate_to_s(&mut propagators, target);
    let parallel_time = start.elapsed();

    println!(
        "Propagated {} satellites in parallel: {:.4} seconds",
        num_sats,
        parallel_time.as_secs_f64()
    );
    println!("\nFinal states:");
    for (i, prop) in propagators.iter().enumerate() {
        let state = prop.current_state();
        let r = (state[0].powi(2) + state[1].powi(2) + state[2].powi(2)).sqrt();
        println!("  Satellite {}: r = {:.1} km", i, r / 1e3);
    }
}

// Output:
// Propagated 10 satellites in parallel: 0.0026 seconds

// Final states:
//   Satellite 0: r = 6876.8 km
//   Satellite 1: r = 6889.7 km
//   Satellite 2: r = 6902.3 km
//   Satellite 3: r = 6914.2 km
//   Satellite 4: r = 6925.0 km
//   Satellite 5: r = 6934.7 km
//   Satellite 6: r = 6943.1 km
//   Satellite 7: r = 6950.7 km
//   Satellite 8: r = 6957.8 km
//   Satellite 9: r = 6965.0 km

Mixing Propagator Types

All propagators in the list must be the same type (either all KeplerianPropagator or all SGPPropagator). Mixing types will raise a TypeError:

1
2
3
4
5
6
7
8
import brahe as bh

# Example propgator intiailization
kep_prop = bh.KeplerianPropagator.from_eci(epoch, state, 60.0)
sgp_prop = bh.SGPPropagator.from_tle(line1, line2, 60.0)

# This will raise TypeError
bh.par_propagate_to([kep_prop, sgp_prop], target)

Memory Considerations

The parallel function clones each propagator before propagation, then updates the originals with final states. Memory usage scales linearly with the number of propagators.

For very large constellations (1000s of satellites), consider processing in batches and monitoring memory usage to avoid crashes from memory exhaustion.

Error Handling

If any propagator fails during parallel propagation, the function will panic (Rust) or raise an exception (Python). This can occur with SGP4 propagators when satellites decay below Earth's surface.

See Also