Skip to content

OPM — Orbit Parameter Message

An Orbit Parameter Message (OPM), defined by the CCSDS 502.0-B-3 Orbit Data Messages standard, carries a single spacecraft state at one epoch — position, velocity, and optionally Keplerian elements, spacecraft parameters, maneuvers, and covariance. It is the standard format for handing off initial conditions for propagation or documenting a maneuver plan.

Parse and Initialize a Propagator

Extract position, velocity, epoch, and spacecraft parameters from an OPM to initialize a NumericalOrbitPropagator:

import numpy as np

import brahe as bh
from brahe.ccsds import OPM

bh.initialize_eop()
bh.initialize_sw()

# Parse OPM — use Example1 which has spacecraft mass
opm = OPM.from_file("test_assets/ccsds/opm/OPMExample1.txt")
print(f"Object: {opm.object_name} ({opm.object_id})")
print(f"Epoch:  {opm.epoch}")
print(f"Frame:  {opm.ref_frame}")

# Extract initial conditions from OPM via .state property
initial_state = opm.state  # numpy array [x, y, z, vx, vy, vz]
print("\nInitial state (ITRF):")
print(
    f"  Position: [{initial_state[0] / 1e3:.3f}, {initial_state[1] / 1e3:.3f}, {initial_state[2] / 1e3:.3f}] km"
)
print(
    f"  Velocity: [{initial_state[3]:.3f}, {initial_state[4]:.3f}, {initial_state[5]:.3f}] m/s"
)

# Build spacecraft parameters from OPM
mass = opm.mass or 500.0
drag_area = opm.drag_area or 2.0
drag_coeff = opm.drag_coeff or 2.2
srp_area = opm.solar_rad_area or 2.0
srp_coeff = opm.solar_rad_coeff or 1.3
params = np.array([mass, drag_area, drag_coeff, srp_area, srp_coeff])
print(f"\nSpacecraft params: mass={mass}kg, Cd={drag_coeff}, Cr={srp_coeff}")

# Initialize propagator from OPM state
# The message declares its state in ITRF2000; the propagator expects GCRF, and
# state_in_frame maps the declared frame through the reference frame router
state_eci = opm.state_in_frame(bh.CelestialFrame.GCRF)
prop = bh.NumericalOrbitPropagator(
    opm.epoch,
    state_eci,
    bh.NumericalPropagationConfig.default(),
    bh.ForceModelConfig.default(),
    params,
)

# Propagate for 1 orbit period (approximately)
r = np.linalg.norm(initial_state[:3])
period = 2 * np.pi * np.sqrt(r**3 / bh.GM_EARTH)
print(f"\nEstimated period: {period:.0f}s ({period / 60:.1f} min)")

target_epoch = opm.epoch + period
prop.propagate_to(target_epoch)

