Control inputs allow you to add external forcing functions to your dynamics without modifying the core dynamics function. This separation is useful for modeling thrust, drag, or other perturbations that can be toggled on and off.
\(\mathbf{f}(t, \mathbf{x})\) is the nominal dynamics (e.g., gravitational acceleration)
\(\mathbf{u}(t, \mathbf{x})\) is the control input (e.g., thrust acceleration)
The control input function \(\mathbf{u}\) takes the current time and state and returns a vector that is added to the state derivative. This additive structure makes it easy to:
Switch control on/off without changing the dynamics function
Combine different control strategies
Test different control laws with the same dynamics
defcontrol_function(t:float,state:np.ndarray,params:np.ndarray)->np.ndarray:""" Args: t: Current time state: Current state vector params: Additional parameters Returns: Control vector of same dimension as state """pass
Must be a closure or function with the signature, that either uses dynamic or static sized vectors:
The function receives: - Current time as a scalar - Current state vector - Additional parameters as a vector
And returns a control vector of the same dimension as the state, which is added to the derivative computed by the dynamics function. The additional parameters can be ignored if not needed, but the signature must be maintained.
Control inputs are passed as a separate parameter to the integrator constructor. The dynamics function computes the nominal state derivative, and the control function computes the perturbation that is added to it.
defdynamics(t,state):"""Orbital dynamics (gravity only). Args: t: Time state: [x, y, z, vx, vy, vz] """r=state[:3]v=state[3:]r_norm=np.linalg.norm(r)a_grav=-bh.GM_EARTH/(r_norm**3)*rreturnnp.concatenate([v,a_grav])defcontrol_input(t,state):"""Control input: constant low thrust in velocity direction. Args: t: Time state: [x, y, z, vx, vy, vz] Returns: Control vector added to state derivative """v=state[3:]v_norm=np.linalg.norm(v)control=np.zeros(6)ifv_norm>0:thrust_magnitude=0.001# m/s^2control[3:]=thrust_magnitude*v/v_normreturncontrol# Initial LEO state (500 km altitude, circular orbit)sma=bh.R_EARTH+500e3state_initial=np.array([sma,0.0,0.0,0.0,7612.6,0.0])# Orbital periodperiod=bh.orbital_period(sma)# Create integrator WITH control input parameterconfig=bh.IntegratorConfig.adaptive(abs_tol=1e-10,rel_tol=1e-8)integrator_thrust=bh.DP54Integrator(6,dynamics,jacobian=None,control_fn=control_input,config=config)# Create integrator without control (for comparison)integrator_coast=bh.DP54Integrator(6,dynamics,config=config)# Propagate with thrust for one orbitstate_thrust=state_initial.copy()t=0.0dt=60.0whilet<period:result=integrator_thrust.step(t,state_thrust,dt)state_thrust=result.statet+=result.dt_useddt=result.dt_next# Propagate without thrust for comparisonstate_coast=state_initial.copy()t=0.0dt=60.0whilet<period:result=integrator_coast.step(t,state_coast,dt)state_coast=result.statet+=result.dt_useddt=result.dt_next# Resultsr_initial=np.linalg.norm(state_initial[:3])r_thrust=np.linalg.norm(state_thrust[:3])r_coast=np.linalg.norm(state_coast[:3])print(f"Initial radius: {r_initial/1000:.3f} km")print(f"Orbital period: {period/3600:.2f} hours")print("\nAfter one orbit:")print(f" With thrust: {r_thrust/1000:.3f} km (delta_r = {(r_thrust-r_initial)/1000:.3f} km)")print(f" Coast only: {r_coast/1000:.3f} km (delta_r = {(r_coast-r_initial)/1000:.3f} km)")# Initial radius: 6878.136 km# Orbital period: 1.58 hours# After one orbit:# With thrust: 6889.325 km (delta_r = 11.189 km)# Coast only: 6878.136 km (delta_r = 0.000 km)
usebrahe::constants::{GM_EARTH,R_EARTH};usebrahe::integrators::*;usebrahe::orbits::keplerian::orbital_period;usenalgebra::DVector;fnmain(){// Orbital dynamics (gravity only)letdynamics=move|_t:f64,state:&DVector<f64>,_params:Option<&DVector<f64>>|->DVector<f64>{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;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);state_dot};// Control input: constant low thrust in velocity directionletcontrol_input=move|_t:f64,state:&DVector<f64>,_params:Option<&DVector<f64>>|->DVector<f64>{letv=state.fixed_rows::<3>(3);letv_norm=v.norm();letmutcontrol=DVector::<f64>::zeros(6);ifv_norm>0.0{letthrust_magnitude=0.001;// m/s^2leta_thrust=thrust_magnitude*v/v_norm;control.fixed_rows_mut::<3>(3).copy_from(&a_thrust);}control};// Create integrator with control inputletconfig=IntegratorConfig::adaptive(1e-10,1e-8);letintegrator=DormandPrince54DIntegrator::with_config(6,Box::new(dynamics),None,// No JacobianNone,// No sensitivity providerSome(Box::new(control_input)),// Control inputconfig,);// Initial LEO state (500 km altitude)letsma=R_EARTH+500e3;letstate=DVector::from_vec(vec![sma,0.0,0.0,// Position0.0,7612.6,0.0,// Velocity (circular orbit)]);// Integrate for one orbit periodletperiod=orbital_period(sma);letmutt=0.0;letmutdt=60.0;letmutcurrent_state=state.clone();println!("Initial semi-major axis: {:.3} km",sma/1000.0);println!("Integrating with continuous thrust for {:.2} hours...",period/3600.0);whilet<period{letresult=integrator.step(t,current_state,None,Some(dt));current_state=result.state;t+=result.dt_used;dt=result.dt_next;}letfinal_r=current_state.fixed_rows::<3>(0).norm();letdelta_r=final_r-sma;println!("Final radius: {:.3} km",final_r/1000.0);println!("Orbit raised by: {:.3} km",delta_r/1000.0);}// Initial radius: 6878.136 km// Orbital period: 1.58 hours// After one orbit:// With thrust: 6889.325 km (delta_r = 11.189 km)// Coast only: 6878.136 km (delta_r = 0.000 km)