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.
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!");}
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!");}
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!");}
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)");}}
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}")