# Check final state
final_state = prop.current_state()
print("\nAfter 1 orbit:")
print(f"  Epoch: {prop.current_epoch()}")
print(
    f"  Position: [{final_state[0] / 1e3:.3f}, {final_state[1] / 1e3:.3f}, {final_state[2] / 1e3:.3f}] km"
)
print(
    f"  Velocity: [{final_state[3]:.3f}, {final_state[4]:.3f}, {final_state[5]:.3f}] m/s"
)
use brahe as bh;
use bh::ccsds::OPM;
use bh::traits::DStatePropagator;
use nalgebra as na;

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

    // Parse OPM — use Example1 which has spacecraft mass
    let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample1.txt").unwrap();
    println!(
        "Object: {} ({})",
        opm.metadata.object_name, opm.metadata.object_id
    );
    println!("Epoch:  {}", opm.state_vector.epoch);
    println!("Frame:  {}", opm.metadata.ref_frame);

    // Extract initial conditions from OPM
    let pos = opm.state_vector.position;
    let vel = opm.state_vector.velocity;
    println!("\nInitial state:");
    println!(
        "  Position: [{:.3}, {:.3}, {:.3}] km",
        pos[0] / 1e3,
        pos[1] / 1e3,
        pos[2] / 1e3
    );
    println!(
        "  Velocity: [{:.3}, {:.3}, {:.3}] m/s",
        vel[0], vel[1], vel[2]
    );

    // Build spacecraft parameters from OPM
    let sc = opm.spacecraft_parameters.as_ref();
    let mass = sc.and_then(|s| s.mass).unwrap_or(500.0);
    let drag_area = sc.and_then(|s| s.drag_area).unwrap_or(2.0);
    let drag_coeff = sc.and_then(|s| s.drag_coeff).unwrap_or(2.2);
    let srp_area = sc.and_then(|s| s.solar_rad_area).unwrap_or(2.0);
    let srp_coeff = sc.and_then(|s| s.solar_rad_coeff).unwrap_or(1.3);
    let params = na::DVector::from_vec(vec![mass, drag_area, drag_coeff, srp_area, srp_coeff]);
    println!(
        "\nSpacecraft params: mass={}kg, Cd={}, Cr={}",
        mass, drag_coeff, srp_coeff
    );

    // The message declares its state in ITRF2000; the propagator expects GCRF,
    // and state_in_frame maps the declared frame through the reference frame router
    let state_eci = opm.state_in_frame(bh::CelestialFrame::GCRF).unwrap();
    let state_dyn = na::DVector::from_column_slice(state_eci.as_slice());

    // Initialize propagator from OPM state
    let mut prop = bh::DNumericalOrbitPropagator::builder(
        opm.state_vector.epoch,
        state_dyn,
        bh::ForceModelConfig::default(),
    )
    .params(params)
    .build()
    .unwrap();

    // Propagate for ~1 orbit period
    let r = (pos[0].powi(2) + pos[1].powi(2) + pos[2].powi(2)).sqrt();
    let period = 2.0 * std::f64::consts::PI * (r.powi(3) / bh::GM_EARTH).sqrt();
    println!("\nEstimated period: {:.0}s ({:.1} min)", period, period / 60.0);

    let target_epoch = opm.state_vector.epoch + period;
    prop.propagate_to(target_epoch).unwrap();

    // Check final state
    let final_state = prop.current_state();
    println!("\nAfter 1 orbit:");
    println!("  Epoch: {}", prop.current_epoch());
    println!(
        "  Position: [{:.3}, {:.3}, {:.3}] km",
        final_state[0] / 1e3,
        final_state[1] / 1e3,
        final_state[2] / 1e3
    );
    println!(
        "  Velocity: [{:.3}, {:.3}, {:.3}] m/s",
        final_state[3], final_state[4], final_state[5]
    );
}
Output
Object: GODZILLA 5 (1998-999A)
Epoch:  1998-12-18 14:28:15.117 UTC
Frame:  ITRF2000

Initial state (ITRF):
  Position: [6503.514, 1239.647, -717.490] km
  Velocity: [-873.160, 8740.420, -4191.076] m/s

Spacecraft params: mass=3000.0kg, Cd=2.5, Cr=1.0

Estimated period: 5408s (90.1 min)

After 1 orbit:
  Epoch: 1998-12-18 15:58:23.416 UTC
  Position: [3145.351, 24713.421, -7071.733] km
  Velocity: [-2068.116, 3184.124, 57.693] m/s
Object: GODZILLA 5 (1998-999A)
Epoch:  1998-12-18 14:28:15.117 UTC
Frame:  ITRF2000

Initial state:
  Position: [6503.514, 1239.647, -717.490] km
  Velocity: [-873.160, 8740.420, -4191.076] m/s

Spacecraft params: mass=3000kg, Cd=2.5, Cr=1

Estimated period: 5408s (90.1 min)

After 1 orbit:
  Epoch: 1998-12-18 15:58:23.416 UTC
  Position: [3145.351, 24713.421, -7071.733] km
  Velocity: [-2068.116, 3184.124, 57.693] m/s

Accessing OPM Data

Parse from file or string, then access the state vector, optional Keplerian elements, spacecraft parameters, covariance, and maneuvers:

import brahe as bh
from brahe.ccsds import OPM

bh.initialize_eop()

# Parse OPM with Keplerian elements and maneuvers
opm = OPM.from_file("test_assets/ccsds/opm/OPMExample2.txt")

# Header
print(f"Format version: {opm.format_version}")
print(f"Originator:     {opm.originator}")
print(f"Creation date:  {opm.creation_date}")

# Metadata
print(f"\nObject name:  {opm.object_name}")
print(f"Object ID:    {opm.object_id}")
print(f"Center name:  {opm.center_name}")
print(f"Ref frame:    {opm.ref_frame}")
print(f"Time system:  {opm.time_system}")

