Beyond Earth and the cislunar bodies, NumericalOrbitPropagator integrates about any body a CentralBody can describe. Mars has a dedicated built-in variant with a mars_default() force model; every other body is reached through CentralBody.from_naif_id (for bodies in an embedded table) or CentralBody.Custom (for anything else, including bodies with no catalogued NAIF ID). Bodies without SPICE orientation data can still be given a body-fixed frame through a user-supplied rotation callback.
CentralBody::Mars integrates in the Mars-Centered Inertial (MCI) frame, centered on the Mars body center (NAIF ID 499), and reports body-fixed states in MCMF.
CentralBody
NAIF ID
Inertial frame
Fixed frame
Mars
499
MCI
MCMF
ForceModelConfig.mars_default() pairs it with a Mars-tuned force model:
Constructor
Central body
Gravity
Drag
SRP occultation
Third bodies
mars_default()
Mars
50x50 GGM2B (ggm2bc80)
Exponential
Mars
Sun
mars_default() requires a spacecraft parameter vector [mass, drag_area, Cd, srp_area, Cr] at propagator construction; all five slots are used. Configuration validation runs automatically when the propagator is constructed; .validate() can also be called directly to check a configuration earlier.
The example below builds a Mars force model with ForceModelConfig.mars_default(), propagates a low Mars orbit for six hours, and reports the initial and final state in the Mars-fixed MCMF frame via state_in_frame.
importnumpyasnpimportbraheasbh# Initialize EOP data and the DE440s planetary ephemeris used for third-body# perturbations and ECI <-> MCI frame conversions.bh.initialize_eop()bh.load_common_spice_kernels()# Initial epochepoch=bh.Epoch.from_datetime(2024,3,1,0,0,0.0,0.0,bh.TimeSystem.UTC)# Circular low Mars orbit at 400 km altitude, expressed directly in the# Mars-Centered Inertial (MCI) frame the propagator integrates in.a=bh.R_MARS+400e3v=bh.periapsis_velocity(a,0.0,gm=bh.GM_MARS)state=np.array([a,0.0,0.0,0.0,v,0.0])# Spacecraft parameters, indexed per mars_default()'s ParameterSource# assignments: [mass, drag_area, Cd, srp_area, Cr].params=np.array([500.0,2.0,2.2,2.0,1.3])force_config=bh.ForceModelConfig.mars_default()force_config.validate()prop=bh.NumericalOrbitPropagator(epoch,state,bh.NumericalPropagationConfig.default(),force_config,params,)# Propagate for 6 hoursfinal_epoch=epoch+6.0*3600.0prop.propagate_to(final_epoch)# state_in_frame routes the propagator's native MCI state through the# reference frame router into any other supported frame. MCMF is the# Mars-fixed, IAU/WGCCRE body-fixed frame.x0_mcmf=prop.state_in_frame(bh.ReferenceFrame.MCMF,epoch)xf_mcmf=prop.state_in_frame(bh.ReferenceFrame.MCMF,final_epoch)print(f"Initial epoch: {epoch}")print(f"Final epoch: {final_epoch}")print("\nInitial state (MCMF, Mars-fixed):")print(f" Position (km): [{x0_mcmf[0]/1e3:.3f}, {x0_mcmf[1]/1e3:.3f}, {x0_mcmf[2]/1e3:.3f}]")print(f" Velocity (m/s): [{x0_mcmf[3]:.3f}, {x0_mcmf[4]:.3f}, {x0_mcmf[5]:.3f}]")print("\nFinal state (MCMF, Mars-fixed):")print(f" Position (km): [{xf_mcmf[0]/1e3:.3f}, {xf_mcmf[1]/1e3:.3f}, {xf_mcmf[2]/1e3:.3f}]")print(f" Velocity (m/s): [{xf_mcmf[3]:.3f}, {xf_mcmf[4]:.3f}, {xf_mcmf[5]:.3f}]")# Validate propagation completed and the orbit remains bound to Marsassertprop.current_epoch()==final_epochr_final=np.linalg.norm(xf_mcmf[:3])assertbh.R_MARS<r_final<bh.R_MARS+1000e3print("\nExample validated successfully!")
usebraheasbh;usebh::traits::{DOrbitStateProvider,DStatePropagator};usenalgebraasna;fnmain(){// Initialize EOP data and the DE440s planetary ephemeris used for// third-body perturbations and ECI <-> MCI frame conversions.bh::initialize_eop().unwrap();bh::load_common_spice_kernels().unwrap();// Initial epochletepoch=bh::Epoch::from_datetime(2024,3,1,0,0,0.0,0.0,bh::TimeSystem::UTC);// Circular low Mars orbit at 400 km altitude, expressed directly in the// Mars-Centered Inertial (MCI) frame the propagator integrates in.leta=bh::R_MARS+400e3;letv=bh::periapsis_velocity(a,0.0,bh::GM_MARS);letstate=na::DVector::from_vec(vec![a,0.0,0.0,0.0,v,0.0]);// Spacecraft parameters, indexed per mars_default()'s ParameterSource// assignments: [mass, drag_area, Cd, srp_area, Cr].letparams=na::DVector::from_vec(vec![500.0,2.0,2.2,2.0,1.3]);letforce_config=bh::ForceModelConfig::mars_default();force_config.validate().unwrap();letmutprop=bh::DNumericalOrbitPropagator::new(epoch,state,bh::NumericalPropagationConfig::default(),force_config,Some(params),None,// No additional dynamicsNone,// No control inputNone,// No initial covariance).unwrap();// Propagate for 6 hoursletfinal_epoch=epoch+6.0*3600.0;prop.propagate_to(final_epoch);// state_in_frame routes the propagator's native MCI state through the// reference frame router into any other supported frame. MCMF is the// Mars-fixed, IAU/WGCCRE body-fixed frame.letx0_mcmf=prop.state_in_frame(bh::ReferenceFrame::MCMF,epoch).unwrap();letxf_mcmf=prop.state_in_frame(bh::ReferenceFrame::MCMF,final_epoch).unwrap();println!("Initial epoch: {}",epoch);println!("Final epoch: {}",final_epoch);println!("\nInitial state (MCMF, Mars-fixed):");println!(" Position (km): [{:.3}, {:.3}, {:.3}]",x0_mcmf[0]/1e3,x0_mcmf[1]/1e3,x0_mcmf[2]/1e3);println!(" Velocity (m/s): [{:.3}, {:.3}, {:.3}]",x0_mcmf[3],x0_mcmf[4],x0_mcmf[5]);println!("\nFinal state (MCMF, Mars-fixed):");println!(" Position (km): [{:.3}, {:.3}, {:.3}]",xf_mcmf[0]/1e3,xf_mcmf[1]/1e3,xf_mcmf[2]/1e3);println!(" Velocity (m/s): [{:.3}, {:.3}, {:.3}]",xf_mcmf[3],xf_mcmf[4],xf_mcmf[5]);// Validate propagation completed and the orbit remains bound to Marsassert_eq!(prop.current_epoch(),final_epoch);letr_final=xf_mcmf.fixed_rows::<3>(0).norm();assert!(r_final>bh::R_MARS&&r_final<bh::R_MARS+1000e3);println!("\nExample validated successfully!");}
CentralBody.from_naif_id(naif_id) constructs one of the five built-in variants for their NAIF IDs (399 → Earth, 301 → Moon, 4 or 499 → Mars, 3 → EMB, 0 → SSB), or a pre-populated Custom body for a fixed table of other commonly used bodies: Mercury, Venus, Jupiter, Saturn, Uranus, Neptune, the Sun, Phobos, Deimos, Enceladus, and Titan. Each table entry carries the body's GM and radius, and its fixed_frame is set to BodyFixedIAU(naif_id) when the body has a compiled-in IAU/WGCCRE rotation model (true for every body in the table). A NAIF ID outside this set returns an error — use CentralBody.Custom directly for those.
For a body outside the built-in table, construct CentralBody.Custom(name, naif_id, gm, radius=None, omega=None, fixed_frame=None) with the body's physical properties. For a body without a catalogued NAIF ID (e.g. a newly observed asteroid), self-assign a unique negativenaif_id, mirroring NAIF's own convention for non-catalogued objects. The ID is used for frame identity and force-model validation; ephemeris queries against it will surface an SPK lookup error unless a kernel covering that ID is loaded.
Pair a custom body with a force model using ForceModelConfig.for_body(central_body, gravity, drag=None, srp=None, third_body=None, relativity=False, mass=None), which fills in the frame-transformation default so callers specify only the terms that vary per body. Validation runs automatically when the propagator is constructed, rejecting options incompatible with the central body (for example, spherical-harmonic gravity requires the body to have a fixed_frame); .validate() can also be called directly to check a configuration earlier.
A custom body that has no SPICE orientation kernel and no compiled-in IAU model can still be given a rotating body-fixed frame. Register a rotation callback with register_custom_frame(key, rotation, omega=None):
rotation is a callback function: it receives an Epoch and returns the 3x3 ICRF→body-fixed rotation matrix at that instant.
omega is an optional second callback function: it receives an Epoch and returns the frame's angular velocity vector (rad/s), used for the exact velocity transport term. When omitted, the angular velocity is derived numerically by central differencing of rotation.
key is an arbitrary integer handle that names the registered callback. It does not need to match (and is unrelated to) the central body's NAIF ID; the body's NAIF ID appears separately as the center of the frame variant.
Reference the registered frame as ReferenceFrame.BodyFixedCustom(center, key), using the same key, and set it as the custom body's fixed_frame. This enables support for user-defined orientation models, enabling the framework to extend to additional bodies without needing hard-coded support for them in the library. See Generic NAIF-ID Variants for the frame-router side.
The example below registers a uniform spin state for an uncatalogued body (self-assigned negative NAIF ID), converts a state between the body's inertial and body-fixed frames, and verifies that a co-rotating surface point is stationary in the body-fixed frame. No kernels are required.
importbraheasbhimportnumpyasnp# An uncatalogued body (e.g. a newly observed asteroid): self-assign a unique# negative NAIF ID for its center, mirroring NAIF's convention.CENTER=-20001# The frame key is an arbitrary integer handle. It is unrelated to NAIF IDs;# it only names the callback registered below so BodyFixedCustom can look it# up at evaluation time.KEY=1042t0=bh.Epoch.from_date(2024,3,1,bh.TimeSystem.TDB)rate=2.0e-4# spin rate (rad/s), ~8.7 h rotation period# The rotation callback is a proper function: it receives an Epoch and returns# the 3x3 ICRF -> body-fixed rotation matrix at that instant. Here the body# spins uniformly about its z-axis.defrotation(epc):theta=rate*(epc-t0)c,s=np.cos(theta),np.sin(theta)returnnp.array([[c,s,0.0],[-s,c,0.0],[0.0,0.0,1.0]])# The optional omega callback is also a function of the epoch, returning the# frame's angular velocity vector (rad/s) for the exact velocity transport# term. Omitting it falls back to numeric differentiation of `rotation`.defomega(epc):returnnp.array([0.0,0.0,rate])bh.register_custom_frame(KEY,rotation,omega)inertial=bh.ReferenceFrame.BodyCenteredICRF(CENTER)fixed=bh.ReferenceFrame.BodyFixedCustom(CENTER,KEY)# Convert an inertial state about the body into its body-fixed frame. Both# frames share the same center, so no ephemeris kernel is needed.epc=t0+600.0x_inertial=np.array([7.0e5,-2.0e5,3.0e5,10.0,25.0,-5.0])x_fixed=bh.state_frame_to_frame(inertial,fixed,epc,x_inertial)print(f"Epoch: {epc}")print(f"Inertial state: {np.array2string(x_inertial,precision=3)}")print(f"Body-fixed state: {np.array2string(x_fixed,precision=3)}")# Round trip back to the inertial frame recovers the input.x_back=bh.state_frame_to_frame(fixed,inertial,epc,x_fixed)np.testing.assert_allclose(x_back,x_inertial,atol=1e-6)# A point co-rotating with the body is stationary in the body-fixed frame.r_surf=rotation(epc).T@np.array([1.0e3,0.0,0.0])v_surf=np.cross(omega(epc),r_surf)x_surf_fixed=bh.state_frame_to_frame(inertial,fixed,epc,np.concatenate([r_surf,v_surf]))np.testing.assert_allclose(x_surf_fixed[3:],0.0,atol=1e-9)bh.unregister_custom_frame(KEY)print("\nExample validated successfully!")
usebraheasbh;usenalgebraasna;fnmain(){// An uncatalogued body (e.g. a newly observed asteroid): self-assign a// unique negative NAIF ID for its center, mirroring NAIF's convention.constCENTER:i32=-20001;// The frame key is an arbitrary integer handle. It is unrelated to NAIF// IDs; it only names the callback registered below so BodyFixedCustom can// look it up at evaluation time.constKEY:u32=1042;lett0=bh::Epoch::from_date(2024,3,1,bh::TimeSystem::TDB);letrate=2.0e-4;// spin rate (rad/s), ~8.7 h rotation period// The rotation callback is a proper function: it receives an Epoch and// returns the 3x3 ICRF -> body-fixed rotation matrix at that instant.// Here the body spins uniformly about its z-axis.letrotation=move|epc:bh::Epoch|{lettheta=rate*(epc-t0);let(s,c)=theta.sin_cos();na::Matrix3::new(c,s,0.0,-s,c,0.0,0.0,0.0,1.0)};// The optional omega callback is also a function of the epoch, returning// the frame's angular velocity vector (rad/s) for the exact velocity// transport term. Passing None falls back to numeric differentiation of// the rotation callback.letomega=move|_epc:bh::Epoch|na::Vector3::new(0.0,0.0,rate);bh::register_custom_frame(KEY,rotation,Some(Box::new(omega)));letinertial=bh::ReferenceFrame::BodyCenteredICRF(CENTER);letfixed=bh::ReferenceFrame::BodyFixedCustom{center:CENTER,key:KEY,};// Convert an inertial state about the body into its body-fixed frame.// Both frames share the same center, so no ephemeris kernel is needed.letepc=t0+600.0;letx_inertial=na::SVector::<f64,6>::new(7.0e5,-2.0e5,3.0e5,10.0,25.0,-5.0);letx_fixed=bh::state_frame_to_frame(inertial,fixed,epc,x_inertial).unwrap();println!("Epoch: {}",epc);println!("Inertial state: {:.3?}",x_inertial.as_slice());println!("Body-fixed state: {:.3?}",x_fixed.as_slice());// Round trip back to the inertial frame recovers the input.letx_back=bh::state_frame_to_frame(fixed,inertial,epc,x_fixed).unwrap();assert!((x_back-x_inertial).norm()<1e-6);// A point co-rotating with the body is stationary in the body-fixed frame.letr_surf=rotation(epc).transpose()*na::Vector3::new(1.0e3,0.0,0.0);letv_surf=omega(epc).cross(&r_surf);letx_surf=na::SVector::<f64,6>::new(r_surf[0],r_surf[1],r_surf[2],v_surf[0],v_surf[1],v_surf[2],);letx_surf_fixed=bh::state_frame_to_frame(inertial,fixed,epc,x_surf).unwrap();assert!(x_surf_fixed.fixed_rows::<3>(3).norm()<1e-9);bh::unregister_custom_frame(KEY);println!("\nExample validated successfully!");}