The NumericalOrbitPropagator can propagate additional quantities alongside the orbital state, enabling covariance propagation and sensitivity analysis. This is essential for uncertainty quantification, orbit determination, and mission analysis.
=== Variational Propagation Overview ===
Initial State:
Semi-major axis: 6878.1 km
Position std: 10.0 m
Velocity std: 10.00 mm/s
--- State Transition Matrix (STM) ---
STM shape: (6, 6)
STM determinant: 0.999999 (should be ~1 for conservative forces)
STM at t/2 available: True
--- Covariance Propagation ---
Position std (GCRF frame):
X: 254.8 m, Y: 26.1 m, Z: 41.4 m
Position std (RTN frame):
R: 8.7 m, T: 259.1 m, N: 10.0 m
--- Parameter Sensitivity ---
Sensitivity matrix shape: (6, 5)
Position sensitivity to 1% parameter uncertainty:
mass : 0.04 m
drag_area : 0.04 m
Cd : 0.04 m
srp_area : 0.00 m
Cr : 0.00 m
--- Summary ---
Total position uncertainty: 17.3 m -> 259.4 m
Uncertainty growth factor: 15.0x
Example validated successfully!
=== Variational Propagation Overview ===
Initial State:
Semi-major axis: 6878.1 km
Position std: 10.0 m
Velocity std: 10.00 mm/s
--- State Transition Matrix (STM) ---
STM shape: (6, 6)
STM determinant: 0.999999 (should be ~1 for conservative forces)
STM at t/2 available: true
--- Covariance Propagation ---
Position std (GCRF frame):
X: 12.3 m, Y: 184.2 m, Z: 184.4 m
Position std (RTN frame):
R: 10.0 m, T: 260.5 m, N: 10.0 m
--- Parameter Sensitivity ---
Sensitivity matrix shape: (6, 5)
Position sensitivity to 1% parameter uncertainty:
mass : 0.08 m
drag_area : 0.08 m
Cd : 0.08 m
srp_area : 0.00 m
Cr : 0.00 m
--- Summary ---
Total position uncertainty: 17.3 m -> 260.9 m
Uncertainty growth factor: 15.1x
Example validated successfully!
The State Transition Matrix (STM) is a foundational tool for linear uncertainty propagation. It describes how small perturbations in the initial state map to perturbations at a later time:
importnumpyasnpimportbraheasbh# Initialize EOP databh.initialize_eop()# Create initial epoch and stateepoch=bh.Epoch.from_datetime(2024,1,1,12,0,0.0,0.0,bh.TimeSystem.UTC)oe=np.array([bh.R_EARTH+500e3,0.01,45.0,15.0,30.0,45.0])state=bh.state_koe_to_eci(oe,bh.AngleFormat.DEGREES)# Method 1: Enable STM via builder patternprop_config=bh.NumericalPropagationConfig.default().with_stm().with_stm_history()# Create propagator with two-body gravityprop=bh.NumericalOrbitPropagator(epoch,state,prop_config,bh.ForceModelConfig.two_body(),None,)print("=== STM Propagation Example ===\n")# Propagate for one orbital periodorbital_period=bh.orbital_period(oe[0])prop.propagate_to(epoch+orbital_period)# Access STM at final timestm=prop.stm()print(f"Final STM shape: {stm.shape}")print(f"STM determinant: {np.linalg.det(stm):.6f}")# STM at initial time should be identitystm_initial=prop.stm_at(epoch)print("\nSTM at t=0 (should be identity):")print(f" Max off-diagonal: {np.max(np.abs(stm_initial-np.eye(6))):.2e}")# STM at intermediate timehalf_period=epoch+orbital_period/2stm_half=prop.stm_at(half_period)print("\nSTM at t=T/2:")print(f" Determinant: {np.linalg.det(stm_half):.6f}")# STM composition property: Phi(t2,t0) = Phi(t2,t1) * Phi(t1,t0)# For verification, we check that the STM is invertiblestm_inv=np.linalg.inv(stm)identity_check=stm@stm_invprint("\nSTM * STM^-1 (should be identity):")print(f" Max deviation from I: {np.max(np.abs(identity_check-np.eye(6))):.2e}")# STM structure interpretationprint("\n=== STM Structure ===")print("Upper-left 3x3: Position sensitivity to initial position")print("Upper-right 3x3: Position sensitivity to initial velocity")print("Lower-left 3x3: Velocity sensitivity to initial position")print("Lower-right 3x3: Velocity sensitivity to initial velocity")# Show magnitude of each blockpos_pos=np.linalg.norm(stm[:3,:3])pos_vel=np.linalg.norm(stm[:3,3:])vel_pos=np.linalg.norm(stm[3:,:3])vel_vel=np.linalg.norm(stm[3:,3:])print("\nBlock Frobenius norms after one orbit:")print(f" dr/dr0: {pos_pos:.2f}")print(f" dr/dv0: {pos_vel:.2f}")print(f" dv/dr0: {vel_pos:.6f}")print(f" dv/dv0: {vel_vel:.2f}")# Validateassertstm.shape==(6,6)assertnp.abs(np.linalg.det(stm)-1.0)<1e-6# Hamiltonian system preserves volumeassertstm_initialisnotNoneassertstm_halfisnotNoneprint("\nExample validated successfully!")
usebraheasbh;usebh::traits::DStatePropagator;usenalgebraasna;usestd::f64::consts::PI;fnmain(){// Initialize EOP databh::initialize_eop().unwrap();// Create initial epoch and stateletepoch=bh::Epoch::from_datetime(2024,1,1,12,0,0.0,0.0,bh::TimeSystem::UTC);letoe=na::SVector::<f64,6>::new(bh::R_EARTH+500e3,0.01,45.0,0.0,0.0,0.0);letstate=bh::state_koe_to_eci(oe,bh::AngleFormat::Degrees);// Enable STM via configletmutprop_config=bh::NumericalPropagationConfig::default();prop_config.variational.enable_stm=true;prop_config.variational.store_stm_history=true;// Create propagator with two-body gravityletmutprop=bh::DNumericalOrbitPropagator::new(epoch,na::DVector::from_column_slice(state.as_slice()),prop_config,bh::ForceModelConfig::two_body_gravity(),None,None,None,None,).unwrap();println!("=== STM Propagation Example ===\n");// Propagate for one orbital periodletorbital_period=2.0*PI*(oe[0].powi(3)/bh::GM_EARTH).sqrt();prop.propagate_to(epoch+orbital_period);// Access STM at final timeletstm=prop.stm().expect("STM should be available");println!("Final STM shape: ({}, {})",stm.nrows(),stm.ncols());println!("STM determinant: {:.6}",stm.determinant());// STM at initial time should be identityletstm_initial=prop.stm_at(epoch).expect("STM at t=0 should be available");letidentity=na::DMatrix::identity(6,6);letmax_diff=(&stm_initial-&identity).abs().max();println!("\nSTM at t=0 (should be identity):");println!(" Max off-diagonal: {:.2e}",max_diff);// STM at intermediate timelethalf_period=epoch+orbital_period/2.0;letstm_half=prop.stm_at(half_period).expect("STM at t=T/2 should be available");println!("\nSTM at t=T/2:");println!(" Determinant: {:.6}",stm_half.determinant());// STM composition property verificationletstm_inv=stm.clone().try_inverse().expect("STM should be invertible");letidentity_check=stm*&stm_inv;letmax_deviation=(&identity_check-&identity).abs().max();println!("\nSTM * STM^-1 (should be identity):");println!(" Max deviation from I: {:.2e}",max_deviation);// STM structure interpretationprintln!("\n=== STM Structure ===");println!("Upper-left 3x3: Position sensitivity to initial position");println!("Upper-right 3x3: Position sensitivity to initial velocity");println!("Lower-left 3x3: Velocity sensitivity to initial position");println!("Lower-right 3x3: Velocity sensitivity to initial velocity");// Show magnitude of each blockletstm=prop.stm().unwrap();letpos_pos=stm.view((0,0),(3,3)).norm();letpos_vel=stm.view((0,3),(3,3)).norm();letvel_pos=stm.view((3,0),(3,3)).norm();letvel_vel=stm.view((3,3),(3,3)).norm();println!("\nBlock Frobenius norms after one orbit:");println!(" dr/dr0: {:.2}",pos_pos);println!(" dr/dv0: {:.2}",pos_vel);println!(" dv/dr0: {:.6}",vel_pos);println!(" dv/dv0: {:.2}",vel_vel);// Validateassert_eq!(stm.nrows(),6);assert_eq!(stm.ncols(),6);assert!((stm.determinant()-1.0).abs()<1e-6);// Hamiltonian system preserves volumeprintln!("\nExample validated successfully!");}
=== STM Propagation Example ===
Final STM shape: (6, 6)
STM determinant: 1.000000
STM at t=0 (should be identity):
Max off-diagonal: 0.00e+00
STM at t=T/2:
Determinant: 0.999999
STM * STM^-1 (should be identity):
Max deviation from I: 5.45e-12
=== STM Structure ===
Upper-left 3x3: Position sensitivity to initial position
Upper-right 3x3: Position sensitivity to initial velocity
Lower-left 3x3: Velocity sensitivity to initial position
Lower-right 3x3: Velocity sensitivity to initial velocity
Block Frobenius norms after one orbit:
dr/dr0: 19.32
dr/dv0: 17271.76
dv/dr0: 0.021459
dv/dv0: 19.34
Example validated successfully!
=== STM Propagation Example ===
Final STM shape: (6, 6)
STM determinant: 1.000000
STM at t=0 (should be identity):
Max off-diagonal: 0.00e0
STM at t=T/2:
Determinant: 0.999999
STM * STM^-1 (should be identity):
Max deviation from I: 2.38e-12
=== STM Structure ===
Upper-left 3x3: Position sensitivity to initial position
Upper-right 3x3: Position sensitivity to initial velocity
Lower-left 3x3: Velocity sensitivity to initial position
Lower-right 3x3: Velocity sensitivity to initial velocity
Block Frobenius norms after one orbit:
dr/dr0: 19.50
dr/dv0: 17375.04
dv/dr0: 0.021718
dv/dv0: 19.50
Example validated successfully!
importnumpyasnpimportbraheasbh# Initialize EOP databh.initialize_eop()# Create initial epoch and stateepoch=bh.Epoch.from_datetime(2024,1,1,12,0,0.0,0.0,bh.TimeSystem.UTC)oe=np.array([bh.R_EARTH+500e3,0.01,45.0,15.0,30.0,45.0])state=bh.state_koe_to_eci(oe,bh.AngleFormat.DEGREES)# Create propagation config with STM enabledprop_config=bh.NumericalPropagationConfig.default().with_stm()# Create propagator (two-body for clean demonstration)prop=bh.NumericalOrbitPropagator(epoch,state,prop_config,bh.ForceModelConfig.two_body(),None,)# Define initial covariance (diagonal)# Position uncertainty: 10 m in each axis# Velocity uncertainty: 0.01 m/s in each axisP0=np.diag([100.0,100.0,100.0,0.0001,0.0001,0.0001])print("Initial Covariance (diagonal, sqrt):")print(f" Position std: {np.sqrt(P0[0,0]):.1f} m")print(f" Velocity std: {np.sqrt(P0[3,3])*1000:.2f} mm/s")# Propagate for one orbital periodorbital_period=bh.orbital_period(oe[0])prop.propagate_to(epoch+orbital_period)# Get the State Transition Matrixstm=prop.stm()print(f"\nSTM shape: {stm.shape}")# Propagate covariance: P(t) = Phi @ P0 @ Phi^TP=stm@P0@stm.T# Extract position and velocity uncertaintiespos_cov=P[:3,:3]vel_cov=P[3:,3:]print("\nPropagated Covariance after one orbit:")print(f" Position std (x,y,z): ({np.sqrt(pos_cov[0,0]):.1f}, {np.sqrt(pos_cov[1,1]):.1f}, {np.sqrt(pos_cov[2,2]):.1f}) m")print(f" Velocity std (x,y,z): ({np.sqrt(vel_cov[0,0])*1000:.2f}, {np.sqrt(vel_cov[1,1])*1000:.2f}, {np.sqrt(vel_cov[2,2])*1000:.2f}) mm/s")# Compute position uncertainty magnitudepos_uncertainty_initial=np.sqrt(np.trace(P0[:3,:3]))pos_uncertainty_final=np.sqrt(np.trace(pos_cov))print("\nTotal position uncertainty:")print(f" Initial: {pos_uncertainty_initial:.1f} m")print(f" Final: {pos_uncertainty_final:.1f} m")print(f" Growth: {pos_uncertainty_final/pos_uncertainty_initial:.1f}x")# Validate that covariance was propagatedassertstmisnotNoneassertstm.shape==(6,6)assertpos_uncertainty_final>=pos_uncertainty_initial# Uncertainty growsprint("\nExample validated successfully!")
usebraheasbh;usebh::traits::DStatePropagator;usenalgebraasna;usestd::f64::consts::PI;fnmain(){// Initialize EOP databh::initialize_eop().unwrap();// Create initial epoch and stateletepoch=bh::Epoch::from_datetime(2024,1,1,12,0,0.0,0.0,bh::TimeSystem::UTC);letoe=na::SVector::<f64,6>::new(bh::R_EARTH+500e3,0.01,45.0,0.0,0.0,0.0);letstate=bh::state_koe_to_eci(oe,bh::AngleFormat::Degrees);// Create propagation config with STM enabledletmutprop_config=bh::NumericalPropagationConfig::default();prop_config.variational.enable_stm=true;// Create propagator (two-body for clean demonstration)letmutprop=bh::DNumericalOrbitPropagator::new(epoch,na::DVector::from_column_slice(state.as_slice()),prop_config,bh::ForceModelConfig::two_body_gravity(),None,None,None,None,).unwrap();// Define initial covariance (diagonal)// Position uncertainty: 10 m in each axis (100 m² variance)// Velocity uncertainty: 0.01 m/s in each axis (0.0001 m²/s² variance)letp0:na::DMatrix<f64>=na::DMatrix::from_diagonal(&na::DVector::from_vec(vec![100.0,100.0,100.0,0.0001,0.0001,0.0001,]));println!("Initial Covariance (diagonal, sqrt):");println!(" Position std: {:.1} m",p0[(0,0)].sqrt());println!(" Velocity std: {:.2} mm/s",p0[(3,3)].sqrt()*1000.0);// Propagate for one orbital periodletorbital_period=2.0*PI*(oe[0].powi(3)/bh::GM_EARTH).sqrt();prop.propagate_to(epoch+orbital_period);// Get the State Transition Matrixletstm=prop.stm().expect("STM should be available").clone();println!("\nSTM shape: ({}, {})",stm.nrows(),stm.ncols());// Propagate covariance: P(t) = Phi @ P0 @ Phi^Tletp=&stm*&p0*stm.transpose();// Extract position and velocity uncertaintiesletpos_std_x=p[(0,0)].sqrt();letpos_std_y=p[(1,1)].sqrt();letpos_std_z=p[(2,2)].sqrt();letvel_std_x=p[(3,3)].sqrt()*1000.0;letvel_std_y=p[(4,4)].sqrt()*1000.0;letvel_std_z=p[(5,5)].sqrt()*1000.0;println!("\nPropagated Covariance after one orbit:");println!(" Position std (x,y,z): ({:.1}, {:.1}, {:.1}) m",pos_std_x,pos_std_y,pos_std_z);println!(" Velocity std (x,y,z): ({:.2}, {:.2}, {:.2}) mm/s",vel_std_x,vel_std_y,vel_std_z);// Compute position uncertainty magnitudeletpos_uncertainty_initial=(p0[(0,0)]+p0[(1,1)]+p0[(2,2)]).sqrt();letpos_uncertainty_final=(p[(0,0)]+p[(1,1)]+p[(2,2)]).sqrt();println!("\nTotal position uncertainty:");println!(" Initial: {:.1} m",pos_uncertainty_initial);println!(" Final: {:.1} m",pos_uncertainty_final);println!(" Growth: {:.1}x",pos_uncertainty_final/pos_uncertainty_initial);// Validate that covariance was propagatedassert_eq!(stm.nrows(),6);assert_eq!(stm.ncols(),6);assert!(pos_uncertainty_final>=pos_uncertainty_initial);// Uncertainty growsprintln!("\nExample validated successfully!");}
Initial Covariance (diagonal, sqrt):
Position std: 10.0 m
Velocity std: 10.00 mm/s
STM shape: (6, 6)
Propagated Covariance after one orbit:
Position std (x,y,z): (254.3, 27.0, 41.9) m
Velocity std (x,y,z): (12.96, 208.90, 199.07) mm/s
Total position uncertainty:
Initial: 17.3 m
Final: 259.2 m
Growth: 15.0x
Example validated successfully!
Initial Covariance (diagonal, sqrt):
Position std: 10.0 m
Velocity std: 10.00 mm/s
STM shape: (6, 6)
Propagated Covariance after one orbit:
Position std (x,y,z): (10.0, 184.6, 184.6) m
Velocity std (x,y,z): (291.55, 10.00, 10.00) mm/s
Total position uncertainty:
Initial: 17.3 m
Final: 261.2 m
Growth: 15.1x
Example validated successfully!
importnumpyasnpimportbraheasbh# Initialize EOP databh.initialize_eop()# Create initial epoch and stateepoch=bh.Epoch.from_datetime(2024,1,1,12,0,0.0,0.0,bh.TimeSystem.UTC)oe=np.array([bh.R_EARTH+500e3,0.01,45.0,15.0,30.0,45.0])state=bh.state_koe_to_eci(oe,bh.AngleFormat.DEGREES)# Enable STM for covariance propagationprop_config=bh.NumericalPropagationConfig.default().with_stm().with_stm_history()# Define initial covariance in ECI frame# Position uncertainty: 10 m in each axis# Velocity uncertainty: 0.01 m/s in each axisP0=np.diag([100.0,100.0,100.0,0.0001,0.0001,0.0001])# Create propagator with initial covarianceprop=bh.NumericalOrbitPropagator(epoch,state,prop_config,bh.ForceModelConfig.two_body(),None,initial_covariance=P0,)print("=== Covariance in RTN Frame ===\n")print("Initial position std (ECI): 10.0 m in each axis")# Propagate for one orbital periodorbital_period=bh.orbital_period(oe[0])prop.propagate_to(epoch+orbital_period)# Get covariance in different framestarget=epoch+orbital_periodP_gcrf=prop.covariance_gcrf(target)P_rtn=prop.covariance_rtn(target)# Extract position covariances (3x3 upper-left block)pos_cov_gcrf=P_gcrf[:3,:3]pos_cov_rtn=P_rtn[:3,:3]print("\n--- GCRF Frame Results ---")print("Position std (X, Y, Z):")print(f" X: {np.sqrt(pos_cov_gcrf[0,0]):.1f} m")print(f" Y: {np.sqrt(pos_cov_gcrf[1,1]):.1f} m")print(f" Z: {np.sqrt(pos_cov_gcrf[2,2]):.1f} m")print("\n--- RTN Frame Results ---")print("Position std (R, T, N):")print(f" Radial (R): {np.sqrt(pos_cov_rtn[0,0]):.1f} m <- Altitude uncertainty")print(f" Tangential (T): {np.sqrt(pos_cov_rtn[1,1]):.1f} m <- Along-track timing")print(f" Normal (N): {np.sqrt(pos_cov_rtn[2,2]):.1f} m <- Cross-track offset")# Physical interpretationprint("\n--- Physical Interpretation ---")print("RTN frame aligns with the orbit:")print(" R (Radial): Points from Earth center to satellite")print(" T (Tangential): Points along velocity direction")print(" N (Normal): Completes right-hand system (cross-track)")print()print("Key insight: Along-track (T) uncertainty grows fastest because")print("velocity uncertainty causes timing errors that accumulate.")print(f"After one orbit: T/R ratio = {np.sqrt(pos_cov_rtn[1,1])/np.sqrt(pos_cov_rtn[0,0]):.1f}x")# Show correlation structureprint("\n--- Position Correlation Matrix (RTN) ---")pos_std_rtn=np.sqrt(np.diag(pos_cov_rtn))corr_rtn=pos_cov_rtn/np.outer(pos_std_rtn,pos_std_rtn)print(" R T N")fori,nameinenumerate(["R","T","N"]):print(f" {name}{corr_rtn[i,0]:6.3f}{corr_rtn[i,1]:6.3f}{corr_rtn[i,2]:6.3f}")# ValidateassertP_gcrf.shape==(6,6)assertP_rtn.shape==(6,6)assertnp.sqrt(pos_cov_rtn[1,1])>np.sqrt(pos_cov_rtn[0,0])# T > Rprint("\nExample validated successfully!")
usebraheasbh;usebh::traits::{DOrbitCovarianceProvider,DStatePropagator};usenalgebraasna;usestd::f64::consts::PI;fnmain(){// Initialize EOP databh::initialize_eop().unwrap();// Create initial epoch and stateletepoch=bh::Epoch::from_datetime(2024,1,1,12,0,0.0,0.0,bh::TimeSystem::UTC);letoe=na::SVector::<f64,6>::new(bh::R_EARTH+500e3,0.01,45.0,0.0,0.0,0.0);letstate=bh::state_koe_to_eci(oe,bh::AngleFormat::Degrees);// Enable STM for covariance propagationletmutprop_config=bh::NumericalPropagationConfig::default();prop_config.variational.enable_stm=true;prop_config.variational.store_stm_history=true;// Define initial covariance in ECI frame// Position uncertainty: 10 m in each axis// Velocity uncertainty: 0.01 m/s in each axisletp0:na::DMatrix<f64>=na::DMatrix::from_diagonal(&na::DVector::from_vec(vec![100.0,100.0,100.0,0.0001,0.0001,0.0001,]));// Create propagator with initial covarianceletmutprop=bh::DNumericalOrbitPropagator::new(epoch,na::DVector::from_column_slice(state.as_slice()),prop_config,bh::ForceModelConfig::two_body_gravity(),None,None,None,Some(p0),).unwrap();println!("=== Covariance in RTN Frame ===\n");println!("Initial position std (ECI): 10.0 m in each axis");// Propagate for one orbital periodletorbital_period=2.0*PI*(oe[0].powi(3)/bh::GM_EARTH).sqrt();prop.propagate_to(epoch+orbital_period);// Get covariance in different frameslettarget=epoch+orbital_period;letp_gcrf=prop.covariance_gcrf(target).unwrap();letp_rtn=prop.covariance_rtn(target).unwrap();println!("\n--- GCRF Frame Results ---");println!("Position std (X, Y, Z):");println!(" X: {:.1} m",p_gcrf[(0,0)].sqrt());println!(" Y: {:.1} m",p_gcrf[(1,1)].sqrt());println!(" Z: {:.1} m",p_gcrf[(2,2)].sqrt());println!("\n--- RTN Frame Results ---");println!("Position std (R, T, N):");println!(" Radial (R): {:.1} m <- Altitude uncertainty",p_rtn[(0,0)].sqrt());println!(" Tangential (T): {:.1} m <- Along-track timing",p_rtn[(1,1)].sqrt());println!(" Normal (N): {:.1} m <- Cross-track offset",p_rtn[(2,2)].sqrt());// Physical interpretationprintln!("\n--- Physical Interpretation ---");println!("RTN frame aligns with the orbit:");println!(" R (Radial): Points from Earth center to satellite");println!(" T (Tangential): Points along velocity direction");println!(" N (Normal): Completes right-hand system (cross-track)");println!();println!("Key insight: Along-track (T) uncertainty grows fastest because");println!("velocity uncertainty causes timing errors that accumulate.");println!("After one orbit: T/R ratio = {:.1}x",p_rtn[(1,1)].sqrt()/p_rtn[(0,0)].sqrt());// Show correlation structureprintln!("\n--- Position Correlation Matrix (RTN) ---");letstd_r=p_rtn[(0,0)].sqrt();letstd_t=p_rtn[(1,1)].sqrt();letstd_n=p_rtn[(2,2)].sqrt();println!(" R T N");println!(" R {:6.3} {:6.3} {:6.3}",p_rtn[(0,0)]/(std_r*std_r),p_rtn[(0,1)]/(std_r*std_t),p_rtn[(0,2)]/(std_r*std_n));println!(" T {:6.3} {:6.3} {:6.3}",p_rtn[(1,0)]/(std_t*std_r),p_rtn[(1,1)]/(std_t*std_t),p_rtn[(1,2)]/(std_t*std_n));println!(" N {:6.3} {:6.3} {:6.3}",p_rtn[(2,0)]/(std_n*std_r),p_rtn[(2,1)]/(std_n*std_t),p_rtn[(2,2)]/(std_n*std_n));// Validateassert_eq!(p_gcrf.nrows(),6);assert_eq!(p_rtn.nrows(),6);assert!(p_rtn[(1,1)].sqrt()>p_rtn[(0,0)].sqrt());// T > Rprintln!("\nExample validated successfully!");}
=== Covariance in RTN Frame ===
Initial position std (ECI): 10.0 m in each axis
--- GCRF Frame Results ---
Position std (X, Y, Z):
X: 254.3 m
Y: 27.0 m
Z: 41.9 m
--- RTN Frame Results ---
Position std (R, T, N):
Radial (R): 8.7 m <- Altitude uncertainty
Tangential (T): 258.8 m <- Along-track timing
Normal (N): 10.0 m <- Cross-track offset
--- Physical Interpretation ---
RTN frame aligns with the orbit:
R (Radial): Points from Earth center to satellite
T (Tangential): Points along velocity direction
N (Normal): Completes right-hand system (cross-track)
Key insight: Along-track (T) uncertainty grows fastest because
velocity uncertainty causes timing errors that accumulate.
After one orbit: T/R ratio = 29.7x
--- Position Correlation Matrix (RTN) ---
R T N
R 1.000 -0.642 0.000
T -0.642 1.000 -0.000
N 0.000 -0.000 1.000
Example validated successfully!
=== Covariance in RTN Frame ===
Initial position std (ECI): 10.0 m in each axis
--- GCRF Frame Results ---
Position std (X, Y, Z):
X: 10.0 m
Y: 184.6 m
Z: 184.6 m
--- RTN Frame Results ---
Position std (R, T, N):
Radial (R): 10.0 m <- Altitude uncertainty
Tangential (T): 260.8 m <- Along-track timing
Normal (N): 10.0 m <- Cross-track offset
--- Physical Interpretation ---
RTN frame aligns with the orbit:
R (Radial): Points from Earth center to satellite
T (Tangential): Points along velocity direction
N (Normal): Completes right-hand system (cross-track)
Key insight: Along-track (T) uncertainty grows fastest because
velocity uncertainty causes timing errors that accumulate.
After one orbit: T/R ratio = 26.1x
--- Position Correlation Matrix (RTN) ---
R T N
R 1.000 -0.745 -0.000
T -0.745 1.000 0.000
N -0.000 0.000 1.000
Example validated successfully!
The along-track (tangential) uncertainty grows fastest because velocity uncertainty causes timing errors that accumulate over time. After one orbit, the T/R ratio is typically 20-30x.
importnumpyasnpimportplotly.graph_objectsasgo# Add plots directory to path for importing brahe_themesys.path.insert(0,str(pathlib.Path(__file__).parent.parent.parent))frombrahe_themeimportsave_themed_html,get_theme_colors# ConfigurationSCRIPT_NAME=pathlib.Path(__file__).stemOUTDIR=pathlib.Path(os.getenv("BRAHE_FIGURE_OUTPUT_DIR","./docs/figures/"))os.makedirs(OUTDIR,exist_ok=True)# Initialize EOP databh.initialize_eop()# Create initial epoch and stateepoch=bh.Epoch.from_datetime(2024,1,1,12,0,0.0,0.0,bh.TimeSystem.UTC)oe=np.array([bh.R_EARTH+500e3,0.01,45.0,15.0,30.0,45.0])state=bh.state_koe_to_eci(oe,bh.AngleFormat.DEGREES)# Create propagation config with STM enabled and history storageprop_config=bh.NumericalPropagationConfig.default().with_stm().with_stm_history()# Create propagator (two-body for clean demonstration)prop=bh.NumericalOrbitPropagator(epoch,state,prop_config,bh.ForceModelConfig.two_body(),None,)# Define initial covariance (diagonal)# Position uncertainty: 10 m in each axis (100 m² variance)# Velocity uncertainty: 0.01 m/s in each axis (0.0001 m²/s² variance)P0=np.diag([100.0,100.0,100.0,0.0001,0.0001,0.0001])# Propagate for 3 orbital periodsorbital_period=bh.orbital_period(oe[0])total_time=3*orbital_periodprop.propagate_to(epoch+total_time)# Sample covariance evolutiontimes=[]# in orbital periodspos_sigma_r=[]# Radial (x) std devpos_sigma_t=[]# Tangential (y) std devpos_sigma_n=[]# Normal (z) std devpos_total=[]# Total position std devdt=orbital_period/50# 50 samples per orbitt=0.0whilet<=total_time:current_epoch=epoch+tstm=prop.stm_at(current_epoch)ifstmisnotNone:# Propagate covariance: P(t) = STM @ P0 @ STM^TP=stm@P0@stm.T# Extract position standard deviationssigma_x=np.sqrt(P[0,0])sigma_y=np.sqrt(P[1,1])sigma_z=np.sqrt(P[2,2])times.append(t/orbital_period)# Convert to orbital periodspos_sigma_r.append(sigma_x)pos_sigma_t.append(sigma_y)pos_sigma_n.append(sigma_z)pos_total.append(np.sqrt(sigma_x**2+sigma_y**2+sigma_z**2))t+=dtdefcreate_figure(theme):colors=get_theme_colors(theme)fig=go.Figure()# Position uncertainty tracesfig.add_trace(go.Scatter(x=times,y=pos_sigma_r,mode="lines",name="X (radial-like)",line=dict(color=colors["primary"],width=2),))fig.add_trace(go.Scatter(x=times,y=pos_sigma_t,mode="lines",name="Y (along-track-like)",line=dict(color=colors["secondary"],width=2),))fig.add_trace(go.Scatter(x=times,y=pos_sigma_n,mode="lines",name="Z (cross-track-like)",line=dict(color=colors["accent"],width=2),))fig.add_trace(go.Scatter(x=times,y=pos_total,mode="lines",name="Total (RSS)",line=dict(color=colors["error"],width=2,dash="dash"),))# Initial uncertainty referenceinitial_total=np.sqrt(3*100.0)# sqrt(3 * 100 m²)fig.add_hline(y=initial_total,line_dash="dot",line_color="gray",annotation_text=f"Initial: {initial_total:.1f} m",annotation_position="top right",)fig.update_layout(title="Position Uncertainty Evolution (Two-Body)",xaxis_title="Time (orbital periods)",yaxis_title="Position Std Dev (m)",showlegend=True,legend=dict(orientation="h",yanchor="bottom",y=1.02,xanchor="right",x=1),height=500,margin=dict(l=60,r=40,t=80,b=60),)returnfig# Save themed HTML fileslight_path,dark_path=save_themed_html(create_figure,OUTDIR/SCRIPT_NAME)print(f"Generated {light_path}")print(f"Generated {dark_path}")
importbraheasbhimportnumpyasnpimportplotly.graph_objectsasgo# Add plots directory to path for importing brahe_themesys.path.insert(0,str(pathlib.Path(__file__).parent.parent.parent))frombrahe_themeimportsave_themed_html,get_theme_colors# ConfigurationSCRIPT_NAME=pathlib.Path(__file__).stemOUTDIR=pathlib.Path(os.getenv("BRAHE_FIGURE_OUTPUT_DIR","./docs/figures/"))os.makedirs(OUTDIR,exist_ok=True)# Initialize EOP databh.initialize_eop()# Create initial epoch and stateepoch=bh.Epoch.from_datetime(2024,1,1,12,0,0.0,0.0,bh.TimeSystem.UTC)oe=np.array([bh.R_EARTH+500e3,0.01,45.0,15.0,30.0,45.0])state=bh.state_koe_to_eci(oe,bh.AngleFormat.DEGREES)# Create propagation config with STM enabled and history storageprop_config=bh.NumericalPropagationConfig.default().with_stm().with_stm_history()# Define initial covariance (diagonal)# Position uncertainty: 10 m in each axis (100 m² variance)# Velocity uncertainty: 0.01 m/s in each axis (0.0001 m²/s² variance)P0=np.diag([100.0,100.0,100.0,0.0001,0.0001,0.0001])# Create propagator with initial covarianceprop=bh.NumericalOrbitPropagator(epoch,state,prop_config,bh.ForceModelConfig.two_body(),None,initial_covariance=P0,)# Propagate for 3 orbital periodsorbital_period=bh.orbital_period(oe[0])total_time=3*orbital_periodprop.propagate_to(epoch+total_time)# Sample RTN covariance evolution using STM-based propagation# This avoids numerical issues with covariance interpolationtimes=[]# in orbital periodssigma_r=[]# Radial std devsigma_t=[]# Tangential std devsigma_n=[]# Normal std devdt=orbital_period/50# 50 samples per orbitt=0.0whilet<=total_time:current_epoch=epoch+tstm=prop.stm_at(current_epoch)ifstmisnotNone:# Propagate covariance in ECI: P(t) = STM @ P0 @ STM^TP_eci=stm@P0@stm.T# Get state at current epoch to compute RTN rotationstate_t=prop.state(current_epoch)ifstate_tisnotNone:# Compute RTN rotation matrix from ECI stater=state_t[:3]v=state_t[3:]# RTN basis vectorsr_hat=r/np.linalg.norm(r)# Radialh=np.cross(r,v)# Angular momentumn_hat=h/np.linalg.norm(h)# Normal (cross-track)t_hat=np.cross(n_hat,r_hat)# Tangential (along-track)# Rotation matrix from ECI to RTN (for position)R_eci_to_rtn=np.array([r_hat,t_hat,n_hat])# Transform position covariance to RTNP_pos_eci=P_eci[:3,:3]P_pos_rtn=R_eci_to_rtn@P_pos_eci@R_eci_to_rtn.Ttimes.append(t/orbital_period)sigma_r.append(np.sqrt(P_pos_rtn[0,0]))sigma_t.append(np.sqrt(P_pos_rtn[1,1]))sigma_n.append(np.sqrt(P_pos_rtn[2,2]))t+=dtdefcreate_figure(theme):colors=get_theme_colors(theme)fig=go.Figure()# RTN uncertainty tracesfig.add_trace(go.Scatter(x=times,y=sigma_r,mode="lines",name="Radial (R)",line=dict(color=colors["primary"],width=2),))fig.add_trace(go.Scatter(x=times,y=sigma_t,mode="lines",name="Tangential (T)",line=dict(color=colors["secondary"],width=2),))fig.add_trace(go.Scatter(x=times,y=sigma_n,mode="lines",name="Normal (N)",line=dict(color=colors["accent"],width=2),))# Add annotations for physical interpretationfig.add_annotation(x=2.5,y=sigma_t[-1]*0.9,text="Along-track: unbounded growth",showarrow=False,font=dict(size=10),)fig.add_annotation(x=2.5,y=sigma_r[-1]*1.5,text="Radial/Normal: bounded oscillation",showarrow=False,font=dict(size=10),)# Initial uncertainty referenceinitial_std=10.0# mfig.add_hline(y=initial_std,line_dash="dot",line_color="gray",annotation_text=f"Initial: {initial_std:.0f} m",annotation_position="top left",)fig.update_layout(title="Position Uncertainty Evolution in RTN Frame",xaxis_title="Time (orbital periods)",yaxis_title="Position Std Dev (m)",showlegend=True,legend=dict(orientation="h",yanchor="bottom",y=1.02,xanchor="right",x=1),height=500,margin=dict(l=60,r=40,t=80,b=60),)returnfig# Save themed HTML fileslight_path,dark_path=save_themed_html(create_figure,OUTDIR/SCRIPT_NAME)print(f"Generated {light_path}")print(f"Generated {dark_path}")
In addition to STM propagation (which tracks sensitivity to initial state), the propagator can compute parameter sensitivity - how the state changes with respect to configuration parameters.
importnumpyasnpimportbraheasbh# Initialize EOP and space weather data (required for NRLMSISE-00 drag model)bh.initialize_eop()bh.initialize_sw()# Create initial epoch and stateepoch=bh.Epoch.from_datetime(2024,1,1,12,0,0.0,0.0,bh.TimeSystem.UTC)oe=np.array([bh.R_EARTH+400e3,0.01,45.0,0.0,0.0,0.0])state=bh.state_koe_to_eci(oe,bh.AngleFormat.DEGREES)# Create propagation config with sensitivity enabledprop_config=(bh.NumericalPropagationConfig.default().with_sensitivity().with_sensitivity_history())# Define spacecraft parameters: [mass, drag_area, Cd, srp_area, Cr]params=np.array([500.0,2.0,2.2,2.0,1.3])# Create propagator with full force model (needed for parameter sensitivity)prop=bh.NumericalOrbitPropagator(epoch,state,prop_config,bh.ForceModelConfig.default(),params=params,)print("Spacecraft Parameters:")print(f" Mass: {params[0]:.1f} kg")print(f" Drag area: {params[1]:.1f} m²")print(f" Drag coefficient (Cd): {params[2]:.1f}")print(f" SRP area: {params[3]:.1f} m²")print(f" SRP coefficient (Cr): {params[4]:.1f}")# Propagate for one orbital periodorbital_period=bh.orbital_period(oe[0])prop.propagate_to(epoch+orbital_period)# Get the sensitivity matrix (6 x 5)sens=prop.sensitivity()ifsensisnotNone:print(f"\nSensitivity Matrix shape: {sens.shape}")print("(Rows: state components [x,y,z,vx,vy,vz], Cols: params [mass,A_d,Cd,A_s,Cr])")# Analyze position sensitivity to each parameterpos_sens=sens[:3,:]# First 3 rowsparam_names=["mass","drag_area","Cd","srp_area","Cr"]print("\nPosition sensitivity magnitude to each parameter:")fori,nameinenumerate(param_names):# Position sensitivity magnitude for this parametermag=np.linalg.norm(pos_sens[:,i])print(f" {name:10s}: {mag:.3e} m per unit param")# Compute impact of 1% parameter uncertaintiesprint("\nPosition error from 1% parameter uncertainty:")param_uncertainties=params*0.01# 1% of each parameterfori,nameinenumerate(param_names):# dpos = sensitivity * dparampos_error=np.linalg.norm(pos_sens[:,i])*param_uncertainties[i]print(f" {name:10s}: {pos_error:.1f} m")# Total position error (RSS)total_pos_error=0.0foriinrange(len(params)):pos_error=np.linalg.norm(pos_sens[:,i])*param_uncertainties[i]total_pos_error+=pos_error**2total_pos_error=np.sqrt(total_pos_error)print(f"\n Total (RSS): {total_pos_error:.1f} m")# Validateassertsens.shape==(6,5)print("\nExample validated successfully!")else:print("\nSensitivity not available (may require full force model)")
usebraheasbh;usebh::traits::DStatePropagator;usenalgebraasna;usestd::f64::consts::PI;fnmain(){// Initialize EOP and space weather data (required for NRLMSISE-00 drag model)bh::initialize_eop().unwrap();bh::initialize_sw().unwrap();// Create initial epoch and stateletepoch=bh::Epoch::from_datetime(2024,1,1,12,0,0.0,0.0,bh::TimeSystem::UTC);letoe=na::SVector::<f64,6>::new(bh::R_EARTH+400e3,0.01,45.0,0.0,0.0,0.0);letstate=bh::state_koe_to_eci(oe,bh::AngleFormat::Degrees);// Create propagation config with sensitivity enabledletmutprop_config=bh::NumericalPropagationConfig::default();prop_config.variational.enable_sensitivity=true;prop_config.variational.store_sensitivity_history=true;// Define spacecraft parameters: [mass, drag_area, Cd, srp_area, Cr]letparams=na::DVector::from_vec(vec![500.0,2.0,2.2,2.0,1.3]);// Create propagator with full force model (needed for parameter sensitivity)letmutprop=bh::DNumericalOrbitPropagator::new(epoch,na::DVector::from_column_slice(state.as_slice()),prop_config,bh::ForceModelConfig::default(),Some(params.clone()),None,None,None,).unwrap();println!("Spacecraft Parameters:");println!(" Mass: {:.1} kg",params[0]);println!(" Drag area: {:.1} m²",params[1]);println!(" Drag coefficient (Cd): {:.1}",params[2]);println!(" SRP area: {:.1} m²",params[3]);println!(" SRP coefficient (Cr): {:.1}",params[4]);// Propagate for one orbital periodletorbital_period=2.0*PI*(oe[0].powi(3)/bh::GM_EARTH).sqrt();prop.propagate_to(epoch+orbital_period);// Get the sensitivity matrix (6 x 5)ifletSome(sens)=prop.sensitivity(){println!("\nSensitivity Matrix shape: ({}, {})",sens.nrows(),sens.ncols());println!("(Rows: state components [x,y,z,vx,vy,vz], Cols: params [mass,A_d,Cd,A_s,Cr])");letparam_names=["mass","drag_area","Cd","srp_area","Cr"];println!("\nPosition sensitivity magnitude to each parameter:");for(i,name)inparam_names.iter().enumerate(){// Position sensitivity magnitude for this parameterletpos_sens=na::Vector3::new(sens[(0,i)],sens[(1,i)],sens[(2,i)]);letmag=pos_sens.norm();println!(" {:10}: {:.3e} m per unit param",name,mag);}// Compute impact of 1% parameter uncertaintiesprintln!("\nPosition error from 1% parameter uncertainty:");letparam_uncertainties:Vec<f64>=params.iter().map(|p|p*0.01).collect();letmuttotal_pos_error_sq=0.0;for(i,name)inparam_names.iter().enumerate(){letpos_sens=na::Vector3::new(sens[(0,i)],sens[(1,i)],sens[(2,i)]);letpos_error=pos_sens.norm()*param_uncertainties[i];total_pos_error_sq+=pos_error.powi(2);println!(" {:10}: {:.1} m",name,pos_error);}lettotal_pos_error=total_pos_error_sq.sqrt();println!("\n Total (RSS): {:.1} m",total_pos_error);// Validateassert_eq!(sens.nrows(),6);assert_eq!(sens.ncols(),5);println!("\nExample validated successfully!");}else{println!("\nSensitivity not available (may require full force model)");}}
Spacecraft Parameters:
Mass: 500.0 kg
Drag area: 2.0 m²
Drag coefficient (Cd): 2.2
SRP area: 2.0 m²
SRP coefficient (Cr): 1.3
Sensitivity Matrix shape: (6, 5)
(Rows: state components [x,y,z,vx,vy,vz], Cols: params [mass,A_d,Cd,A_s,Cr])
Position sensitivity magnitude to each parameter:
mass : 9.384e-02 m per unit param
drag_area : 2.343e+01 m per unit param
Cd : 2.130e+01 m per unit param
srp_area : 6.119e-02 m per unit param
Cr : 9.414e-02 m per unit param
Position error from 1% parameter uncertainty:
mass : 0.5 m
drag_area : 0.5 m
Cd : 0.5 m
srp_area : 0.0 m
Cr : 0.0 m
Total (RSS): 0.8 m
Example validated successfully!
Spacecraft Parameters:
Mass: 500.0 kg
Drag area: 2.0 m²
Drag coefficient (Cd): 2.2
SRP area: 2.0 m²
SRP coefficient (Cr): 1.3
Sensitivity Matrix shape: (6, 5)
(Rows: state components [x,y,z,vx,vy,vz], Cols: params [mass,A_d,Cd,A_s,Cr])
Position sensitivity magnitude to each parameter:
mass : 9.384e-2 m per unit param
drag_area : 2.343e1 m per unit param
Cd : 2.130e1 m per unit param
srp_area : 6.119e-2 m per unit param
Cr : 9.414e-2 m per unit param
Position error from 1% parameter uncertainty:
mass : 0.5 m
drag_area : 0.5 m
Cd : 0.5 m
srp_area : 0.0 m
Cr : 0.0 m
Total (RSS): 0.8 m
Example validated successfully!
importnumpyasnpimportplotly.graph_objectsasgo# Add plots directory to path for importing brahe_themesys.path.insert(0,str(pathlib.Path(__file__).parent.parent.parent))frombrahe_themeimportsave_themed_html,get_theme_colors# ConfigurationSCRIPT_NAME=pathlib.Path(__file__).stemOUTDIR=pathlib.Path(os.getenv("BRAHE_FIGURE_OUTPUT_DIR","./docs/figures/"))os.makedirs(OUTDIR,exist_ok=True)# Initialize EOP and space weather data# (Space weather is required for NRLMSISE00 atmospheric model)bh.initialize_eop()bh.initialize_sw()# Create initial epoch and state (LEO orbit with significant drag)epoch=bh.Epoch.from_datetime(2024,1,1,12,0,0.0,0.0,bh.TimeSystem.UTC)oe=np.array([bh.R_EARTH+400e3,0.01,45.0,0.0,0.0,0.0])state=bh.state_koe_to_eci(oe,bh.AngleFormat.DEGREES)# Create propagation config with sensitivity enabled and history storageprop_config=(bh.NumericalPropagationConfig.default().with_sensitivity().with_sensitivity_history())# Define spacecraft parameters: [mass, drag_area, Cd, srp_area, Cr]params=np.array([500.0,2.0,2.2,2.0,1.3])# Create propagator with full force model (uses NRLMSISE00 for drag)prop=bh.NumericalOrbitPropagator(epoch,state,prop_config,bh.ForceModelConfig.default(),params=params,)# Propagate for 3 orbital periodsorbital_period=bh.orbital_period(oe[0])total_time=3*orbital_periodprop.propagate_to(epoch+total_time)# Sample sensitivity evolutionparam_names=["mass","drag_area","Cd","srp_area","Cr"]times=[]# in orbital periodssens_mag={name:[]fornameinparam_names}# Position sensitivity magnitudedt=orbital_period/50# 50 samples per orbitt=0.0whilet<=total_time:current_epoch=epoch+tsens=prop.sensitivity_at(current_epoch)ifsensisnotNone:times.append(t/orbital_period)# Convert to orbital periods# Compute position sensitivity magnitude for each parameterfori,nameinenumerate(param_names):pos_sens=np.linalg.norm(sens[:3,i])sens_mag[name].append(pos_sens)t+=dtdefcreate_figure(theme):colors=get_theme_colors(theme)fig=go.Figure()color_map={"mass":colors["primary"],"drag_area":colors["secondary"],"Cd":colors["accent"],"srp_area":colors["error"],"Cr":"gray",}# Add traces for each parameterfornameinparam_names:ifsens_mag[name]:fig.add_trace(go.Scatter(x=times,y=sens_mag[name],mode="lines",name=name,line=dict(color=color_map[name],width=2),))fig.update_layout(title="Position Sensitivity to Parameters (LEO, 400 km)",xaxis_title="Time (orbital periods)",yaxis_title="Position Sensitivity (m per unit param)",yaxis_type="log",showlegend=True,legend=dict(orientation="h",yanchor="bottom",y=1.02,xanchor="right",x=1),height=500,margin=dict(l=60,r=40,t=80,b=60),)returnfig# Save themed HTML fileslight_path,dark_path=save_themed_html(create_figure,OUTDIR/SCRIPT_NAME)print(f"Generated {light_path}")print(f"Generated {dark_path}")