# State vector (SI units: meters, m/s)
print(f"\nEpoch:    {opm.epoch}")
pos = opm.position
vel = opm.velocity
print(f"Position: [{pos[0] / 1e3:.4f}, {pos[1] / 1e3:.4f}, {pos[2] / 1e3:.4f}] km")
print(f"Velocity: [{vel[0]:.8f}, {vel[1]:.8f}, {vel[2]:.8f}] m/s")

# Keplerian elements
print(f"\nHas Keplerian: {opm.has_keplerian_elements}")
if opm.has_keplerian_elements:
    print(f"  Semi-major axis:    {opm.semi_major_axis / 1e3:.4f} km")
    print(f"  Eccentricity:       {opm.eccentricity:.9f}")
    print(f"  Inclination:        {opm.inclination:.6f} deg")
    print(f"  RAAN:               {opm.ra_of_asc_node:.6f} deg")
    print(f"  Arg of pericenter:  {opm.arg_of_pericenter:.6f} deg")
    print(f"  True anomaly:       {opm.true_anomaly:.6f} deg")
    print(f"  GM:                 {opm.gm:.4e} m³/s²")

# Spacecraft parameters
print(f"\nMass:           {opm.mass} kg")
print(f"Solar rad area: {opm.solar_rad_area} m²")
print(f"Solar rad coef: {opm.solar_rad_coeff}")
print(f"Drag area:      {opm.drag_area} m²")
print(f"Drag coeff:     {opm.drag_coeff}")

# Maneuvers
print(f"\nManeuvers: {len(opm.maneuvers)}")
for i, man in enumerate(opm.maneuvers):
    print(f"\n  Maneuver {i}:")
    print(f"    Epoch ignition: {man.epoch_ignition}")
    print(f"    Duration:       {man.duration} s")
    print(f"    Delta mass:     {man.delta_mass} kg")
    print(f"    Ref frame:      {man.ref_frame}")
    dv = man.dv
    print(f"    Delta-V:        [{dv[0]:.5f}, {dv[1]:.5f}, {dv[2]:.5f}] m/s")
use brahe as bh;
use brahe::ccsds::OPM;

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

    // Parse OPM with Keplerian elements and maneuvers
    let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample2.txt").unwrap();

    // Header
    println!("Format version: {}", opm.header.format_version);
    println!("Originator:     {}", opm.header.originator);
    println!("Creation date:  {}", opm.header.creation_date);

    // Metadata
    println!("\nObject name:  {}", opm.metadata.object_name);
    println!("Object ID:    {}", opm.metadata.object_id);
    println!("Center name:  {}", opm.metadata.center_name);
    println!("Ref frame:    {}", opm.metadata.ref_frame);
    println!("Time system:  {}", opm.metadata.time_system);

    // State vector (SI units: meters, m/s)
    println!("\nEpoch:    {}", opm.state_vector.epoch);
    let pos = opm.state_vector.position;
    let vel = opm.state_vector.velocity;
    println!(
        "Position: [{:.4}, {:.4}, {:.4}] km",
        pos[0] / 1e3,
        pos[1] / 1e3,
        pos[2] / 1e3
    );
    println!(
        "Velocity: [{:.8}, {:.8}, {:.8}] m/s",
        vel[0], vel[1], vel[2]
    );

    // Keplerian elements
    if let Some(ref kep) = opm.keplerian_elements {
        println!("\nKeplerian elements:");
        println!("  Semi-major axis:    {:.4} km", kep.semi_major_axis / 1e3);
        println!("  Eccentricity:       {:.9}", kep.eccentricity);
        println!("  Inclination:        {:.6} deg", kep.inclination);
        println!("  RAAN:               {:.6} deg", kep.ra_of_asc_node);
        println!(
            "  Arg of pericenter:  {:.6} deg",
            kep.arg_of_pericenter
        );
        if let Some(ta) = kep.true_anomaly {
            println!("  True anomaly:       {:.6} deg", ta);
        }
        println!("  GM:                 {:.4e} m³/s²", kep.gm.unwrap_or(0.0));
    }

    // Spacecraft parameters
    if let Some(ref sc) = opm.spacecraft_parameters {
        println!("\nMass:           {} kg", sc.mass.unwrap_or(0.0));
        println!(
            "Solar rad area: {} m²",
            sc.solar_rad_area.unwrap_or(0.0)
        );
        println!(
            "Solar rad coef: {}",
            sc.solar_rad_coeff.unwrap_or(0.0)
        );
        println!("Drag area:      {} m²", sc.drag_area.unwrap_or(0.0));
        println!("Drag coeff:     {}", sc.drag_coeff.unwrap_or(0.0));
    }

    // Maneuvers
    println!("\nManeuvers: {}", opm.maneuvers.len());
    for (i, man) in opm.maneuvers.iter().enumerate() {
        println!("\n  Maneuver {}:", i);
        println!("    Epoch ignition: {}", man.epoch_ignition);
        println!("    Duration:       {} s", man.duration);
        println!(
            "    Delta mass:     {} kg",
            man.delta_mass.map_or("None".to_string(), |m| format!("{}", m))
        );
        println!("    Ref frame:      {}", man.ref_frame);
        println!(
            "    Delta-V:        [{:.5}, {:.5}, {:.5}] m/s",
            man.dv[0], man.dv[1], man.dv[2]
        );
    }
}
Output
Format version: 3.0
Originator:     GSOC
Creation date:  2000-06-03 05:33:00.000 UTC

