The sensitivity matrix extends variational equations to include the effect of uncertain parameters on the state. While the State Transition Matrix (STM) maps initial state uncertainties to final state uncertainties, the sensitivity matrix maps parameter uncertainties to state uncertainties.
where \(\mathbf{p}\) is a vector of "consider parameters" (parameters that affect dynamics but aren't estimated), the sensitivity matrix \(\mathbf{S}\) describes how state evolves with respect to parameter changes:
Brahe provides two approaches for computing the sensitivity matrix \(\partial \mathbf{f}/\partial \mathbf{p}\)---NumericalSensitivity and AnalyticSensitivity classes. The NumericalSensitivity provider computes sensitivities automatically by perturbing parameters, while AnalyticSensitivity allows you to supply analytical derivatives for better performance. When you know the analytical form of \(\partial \mathbf{f}/\partial \mathbf{p}\), use AnalyticSensitivity for better accuracy and performance. The example files below demonstrate both numerical and analytical approaches.
Derivatives \(\partial \mathbf{f}/\partial \mathbf{p}\) are simple to derive (e.g., drag coefficient, solar radiation pressure coefficient)
Maximum accuracy is required (no finite difference errors)
Sensitivity will be computed many times (performance critical)
Working with well-understood parameter dependencies
Recommendation
For common parameters like atmospheric drag coefficient or solar radiation pressure coefficient, the analytical derivatives are often straightforward. When analytical forms are available, they provide better accuracy and performance than numerical approximations.
//! Sensitivity matrix propagation pattern example.//!//! Demonstrates propagating the sensitivity matrix alongside state to map//! parameter uncertainties to state uncertainties over time.//!//! The sensitivity matrix Φ = ∂x/∂p evolves according to://! dΦ/dt = (∂f/∂x) * Φ + (∂f/∂p)//!//! This augmented state approach propagates [state, vec(Φ)] together.usebrahe::constants::{GM_EARTH,R_EARTH};usebrahe::integrators::*;usebrahe::math::jacobian::{DJacobianProvider,DNumericalJacobian};usebrahe::math::sensitivity::{DNumericalSensitivity,DSensitivityProvider};usebrahe::{state_koe_to_eci,AngleFormat};usenalgebra::{DVector,SVector};fnmain(){// Consider parametersletcd_area_m=2.2*10.0/500.0;// Cd=2.2, A=10m^2, m=500kgletparams=DVector::from_vec(vec![cd_area_m]);letnum_params=params.len();// Dynamics function with parametersletdynamics_with_params=|_t:f64,state:&DVector<f64>,params:&DVector<f64>|->DVector<f64>{letcd_area_m=params[0];letr=state.fixed_rows::<3>(0);letv=state.fixed_rows::<3>(3);letr_norm=r.norm();// Gravitational accelerationleta_grav=-GM_EARTH/(r_norm*r_norm*r_norm)*r;// Atmospheric drag (simplified exponential model)leth=r_norm-R_EARTH;letrho0=1.225;// kg/m^3letscale_height=8500.0;// mletrho=rho0*(-h/scale_height).exp();letv_norm=v.norm();leta_drag=ifv_norm>0.0{-0.5*rho*cd_area_m*v_norm*v}else{nalgebra::Vector3::zeros()};letmutstate_dot=DVector::<f64>::zeros(6);state_dot.fixed_rows_mut::<3>(0).copy_from(&v.clone_owned());state_dot.fixed_rows_mut::<3>(3).copy_from(&(a_grav+a_drag));state_dot};// Create sensitivity providerletsensitivity_provider=DNumericalSensitivity::central(Box::new(dynamics_with_params.clone()));// Create Jacobian provider (dynamics without explicit params for Jacobian)letparams_clone=params.clone();letdynamics_for_jacobian=move|_t:f64,state:&DVector<f64>,_params:Option<&DVector<f64>>|->DVector<f64>{letcd_area_m=params_clone[0];letr=state.fixed_rows::<3>(0);letv=state.fixed_rows::<3>(3);letr_norm=r.norm();leta_grav=-GM_EARTH/(r_norm*r_norm*r_norm)*r;leth=r_norm-R_EARTH;letrho0=1.225;letscale_height=8500.0;letrho=rho0*(-h/scale_height).exp();letv_norm=v.norm();leta_drag=ifv_norm>0.0{-0.5*rho*cd_area_m*v_norm*v}else{nalgebra::Vector3::zeros()};letmutstate_dot=DVector::<f64>::zeros(6);state_dot.fixed_rows_mut::<3>(0).copy_from(&v.clone_owned());state_dot.fixed_rows_mut::<3>(3).copy_from(&(a_grav+a_drag));state_dot};letjacobian_provider=DNumericalJacobian::central(Box::new(dynamics_for_jacobian));// Augmented dynamics for state + sensitivity matrix propagationletparams_for_aug=params.clone();letaugmented_dynamics=move|t:f64,aug_state:&DVector<f64>,_params:Option<&DVector<f64>>|->DVector<f64>{// Extract state and sensitivity matrixletstate=aug_state.rows(0,6).into_owned();letphi_flat=aug_state.rows(6,6*num_params).into_owned();// Reshape phi from flat vector to matrix (column-major)letmutphi=nalgebra::DMatrix::<f64>::zeros(6,num_params);forjin0..num_params{foriin0..6{phi[(i,j)]=phi_flat[j*6+i];}}// State derivativeletstate_dot=dynamics_with_params(t,&state,¶ms_for_aug);// Compute Jacobian ∂f/∂xletjacobian=jacobian_provider.compute(t,&state,None);// Compute sensitivity ∂f/∂pletsensitivity=sensitivity_provider.compute(t,&state,¶ms_for_aug);// Sensitivity matrix derivative: dΦ/dt = J*Φ + Sletphi_dot=&jacobian*&phi+&sensitivity;// Flatten phi_dot back to vector (column-major)letmutphi_dot_flat=DVector::<f64>::zeros(6*num_params);forjin0..num_params{foriin0..6{phi_dot_flat[j*6+i]=phi_dot[(i,j)];}}// Concatenate state_dot and phi_dotletmutaug_dot=DVector::<f64>::zeros(6+6*num_params);aug_dot.rows_mut(0,6).copy_from(&state_dot);aug_dot.rows_mut(6,6*num_params).copy_from(&phi_dot_flat);aug_dot};// Initial state (200 km LEO for significant drag effects)letoe=SVector::<f64,6>::from_row_slice(&[R_EARTH+200e3,0.001,51.6,0.0,0.0,0.0]);letstate_vec=state_koe_to_eci(oe,AngleFormat::Degrees);letstate=DVector::from_iterator(6,state_vec.iter().copied());// Initial sensitivity matrix (zeros - we're interested in how it develops)letphi0=DVector::<f64>::zeros(6*num_params);// Augmented initial stateletmutaug_state=DVector::<f64>::zeros(6+6*num_params);aug_state.rows_mut(0,6).copy_from(&state);aug_state.rows_mut(6,6*num_params).copy_from(&phi0);// Create integrator for augmented system// Using fixed step RK4 for simplicity and exact parity with Pythonletaug_dim=6+6*num_params;letconfig=IntegratorConfig::fixed_step(1.0);letintegrator=RK4DIntegrator::with_config(aug_dim,Box::new(augmented_dynamics),None,None,None,config,);// Propagate for 1 hourlett_final=3600.0;letmutt=0.0;letdt=1.0;whilet<t_final{aug_state=integrator.step(t,aug_state,None,None).state;t+=dt;}// Extract final state and sensitivity matrixletfinal_state=aug_state.rows(0,6).into_owned();letfinal_phi_flat=aug_state.rows(6,6*num_params).into_owned();// Reshape to matrixletmutfinal_phi=nalgebra::DMatrix::<f64>::zeros(6,num_params);forjin0..num_params{foriin0..6{final_phi[(i,j)]=final_phi_flat[j*6+i];}}println!("Final position after {} minutes:",t_final/60.0);println!(" x: {:.3} km",final_state[0]/1000.0);println!(" y: {:.3} km",final_state[1]/1000.0);println!(" z: {:.3} km",final_state[2]/1000.0);println!("\nSensitivity matrix Φ = ∂x/∂p (position per unit Cd*A/m):");println!(" dx/dp: {:.3} m/(m²/kg)",final_phi[(0,0)]);println!(" dy/dp: {:.3} m/(m²/kg)",final_phi[(1,0)]);println!(" dz/dp: {:.3} m/(m²/kg)",final_phi[(2,0)]);println!("\nSensitivity matrix Φ = ∂x/∂p (velocity per unit Cd*A/m):");println!(" dvx/dp: {:.6} m/s/(m²/kg)",final_phi[(3,0)]);println!(" dvy/dp: {:.6} m/s/(m²/kg)",final_phi[(4,0)]);println!(" dvz/dp: {:.6} m/s/(m²/kg)",final_phi[(5,0)]);// Interpret: If we have 10% uncertainty in Cd*A/m,// the position uncertainty after 1 hour would be:letdelta_p=0.1*cd_area_m;letpos_uncertainty=(final_phi[(0,0)].powi(2)+final_phi[(1,0)].powi(2)+final_phi[(2,0)].powi(2)).sqrt()*delta_p;println!("\nPosition uncertainty for 10% parameter uncertainty: {:.1} m",pos_uncertainty);}// Example output:// Final position after 60 minutes:// x: -2884.245 km// y: -3673.659 km// z: -4635.004 km// Sensitivity matrix Φ = ∂x/∂p (position per unit Cd*A/m):// dx/dp: 59942.895 m/(m²/kg)// dy/dp: -3796.877 m/(m²/kg)// dz/dp: -4790.467 m/(m²/kg)// Sensitivity matrix Φ = ∂x/∂p (velocity per unit Cd*A/m):// dvx/dp: 44.091415 m/s/(m²/kg)// dvy/dp: 33.444232 m/s/(m²/kg)// dvz/dp: 42.196119 m/s/(m²/kg)// Position uncertainty for 10% parameter uncertainty: 265.1 m