An Orbit Ephemeris Message (OEM) carries time-ordered state vectors for spacecraft ephemeris exchange. The typical workflow is to parse an OEM file and convert it into an OrbitTrajectory for interpolation and analysis, or to generate an OEM from a propagator for distribution.
The primary interoperability point for OEM data is conversion to brahe's OrbitTrajectory. Each OEM segment maps to a trajectory object, giving you Hermite interpolation at arbitrary epochs within the covered time span:
An OEM message begins with a header that records the format version, creation date, and originator. The bulk of the data lives in one or more segments, each of which has its own metadata block and a sequence of state vectors.
Multiple segments exist because a single file may need to cover different trajectory arcs. A maneuver boundary, a change in reference frame, or a gap in tracking data each warrant a new segment. Within a segment, the metadata block records the object identity, center body, reference frame, time system, time span, and interpolation settings. The state vectors follow — each line provides an epoch plus position and velocity (and optionally acceleration). If covariance data is available, it appears as one or more 6\(\times\)6 symmetric matrices attached to the segment, each with its own epoch and optional reference frame override.
Build an OEM programmatically by defining a header, adding segments with metadata, and populating state vectors. The resulting message can be serialized to KVN, XML, or JSON:
importbraheasbhimportnumpyasnpfrombrahe.ccsdsimportOEMbh.initialize_eop()# Create a new OEM with header infooem=OEM(originator="BRAHE_EXAMPLE")oem.classification="unclassified"oem.message_id="OEM-2024-001"# Define a LEO orbit and propagate with KeplerianPropagator (two-body)epoch=bh.Epoch.from_datetime(2024,6,15,0,0,0.0,0.0,bh.TimeSystem.UTC)oe=np.array([bh.R_EARTH+500e3,0.001,51.6,15.0,30.0,0.0])prop=bh.KeplerianPropagator.from_keplerian(epoch,oe,bh.AngleFormat.DEGREES,60.0)# Add a segment with metadatastep=60.0# 60-second spacingn_states=5stop_epoch=epoch+step*(n_states-1)seg_idx=oem.add_segment(object_name="LEO SAT",object_id="2024-100A",center_name="EARTH",ref_frame="EME2000",time_system="UTC",start_time=epoch,stop_time=stop_epoch,interpolation="LAGRANGE",interpolation_degree=7,)# Propagate to build trajectory, then bulk-add states to segmentprop.propagate_to(stop_epoch)seg=oem.segments[seg_idx]seg.add_trajectory(prop.trajectory)print(f"Created OEM with {len(oem.segments)} segment, {seg.num_states} states")# Created OEM with 1 segment, 5 states# Write to KVN stringkvn=oem.to_string("KVN")print(f"\nKVN output ({len(kvn)} chars):")print(kvn[:500])# Write to fileoem.to_file("/tmp/brahe_example_oem.txt","KVN")print("\nWritten to /tmp/brahe_example_oem.txt")# Verify round-tripoem2=OEM.from_file("/tmp/brahe_example_oem.txt")print(f"Round-trip: {len(oem2.segments)} segment, {oem2.segments[0].num_states} states")# Round-trip: 1 segment, 5 states
usebraheasbh;usebrahe::ccsds::{CCSDSFormat,CCSDSRefFrame,CCSDSTimeSystem,OEM,OEMMetadata,OEMSegment,OEMStateVector,};usebrahe::traits::DStateProvider;usenalgebraasna;fnmain(){bh::initialize_eop().unwrap();// Create a new OEM with header infoletmutoem=OEM::new("BRAHE_EXAMPLE".to_string());// Define a LEO orbit and propagate with KeplerianPropagator (two-body)letepoch=bh::Epoch::from_datetime(2024,6,15,0,0,0.0,0.0,bh::TimeSystem::UTC);letoe=na::SVector::<f64,6>::new(bh::R_EARTH+500e3,0.001,51.6,15.0,30.0,0.0);letprop=bh::KeplerianPropagator::from_keplerian(epoch,oe,bh::AngleFormat::Degrees,60.0);// Create segment metadataletstep=60.0_f64;// 60-second spacingletn_states=5usize;letstop_epoch=epoch+step*(n_states-1)asf64;letmetadata=OEMMetadata::new("LEO SAT".to_string(),"2024-100A".to_string(),"EARTH".to_string(),CCSDSRefFrame::EME2000,CCSDSTimeSystem::UTC,epoch,stop_epoch,).with_interpolation("LAGRANGE".to_string(),Some(7));letmutseg=OEMSegment::new(metadata);// Populate states from the Keplerian propagatorforiin0..n_states{lett=epoch+iasf64*step;lets=prop.state(t).unwrap();seg.push_state(OEMStateVector::new(t,[s[0],s[1],s[2]],[s[3],s[4],s[5]],));}oem.push_segment(seg);println!("Created OEM with {} segment, {} states",oem.segments.len(),oem.segments[0].states.len());// Expected output:// Created OEM with 1 segment, 5 states// Write to KVN stringletkvn=oem.to_string(CCSDSFormat::KVN).unwrap();println!("\nKVN output ({} chars):",kvn.len());letpreview:String=kvn.chars().take(500).collect();println!("{}",preview);// Write to fileoem.to_file("/tmp/brahe_example_oem.txt",CCSDSFormat::KVN).unwrap();println!("\nWritten to /tmp/brahe_example_oem.txt");// Verify round-tripletoem2=OEM::from_file("/tmp/brahe_example_oem.txt").unwrap();println!("Round-trip: {} segment, {} states",oem2.segments.len(),oem2.segments[0].states.len());// Expected output:// Round-trip: 1 segment, 5 states}
Round-Trip Fidelity
Writing and re-parsing an OEM preserves all metadata, state vectors, and covariance data. Numeric precision may vary slightly due to floating-point formatting, but values are preserved within the precision of the output format.
importbraheasbhimportnumpyasnpfrombrahe.ccsdsimportOEMbh.initialize_eop()bh.initialize_sw()# Define initial stateepoch=bh.Epoch.from_datetime(2024,6,15,0,0,0.0,0.0,bh.TimeSystem.UTC)oe=np.array([bh.R_EARTH+500e3,0.001,51.6,15.0,30.0,45.0])state=bh.state_koe_to_eci(oe,bh.AngleFormat.DEGREES)params=np.array([500.0,2.0,2.2,2.0,1.3])# Create propagator with default force modelprop=bh.NumericalOrbitPropagator(epoch,state,bh.NumericalPropagationConfig.default(),bh.ForceModelConfig.default(),params,)# Propagate for 90 minutestarget_epoch=epoch+5400.0prop.propagate_to(target_epoch)print(f"Propagated from {epoch} to {prop.current_epoch()}")# Get the accumulated trajectorytraj=prop.trajectoryprint(f"Trajectory: {len(traj)} states, span={traj.timespan():.0f}s")# Build an OEM from the trajectory states using the trajectory kwargoem=OEM(originator="BRAHE_PROP")stop_epoch=prop.current_epoch()seg_idx=oem.add_segment(object_name="LEO SAT",object_id="2024-100A",center_name="EARTH",ref_frame="EME2000",time_system="UTC",start_time=epoch,stop_time=stop_epoch,interpolation="LAGRANGE",interpolation_degree=7,trajectory=traj,)seg=oem.segments[seg_idx]print(f"\nOEM: {len(oem.segments)} segment, {seg.num_states} states")# Write to KVNkvn=oem.to_string("KVN")print(f"KVN output: {len(kvn)} characters")# Verify by re-parsingoem2=OEM.from_str(kvn)print(f"Round-trip: {oem2.segments[0].num_states} states")
usebraheasbh;usebh::ccsds::{CCSDSFormat,CCSDSRefFrame,CCSDSTimeSystem,OEM,OEMMetadata,OEMSegment,OEMStateVector,};usebh::traits::{DStatePropagator,Trajectory};usenalgebraasna;fnmain(){bh::initialize_eop().unwrap();bh::initialize_sw().unwrap();// Define initial stateletepoch=bh::Epoch::from_datetime(2024,6,15,0,0,0.0,0.0,bh::TimeSystem::UTC);letoe=na::SVector::<f64,6>::new(bh::R_EARTH+500e3,0.001,51.6,15.0,30.0,45.0);letstate=bh::state_koe_to_eci(oe,bh::AngleFormat::Degrees);letparams=na::DVector::from_vec(vec![500.0,2.0,2.2,2.0,1.3]);// Create propagator with default force modelletmutprop=bh::DNumericalOrbitPropagator::new(epoch,na::DVector::from_column_slice(state.as_slice()),bh::NumericalPropagationConfig::default(),bh::ForceModelConfig::default(),Some(params),None,None,None,).unwrap();// Propagate for 90 minuteslettarget_epoch=epoch+5400.0;prop.propagate_to(target_epoch);println!("Propagated from {} to {}",epoch,prop.current_epoch());// Get the accumulated trajectorylettraj=prop.trajectory();println!("Trajectory: {} states, span={:.0}s",traj.len(),traj.timespan().unwrap());// Build an OEM from the trajectory statesletmutoem=OEM::new("BRAHE_PROP".to_string());letstop_epoch=prop.current_epoch();letmetadata=OEMMetadata::new("LEO SAT".to_string(),"2024-100A".to_string(),"EARTH".to_string(),CCSDSRefFrame::EME2000,CCSDSTimeSystem::UTC,epoch,stop_epoch,).with_interpolation("LAGRANGE".to_string(),Some(7));letmutseg=OEMSegment::new(metadata);// Extract states from trajectory and add to OEMforiin0..traj.len(){let(epc,s)=traj.get(i).unwrap();seg.push_state(OEMStateVector::new(epc,[s[0],s[1],s[2]],[s[3],s[4],s[5]],));}letnum_states=seg.states.len();oem.push_segment(seg);println!("\nOEM: {} segment, {} states",oem.segments.len(),num_states);// Write to KVNletkvn=oem.to_string(CCSDSFormat::KVN).unwrap();println!("KVN output: {} characters",kvn.len());// Verify by re-parsingletoem2=OEM::from_str(&kvn).unwrap();println!("Round-trip: {} states",oem2.segments[0].states.len());}