Object name:  EUTELSAT W4
Object ID:    2000-028A
Center name:  EARTH
Ref frame:    TOD
Time system:  UTC

Epoch:    2006-06-03 00:00:00.000 UTC
Position: [6655.9942, -40218.5751, -82.9177] km
Velocity: [3115.48208000, 470.42605000, -1.01495000] m/s

Has Keplerian: True
  Semi-major axis:    41399.5123 km
  Eccentricity:       0.020842611
  Inclination:        0.117746 deg
  RAAN:               17.604721 deg
  Arg of pericenter:  218.242943 deg
  True anomaly:       41.922339 deg
  GM:                 3.9860e+14 m³/s²

Mass:           1913.0 kg
Solar rad area: 10.0 m²
Solar rad coef: 1.3
Drag area:      10.0 m²
Drag coeff:     2.3

Maneuvers: 2

  Maneuver 0:
    Epoch ignition: 2000-06-03 09:00:34.100 UTC
    Duration:       132.6 s
    Delta mass:     -18.418 kg
    Ref frame:      J2000
    Delta-V:        [-23.25700, 16.83160, -8.93444] m/s

  Maneuver 1:
    Epoch ignition: 2000-06-05 18:59:21.000 UTC
    Duration:       0.0 s
    Delta mass:     -1.469 kg
    Ref frame:      RTN
    Delta-V:        [1.01500, -1.87300, 0.00000] m/s
Format version: 3
Originator:     GSOC
Creation date:  2000-06-03 05:33:00.000 UTC

Object name:  EUTELSAT W4
Object ID:    2000-028A
Center name:  EARTH
Ref frame:    TOD
Time system:  UTC

Epoch:    2006-06-03 00:00:00.000 UTC
Position: [6655.9942, -40218.5751, -82.9177] km
Velocity: [3115.48208000, 470.42605000, -1.01495000] m/s

Keplerian elements:
  Semi-major axis:    41399.5123 km
  Eccentricity:       0.020842611
  Inclination:        0.117746 deg
  RAAN:               17.604721 deg
  Arg of pericenter:  218.242943 deg
  True anomaly:       41.922339 deg
  GM:                 3.9860e14 m³/s²

Mass:           1913 kg
Solar rad area: 10 m²
Solar rad coef: 1.3
Drag area:      10 m²
Drag coeff:     2.3

Maneuvers: 2

  Maneuver 0:
    Epoch ignition: 2000-06-03 09:00:34.100 UTC
    Duration:       132.6 s
    Delta mass:     -18.418 kg
    Ref frame:      J2000
    Delta-V:        [-23.25700, 16.83160, -8.93444] m/s

  Maneuver 1:
    Epoch ignition: 2000-06-05 18:59:21.000 UTC
    Duration:       0 s
    Delta mass:     -1.469 kg
    Ref frame:      RTN
    Delta-V:        [1.01500, -1.87300, 0.00000] m/s

What an OPM Contains

