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.
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
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.
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:
importnumpyasnpimportbraheasbhfrombrahe.ccsdsimportOPMbh.initialize_eop()bh.initialize_sw()# Parse OPM with maneuversopm=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 framestate_eci=opm.state_in_frame(bh.CelestialFrame.GCRF)# Spacecraft parameters from OPMmass=opm.massor500.0params=np.array([mass,opm.drag_areaor10.0,opm.drag_coeffor2.3,opm.solar_rad_areaor10.0,opm.solar_rad_coeffor1.3,])# Create propagatorprop=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 themfirst_ignition=opm.maneuvers[0].epoch_ignitionscheduled_epochs=[opm.epoch+3600.0+(man.epoch_ignition-first_ignition)formaninopm.maneuvers]defmake_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 """defapply_dv(epoch,state):dv_gcrf=bh.rotation_rtn_to_eci(state)@dv_vecifis_rtnelsedv_vecnew_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)returnapply_dv# The frame bias between EME2000 (alias J2000) and GCRF is epoch-independentr_eme2000_to_gcrf=bh.rotation_eme2000_to_gcrf()# Add an event detector for each maneuverfori,(man,sched_epoch)inenumerate(zip(opm.maneuvers,scheduled_epochs)):dv=man.dv# [dv1, dv2, dv3] in m/s in the maneuver's ref frameframe=man.ref_frameifframein("J2000","EME2000"):# Inertial delta-V: rotate into GCRF once, ahead of propagationcallback=make_callback(r_eme2000_to_gcrf@dv,i,False)elifframe=="RTN":# RTN delta-V: the rotation depends on the state at the ignition epochcallback=make_callback(dv,i,True)else:raiseValueError(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 maneuverstarget=scheduled_epochs[-1]+3600.0# 1 hour after last maneuverprint(f"\nPropagating to {target}...")prop.propagate_to(target)# Report final statefinal=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 logevents=prop.event_log()print(f"\nEvent log: {len(events)} events triggered")foreinevents:print(f" {e}")
usebraheasbh;usebh::ccsds::{CCSDSRefFrame,OPM};usebh::events::{DTimeEvent,EventAction};usebh::traits::DStatePropagator;usenalgebraasna;fnmain(){bh::initialize_eop().unwrap();bh::initialize_sw().unwrap();// Parse OPM with maneuversletopm=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 frameletstate_eci=opm.state_in_frame(bh::CelestialFrame::GCRF).unwrap();// Spacecraft parametersletsc=opm.spacecraft_parameters.as_ref();letmass=sc.and_then(|s|s.mass).unwrap_or(500.0);letparams=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 propagatorletmutprop=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 themletfirst_ignition=opm.maneuvers[0].epoch_ignition;letscheduled_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-independentletr_eme2000_to_gcrf=bh::rotation_eme2000_to_gcrf();// Add an event detector for each maneuverfor(i,man)inopm.maneuvers.iter().enumerate(){letsched_epoch=scheduled_epochs[i];letdv=na::Vector3::new(man.dv[0],man.dv[1],man.dv[2]);letdv_mag=dv.norm();letidx=i;// Inertial delta-Vs rotate into GCRF once; RTN delta-Vs depend on the// state at the ignition epoch and rotate inside the callbackletis_rtn=matchman.ref_frame{CCSDSRefFrame::J2000|CCSDSRefFrame::EME2000=>false,CCSDSRefFrame::RTN=>true,refother=>panic!("Unsupported maneuver reference frame: {}",other),};letdv_frame=ifis_rtn{dv}else{r_eme2000_to_gcrf*dv};letcallback: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){letdv_gcrf=ifis_rtn{letx=na::SVector::<f64,6>::from_column_slice(&state.as_slice()[..6]);bh::rotation_rtn_to_eci(x)*dv_frame}else{dv_frame};letmutnew_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)},);letevent=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 maneuverslettarget=*scheduled_epochs.last().unwrap()+3600.0;println!("\nPropagating to {}...",target);prop.propagate_to(target).unwrap();// Report final stateletfinal_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!");}