Variational Equations and State Transition Matrix¶
Variational equations enable propagating not just the statye dynamics, but also how small perturbations would affect the state over time. They help relate how changes in state at one time map to changes at a later time which is critical for orbit determination and control.
For a dynamical system \(\dot{\mathbf{x}} = \mathbf{f}(t, \mathbf{x})\), variational equations describe how small deviations from the nominal trajectory evolve. Consider two nearby initial conditions:
Nominal: \(\mathbf{x}_0\)
Perturbed: \(\mathbf{x}_0 + \delta\mathbf{x}_0\)
The difference in trajectories can be approximated by the State Transition Matrix (STM) \(\Phi(t, t_0)\):
The power of the STM is that it allows predicting many perturbed trajectories efficiently. Instead of integrating each perturbed initial condition separately, we can integrate the nominal trajectory once (with STM) and map any initial perturbation through the STM.
The following example demonstrates this equivalence:
importbraheasbhimportnumpyasnp# Initialize EOPbh.initialize_eop()# Define two-body orbital dynamicsdefdynamics(t,state):"""Two-body point-mass dynamics with Earth gravity."""mu=bh.GM_EARTHr=state[0:3]v=state[3:6]r_norm=np.linalg.norm(r)a=-mu/r_norm**3*rreturnnp.concatenate([v,a])# Create numerical Jacobian for variational equationsjacobian=bh.NumericalJacobian.central(dynamics).with_fixed_offset(0.1)# Configuration for high accuracyconfig=bh.IntegratorConfig.adaptive(abs_tol=1e-12,rel_tol=1e-10)# Create two integrators:# 1. With Jacobian - propagates STM alongside stateintegrator_nominal=bh.RKN1210Integrator(6,dynamics,jacobian=jacobian,config=config)# 2. Without Jacobian - for direct perturbation integrationintegrator_pert=bh.RKN1210Integrator(6,dynamics,config=config)# Initial state: circular orbit at 500 km altitudeoe0=np.array([bh.R_EARTH+500e3,0.0,0.0,0.0,0.0,0.0])state0=bh.state_koe_to_eci(oe0,bh.AngleFormat.DEGREES)# Apply 10-meter perturbation in X directionperturbation=np.array([10.0,0.0,0.0,0.0,0.0,0.0])# Integration parameterstotal_time=100.0# Total propagation time (seconds)num_steps=10# Number of stepsdt=total_time/num_steps# Initialize statesstate_nominal=state0.copy()phi=np.eye(6)# State Transition Matrix starts as identitystate_pert=state0+perturbationprint("STM vs Direct Perturbation Comparison")print("="*70)print(f"Initial orbit: {(oe0[0]-bh.R_EARTH)/1e3:.1f} km altitude (circular)")print(f"Perturbation: {perturbation[0]:.1f} m in X direction")print(f"Propagating for {total_time:.0f} seconds in {num_steps} steps\n")print("Theory: For small δx₀, the perturbed state should satisfy:")print(" x_pert(t) ≈ x_nominal(t) + Φ(t,t₀)·δx₀\n")print("Step Time(s) ||Error||(m) Max Component(m) Relative Error")print("-"*70)t=0.0forstepinrange(num_steps):# Propagate nominal trajectory with STMnew_state_nominal,new_phi,dt_used,_,_=integrator_nominal.step_with_varmat(t,state_nominal,phi,dt)# Propagate perturbed trajectory directlyresult_pert=integrator_pert.step(t,state_pert,dt)# Predict perturbed state using STM: x_pert ≈ x_nominal + Φ·δx₀state_pert_predicted=new_state_nominal+new_phi@perturbation# Compute error between STM prediction and direct integrationerror=result_pert.state-state_pert_predictederror_norm=np.linalg.norm(error)error_max=np.max(np.abs(error))# Relative error compared to perturbation magnituderelative_error=error_norm/np.linalg.norm(perturbation)print(f"{step+1:4d}{t+dt_used:7.1f}{error_norm:12.6f}{error_max:16.6f}{relative_error:13.6f}")# Update for next stepstate_nominal=new_state_nominalphi=new_phistate_pert=result_pert.statet+=dt_used# Example output:# STM vs Direct Perturbation Comparison# ======================================================================# Initial orbit: 500.0 km altitude (circular)# Perturbation: 10.0 m in X direction# Propagating for 100 seconds in 10 steps## Theory: For small δx₀, the perturbed state should satisfy:# x_pert(t) ≈ x_nominal(t) + Φ(t,t₀)·δx₀## Step Time(s) ||Error||(m) Max Component(m) Relative Error# ----------------------------------------------------------------------# 1 10.0 0.000078 0.000053 0.000008# 2 20.0 0.000299 0.000211 0.000030# 3 30.0 0.000627 0.000462 0.000063# 4 40.0 0.001025 0.000791 0.000103# 5 50.0 0.001463 0.001176 0.000146# 6 60.0 0.001919 0.001600 0.000192# 7 70.0 0.002378 0.002057 0.000238# 8 80.0 0.002831 0.002539 0.000283# 9 90.0 0.003271 0.003040 0.000327# 10 100.0 0.003693 0.003556 0.000369
usebrahe::*;usenalgebra::{DMatrix,DVector,SVector};/// Two-body point-mass dynamics with Earth gravity (for integrator)fndynamics(_t:f64,state:&DVector<f64>,_params:Option<&DVector<f64>>)->DVector<f64>{letr=state.rows(0,3);letv=state.rows(3,3);letr_norm=r.norm();leta=-GM_EARTH/(r_norm*r_norm*r_norm)*r;letmutstate_dot=DVector::zeros(6);state_dot.rows_mut(0,3).copy_from(&v);state_dot.rows_mut(3,3).copy_from(&a);state_dot}/// Two-body dynamics (for Jacobian computation - no params)fndynamics_for_jac(_t:f64,state:&DVector<f64>,_params:Option<&DVector<f64>>)->DVector<f64>{letr=state.rows(0,3);letv=state.rows(3,3);letr_norm=r.norm();leta=-GM_EARTH/(r_norm*r_norm*r_norm)*r;letmutstate_dot=DVector::zeros(6);state_dot.rows_mut(0,3).copy_from(&v);state_dot.rows_mut(3,3).copy_from(&a);state_dot}fnmain(){// Initialize EOPinitialize_eop().unwrap();// Create numerical Jacobian for variational equationsletjacobian=DNumericalJacobian::central(Box::new(dynamics_for_jac)).with_fixed_offset(0.1);// Configuration for high accuracyletconfig=IntegratorConfig::adaptive(1e-12,1e-10);// Create two integrators:// 1. With Jacobian - propagates STM alongside stateletintegrator_nominal=RKN1210DIntegrator::with_config(6,Box::new(dynamics),Some(Box::new(jacobian)),None,None,config.clone(),);// 2. Without Jacobian - for direct perturbation integrationletintegrator_pert=RKN1210DIntegrator::with_config(6,Box::new(dynamics),None,None,None,config,);// Initial state: circular orbit at 500 km altitudeletoe0=SVector::<f64,6>::new(R_EARTH+500e3,0.0,0.0,0.0,0.0,0.0);letstate0_static=state_koe_to_eci(oe0,AngleFormat::Degrees);letstate0=DVector::from_vec(state0_static.as_slice().to_vec());// Apply 10-meter perturbation in X directionletperturbation=DVector::from_vec(vec![10.0,0.0,0.0,0.0,0.0,0.0]);// Integration parameterslettotal_time=100.0;// Total propagation time (seconds)letnum_steps=10;// Number of stepsletdt=total_time/num_stepsasf64;// Initialize statesletmutstate_nominal=state0.clone();letmutphi=DMatrix::identity(6,6);// State Transition Matrix starts as identityletmutstate_pert=&state0+&perturbation;letmutt=0.0;println!("STM vs Direct Perturbation Comparison");println!("{}","=".repeat(70));println!("Initial orbit: {:.1} km altitude (circular)",(oe0[0]-R_EARTH)/1e3);println!("Perturbation: {:.1} m in X direction",perturbation[0]);println!("Propagating for {:.0} seconds in {} steps\n",total_time,num_steps);println!("Theory: For small δx₀, the perturbed state should satisfy:");println!(" x_pert(t) ≈ x_nominal(t) + Φ(t,t₀)·δx₀\n");println!("Step Time(s) ||Error||(m) Max Component(m) Relative Error");println!("{}","-".repeat(70));forstepin0..num_steps{// Propagate nominal trajectory with STMletresult_nominal=integrator_nominal.step_with_varmat(t,state_nominal.clone(),None,phi.clone(),Some(dt),);letnew_state_nominal=result_nominal.state;letnew_phi=result_nominal.phi.unwrap();letdt_used=result_nominal.dt_used;// Propagate perturbed trajectory directlyletresult_pert=integrator_pert.step(t,state_pert.clone(),None,Some(dt),);// Predict perturbed state using STM: x_pert ≈ x_nominal + Φ·δx₀letstate_pert_predicted=&new_state_nominal+&new_phi*&perturbation;// Compute error between STM prediction and direct integrationleterror=&result_pert.state-&state_pert_predicted;leterror_norm=error.norm();leterror_max=error.abs().max();// Relative error compared to perturbation magnitudeletrelative_error=error_norm/perturbation.norm();println!("{:4} {:7.1} {:12.6} {:16.6} {:13.6}",step+1,t+dt_used,error_norm,error_max,relative_error);// Update for next stepstate_nominal=new_state_nominal;phi=new_phi;state_pert=result_pert.state;t+=dt_used;}}// Example output:// STM vs Direct Perturbation Comparison// ======================================================================// Initial orbit: 500.0 km altitude (circular)// Perturbation: 10.0 m in X direction// Propagating for 100 seconds in 10 steps//// Theory: For small δx₀, the perturbed state should satisfy:// x_pert(t) ≈ x_nominal(t) + Φ(t,t₀)·δx₀//// Step Time(s) ||Error||(m) Max Component(m) Relative Error// ----------------------------------------------------------------------// 1 10.0 0.000078 0.000053 0.000008// 2 20.0 0.000299 0.000211 0.000030// 3 30.0 0.000627 0.000462 0.000063// 4 40.0 0.001025 0.000791 0.000103// 5 50.0 0.001463 0.001176 0.000146// 6 60.0 0.001919 0.001600 0.000192// 7 70.0 0.002378 0.002057 0.000238// 8 80.0 0.002831 0.002539 0.000283// 9 90.0 0.003271 0.003040 0.000327// 10 100.0 0.003693 0.003556 0.000369