Every OPM has a header (version, creation date, originator), metadata (object identity, center body, reference frame, time system), and a state vector (epoch plus position and velocity). Beyond these required parts, four optional sections can be present.

CENTER_NAME and REF_FRAME are resolved jointly: REF_FRAME names the state vector's orientation and CENTER_NAME its origin, independently of each other. state_in_frame converts the state vector to any other supported frame through the reference frame router; converting to a frame centered on the same body as the OPM's CENTER_NAME is a rotation only, while converting to a frame centered on a different body also translates through the loaded SPK kernels. See Axes and Centers for the full FrameAxes/CelestialFrame picture.

Keplerian elements duplicate the state vector information in orbital-element form — semi-major axis, eccentricity, inclination, RAAN, argument of pericenter, and true or mean anomaly, plus \(GM\). The redundancy is intentional: elements are easier for humans to review at a glance, and some receiving systems prefer them as input.

Spacecraft parameters record physical properties relevant to force modeling — mass, drag area and coefficient (\(C_D\)), and solar radiation pressure area and coefficient (\(C_R\)). These feed directly into atmospheric drag and SRP force models during numerical propagation.

Maneuvers describe planned or executed burns. Each maneuver specifies an ignition epoch, duration, delta-mass, reference frame, and three delta-V components. Multiple maneuvers are allowed, and the reference frame can differ between them (e.g., RTN for in-plane burns, EME2000 for inertial targeting).

Covariance provides a 6\(\times\)6 symmetric position-velocity covariance matrix with an optional reference frame override relative to the state vector frame.

Maneuver Propagation

Read OPM maneuvers and apply them as impulsive delta-V events during propagation. The example message declares its state vector in the TOD frame, and state_in_frame converts it to GCRF through the reference frame router before it is handed to the propagator. The message's maneuver ignition epochs precede its state vector epoch, so the example schedules the maneuvers relative to the state epoch, preserving the spacing between them:

import numpy as np

import brahe as bh
from brahe.ccsds import OPM

bh.initialize_eop()
bh.initialize_sw()

# Parse OPM with maneuvers
opm = OPM.from_file("test_assets/ccsds/opm/OPMExample2.txt")
print(f"Object: {opm.object_name}")
print(f"Epoch:  {opm.epoch}")
print(f"Maneuvers: {len(opm.maneuvers)}")

# Extract initial state; the OPM declares its state in the TOD frame
state_eci = opm.state_in_frame(bh.CelestialFrame.GCRF)

# Spacecraft parameters from OPM
mass = opm.mass or 500.0
params = np.array(
    [
        mass,
        opm.drag_area or 10.0,
        opm.drag_coeff or 2.3,
        opm.solar_rad_area or 10.0,
        opm.solar_rad_coeff or 1.3,
    ]
)

# Create propagator
prop = bh.NumericalOrbitPropagator(
    opm.epoch,
    state_eci,
    bh.NumericalPropagationConfig.default(),
    bh.ForceModelConfig.default(),
    params,
)

# The message's ignition epochs precede its state epoch, so maneuvers are
# scheduled relative to the state epoch, preserving the spacing between them
first_ignition = opm.maneuvers[0].epoch_ignition
scheduled_epochs = [
    opm.epoch + 3600.0 + (man.epoch_ignition - first_ignition) for man in opm.maneuvers
]


def make_callback(dv_vec, man_idx, is_rtn):
    """Create a closure that rotates the delta-V into GCRF and applies it.

    Args:
        dv_vec (numpy.ndarray): Delta-V [dv1, dv2, dv3] in the maneuver frame (m/s)
        man_idx (int): Index of the maneuver within the OPM
        is_rtn (bool): True if `dv_vec` is expressed in the RTN frame

    Returns:
        callable: Event callback returning the post-maneuver state and action
    """

    def apply_dv(epoch, state):
        dv_gcrf = bh.rotation_rtn_to_eci(state) @ dv_vec if is_rtn else dv_vec
        new_state = state.copy()
        new_state[3] += dv_gcrf[0]
        new_state[4] += dv_gcrf[1]
        new_state[5] += dv_gcrf[2]
        dv_mag = np.linalg.norm(dv_gcrf)
        print(f"  Applied maneuver {man_idx} at {epoch}: |dv|={dv_mag:.3f} m/s")
        return (new_state, bh.EventAction.CONTINUE)

    return apply_dv


# The frame bias between EME2000 (alias J2000) and GCRF is epoch-independent
r_eme2000_to_gcrf = bh.rotation_eme2000_to_gcrf()

# Add an event detector for each maneuver
for i, (man, sched_epoch) in enumerate(zip(opm.maneuvers, scheduled_epochs)):
    dv = man.dv  # [dv1, dv2, dv3] in m/s in the maneuver's ref frame
    frame = man.ref_frame

    if frame in ("J2000", "EME2000"):
        # Inertial delta-V: rotate into GCRF once, ahead of propagation
        callback = make_callback(r_eme2000_to_gcrf @ dv, i, False)
    elif frame == "RTN":
        # RTN delta-V: the rotation depends on the state at the ignition epoch
        callback = make_callback(dv, i, True)
    else:
        raise ValueError(f"Unsupported maneuver reference frame: {frame}")

    event = bh.TimeEvent(sched_epoch, f"Maneuver-{i}")
    event = event.with_callback(callback)
    prop.add_event_detector(event)
    print(
        f"  Registered maneuver {i}: epoch={sched_epoch}, frame={frame}, "
        f"|dv|={np.linalg.norm(dv):.3f} m/s"
    )

# Propagate past all maneuvers
target = scheduled_epochs[-1] + 3600.0  # 1 hour after last maneuver
print(f"\nPropagating to {target}...")
prop.propagate_to(target)

# Report final state
final = prop.current_state()
print(f"\nFinal state at {prop.current_epoch()}:")
print(
    f"  Position: [{final[0] / 1e3:.3f}, {final[1] / 1e3:.3f}, {final[2] / 1e3:.3f}] km"
)
print(f"  Velocity: [{final[3]:.3f}, {final[4]:.3f}, {final[5]:.3f}] m/s")

# Check event log
events = prop.event_log()
print(f"\nEvent log: {len(events)} events triggered")
for e in events:
    print(f"  {e}")
use brahe as bh;
use bh::ccsds::{CCSDSRefFrame, OPM};
use bh::events::{DTimeEvent, EventAction};
use bh::traits::DStatePropagator;
use nalgebra as na;

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

    // Parse OPM with maneuvers
    let opm = OPM::from_file("test_assets/ccsds/opm/OPMExample2.txt").unwrap();
    println!("Object: {}", opm.metadata.object_name);
    println!("Epoch:  {}", opm.state_vector.epoch);
    println!("Maneuvers: {}", opm.maneuvers.len());

    // Extract initial state; the OPM declares its state in the TOD frame
    let state_eci = opm.state_in_frame(bh::CelestialFrame::GCRF).unwrap();

    // Spacecraft parameters
    let sc = opm.spacecraft_parameters.as_ref();
    let mass = sc.and_then(|s| s.mass).unwrap_or(500.0);
    let params = na::DVector::from_vec(vec![
        mass,
        sc.and_then(|s| s.drag_area).unwrap_or(10.0),
        sc.and_then(|s| s.drag_coeff).unwrap_or(2.3),
        sc.and_then(|s| s.solar_rad_area).unwrap_or(10.0),
        sc.and_then(|s| s.solar_rad_coeff).unwrap_or(1.3),
    ]);

    // Create propagator
    let mut prop = bh::DNumericalOrbitPropagator::builder(
        opm.state_vector.epoch,
        na::DVector::from_column_slice(state_eci.as_slice()),
        bh::ForceModelConfig::default(),
    )
    .params(params)
    .build()
    .unwrap();

    // The message's ignition epochs precede its state epoch, so maneuvers are
    // scheduled relative to the state epoch, preserving the spacing between them
    let first_ignition = opm.maneuvers[0].epoch_ignition;
    let scheduled_epochs: Vec<bh::Epoch> = opm
        .maneuvers
        .iter()
        .map(|man| opm.state_vector.epoch + 3600.0 + (man.epoch_ignition - first_ignition))
        .collect();

    // The frame bias between EME2000 (alias J2000) and GCRF is epoch-independent
    let r_eme2000_to_gcrf = bh::rotation_eme2000_to_gcrf();

    // Add an event detector for each maneuver
    for (i, man) in opm.maneuvers.iter().enumerate() {
        let sched_epoch = scheduled_epochs[i];
        let dv = na::Vector3::new(man.dv[0], man.dv[1], man.dv[2]);
        let dv_mag = dv.norm();
        let idx = i;

        // Inertial delta-Vs rotate into GCRF once; RTN delta-Vs depend on the
        // state at the ignition epoch and rotate inside the callback
        let is_rtn = match man.ref_frame {
            CCSDSRefFrame::J2000 | CCSDSRefFrame::EME2000 => false,
            CCSDSRefFrame::RTN => true,
            ref other => panic!("Unsupported maneuver reference frame: {}", other),
        };
        let dv_frame = if is_rtn { dv } else { r_eme2000_to_gcrf * dv };

        let callback: bh::events::DEventCallback = Box::new(
            move |_t: bh::Epoch,
                  state: &na::DVector<f64>,
                  _params: Option<&na::DVector<f64>>|
                  -> (Option<na::DVector<f64>>, Option<na::DVector<f64>>, EventAction) {
                let dv_gcrf = if is_rtn {
                    let x = na::SVector::<f64, 6>::from_column_slice(&state.as_slice()[..6]);
                    bh::rotation_rtn_to_eci(x) * dv_frame
                } else {
                    dv_frame
                };
                let mut new_state = state.clone();
                new_state[3] += dv_gcrf[0];
                new_state[4] += dv_gcrf[1];
                new_state[5] += dv_gcrf[2];
                println!("  Applied maneuver {}: |dv|={:.3} m/s", idx, dv_gcrf.norm());
                (Some(new_state), None, EventAction::Continue)
            },
        );

        let event =
            DTimeEvent::new(sched_epoch, format!("Maneuver-{}", i)).with_callback(callback);
        prop.add_event_detector(Box::new(event));
        println!(
            "  Registered maneuver {}: epoch={}, frame={}, |dv|={:.3} m/s",
            i, sched_epoch, man.ref_frame, dv_mag
        );
    }

    // Propagate past all maneuvers
    let target = *scheduled_epochs.last().unwrap() + 3600.0;
    println!("\nPropagating to {}...", target);
    prop.propagate_to(target).unwrap();

    // Report final state
    let final_state = prop.current_state();
    println!("\nFinal state at {}:", prop.current_epoch());
    println!(
        "  Position: [{:.3}, {:.3}, {:.3}] km",
        final_state[0] / 1e3,
        final_state[1] / 1e3,
        final_state[2] / 1e3
    );
    println!(
        "  Velocity: [{:.3}, {:.3}, {:.3}] m/s",
        final_state[3], final_state[4], final_state[5]
    );

    println!("\nExample completed successfully!");
}
Output


KVN Format Example

An OPM KVN file with a state vector, Keplerian elements, and a maneuver:

CCSDS_OPM_VERS = 3.0
CREATION_DATE = 2024-01-15T00:00:00
ORIGINATOR = EXAMPLE

OBJECT_NAME = MY SATELLITE
OBJECT_ID = 2024-001A
CENTER_NAME = EARTH
REF_FRAME = EME2000
TIME_SYSTEM = UTC

EPOCH = 2024-01-15T00:00:00
X = 6878.137 [km]
Y = 0.000 [km]
Z = 0.000 [km]
X_DOT = 0.000 [km/s]
Y_DOT = 7.612 [km/s]
Z_DOT = 0.000 [km/s]

SEMI_MAJOR_AXIS = 6878.137 [km]
ECCENTRICITY = 0.001
INCLINATION = 0.0 [deg]
RA_OF_ASC_NODE = 0.0 [deg]
ARG_OF_PERICENTER = 0.0 [deg]
TRUE_ANOMALY = 0.0 [deg]
GM = 398600.4415 [km**3/s**2]

MAN_EPOCH_IGNITION = 2024-01-15T01:00:00
MAN_DURATION = 60.0 [s]
MAN_DELTA_MASS = -5.0 [kg]
MAN_REF_FRAME = RTN
MAN_DV_1 = 0.010 [km/s]
MAN_DV_2 = 0.000 [km/s]
MAN_DV_3 = 0.000 [km/s]

Note the optional unit annotations in square brackets ([km], [deg]). Brahe strips these during parsing.


See Also