Adaptive-Step Integrators Adaptive-step integrators automatically adjust their step size to maintain a specified error tolerance. This makes them efficient and reliable for problems where the optimal step size isn't known in advance or varies during integration. They are particularly useful for orbital mechanics, where dynamics can change rapidly due to close encounters or perturbations. In elliptical orbits, for example, smaller steps are needed near periapsis to capture rapid motion, while taking larger steps near apoapsis is acceptable and saves computation.
How Adaptive Stepping Works Adaptive methods estimate the local truncation error at each step by computing two solutions of different orders:
Higher-order solution (order \(p\) ): More accurate, used for propagation Lower-order solution (order \(p-1\) ): Less accurate, used for error estimation The error estimate is the difference between these solutions:
\[ \varepsilon \approx \|\mathbf{x}_p - \mathbf{x}_{p-1}\| \]
This is compared against a tolerance :
\[ \text{tol} = \text{abs\_tol} + \text{rel\_tol} \times \|\mathbf{x}\| \]
If \(\varepsilon < \text{tol}\) : Step accepted, state advances If \(\varepsilon \geq \text{tol}\) : Step rejected, retry with smaller \(h\) Available Adaptive Integrators RKF45: Runge-Kutta-Fehlberg 4(5) An embedded Runge-Kutta method using 5th-order solution for propagation and 4th-order solution for error estimation.
DP54: Dormand-Prince 5(4) An embedded Runge-Kutta method widely used in scientific computing (e.g., MATLAB's ode45).
RKN1210: Runge-Kutta-Nyström 12(10) A high-order method specialized for second-order differential equations, particularly well-suited to orbital mechanics.
Requirements:
State dimension must be even (position and velocity components) Best suited for problems naturally expressed as second-order systems (e.g., \(\mathbf{F} = m\mathbf{a}\) ) Basic Usage Python Rust
import brahe as bh
import numpy as np
# Define dynamics: Van der Pol oscillator (stiff for large mu)
mu = 1.0
def dynamics ( t , state ):
x , v = state
return np . array ([ v , mu * ( 1 - x ** 2 ) * v - x ])
# Initial conditions
t0 = 0.0
state0 = np . array ([ 2.0 , 0.0 ])
t_end = 10.0
# Create adaptive integrator
abs_tol = 1e-8
rel_tol = 1e-7
config = bh . IntegratorConfig . adaptive ( abs_tol = abs_tol , rel_tol = rel_tol )
integrator = bh . DP54Integrator ( 2 , dynamics , config = config )
print ( f "Adaptive integration of Van der Pol oscillator (μ= { mu } )" )
print ( f "Tolerances: abs_tol= { abs_tol } , rel_tol= { rel_tol } " )
print ( f "Integration time: 0 to { t_end } seconds \n " )
# Integrate with adaptive stepping
t = t0
state = state0 . copy ()
dt = 0.1 # Initial guess
steps = 0
min_dt = float ( "inf" )
max_dt = 0.0
print ( " Time State Step Size Error Est" )
print ( "-" * 65 )
while t < t_end :
result = integrator . step ( t , state , min ( dt , t_end - t ))
# Track step size statistics
min_dt = min ( min_dt , result . dt_used )
max_dt = max ( max_dt , result . dt_used )
# Update state
t += result . dt_used
state = result . state
dt = result . dt_next
steps += 1
# Print progress
if steps % 10 == 1 :
print (
f " { t : 7.3f } [ { state [ 0 ] : 6.3f } , { state [ 1 ] : 6.3f } ] { result . dt_used : 7.4f } { result . error_estimate : .2e } "
)
print ( " \n Integration complete!" )
print ( f "Total steps: { steps } " )
print ( f "Min step size: { min_dt : .6f } s" )
print ( f "Max step size: { max_dt : .6f } s" )
print ( f "Average step: { t_end / steps : .6f } s" )
print ( " \n Adaptive stepping automatically adjusted step size" )
print ( f "by { max_dt / min_dt : .1f } x during integration" )
use brahe :: integrators :: * ;
use nalgebra :: DVector ;
fn main () {
// Define dynamics: Van der Pol oscillator (stiff for large mu)
let mu = 1.0 ;
let dynamics = move | _t : f64 , state : & DVector < f64 > , _params : Option <& DVector < f64 >>| -> DVector < f64 > {
let x = state [ 0 ];
let v = state [ 1 ];
DVector :: from_vec ( vec! [ v , mu * ( 1.0 - x . powi ( 2 )) * v - x ])
};
// Initial conditions
let t0 = 0.0 ;
let state0 = DVector :: from_vec ( vec! [ 2.0 , 0.0 ]);
let t_end = 10.0 ;
// Create adaptive integrator
let abs_tol = 1e-8 ;
let rel_tol = 1e-7 ;
let config = IntegratorConfig :: adaptive ( abs_tol , rel_tol );
let integrator = DormandPrince54DIntegrator :: with_config ( 2 , Box :: new ( dynamics ), None , None , None , config );
println! ( "Adaptive integration of Van der Pol oscillator (μ={})" , mu );
println! ( "Tolerances: abs_tol={}, rel_tol={}" , abs_tol , rel_tol );
println! ( "Integration time: 0 to {} seconds \n " , t_end );
// Integrate with adaptive stepping
let mut t = t0 ;
let mut state = state0 . clone ();
let mut dt = 0.1_ f64 ; // Initial guess
let mut steps = 0 ;
let mut min_dt = f64 :: INFINITY ;
let mut max_dt = 0.0_ f64 ;
println! ( " Time State Step Size Error Est" );
println! ( "{}" , "-" . repeat ( 65 ));
while t < t_end {
let result = integrator . step ( t , state , None , Some ( dt . min ( t_end - t )));
// Track step size statistics
let dt_used = result . dt_used ;
min_dt = min_dt . min ( dt_used );
max_dt = max_dt . max ( dt_used );
// Update state
t += dt_used ;
state = result . state ;
dt = result . dt_next ;
steps += 1 ;
// Print progress
if steps % 10 == 1 {
println! ( "{:7.3} [{:6.3}, {:6.3}] {:7.4} {:.2e}" ,
t , state [ 0 ], state [ 1 ], dt_used , result . error_estimate . unwrap ());
}
}
println! ( " \n Integration complete!" );
println! ( "Total steps: {}" , steps );
println! ( "Min step size: {:.6} s" , min_dt );
println! ( "Max step size: {:.6} s" , max_dt );
println! ( "Average step: {:.6} s" , t_end / steps as f64 );
println! ( " \n Adaptive stepping automatically adjusted step size" );
println! ( "by {:.1}x during integration" , max_dt / min_dt );
}
Step Size Control Algorithm After computing error estimate \(\varepsilon\) , the integrator calculates a new step size:
\[h_{\text{new}} = \text{safety\_factor} \times h \times \left(\frac{\text{tol}}{\varepsilon}\right)^{1/p}\]
where: - safety_factor : Conservative multiplier (default 0.9) - \(p\) : Order of error estimate - \(h\) : Current step size
This is clamped to reasonable bounds:
\[h_{\text{new}} = \text{clip}(h_{\text{new}}, \text{min\_scale} \times h, \text{max\_scale} \times h)\]
and absolute limits:
\[h_{\text{new}} = \text{clip}(h_{\text{new}}, h_{\text{min}}, h_{\text{max}})\]
Control Parameters From IntegratorConfig:
abs_tol: Absolute error tolerance (default 1e-10) rel_tol: Relative error tolerance (default 1e-9) min_step: Minimum allowed step size (default 1e-12 s) max_step: Maximum allowed step size (default 900 s) step_safety_factor: Safety margin (default 0.9) min_step_scale_factor: Min step change ratio (default 0.2) max_step_scale_factor: Max step change ratio (default 10.0) max_step_attempts: Max tries before error (default 10) Highly Elliptical Orbit Example The following example demonstrates propagating a highly elliptical orbit (HEO) using the RKN1210 adaptive-step integrator with tight tolerances for high precision.
Python Rust
import brahe as bh
import numpy as np
# Initialize EOP data
bh . initialize_eop ()
# Define HEO orbit (Molniya-type)
a = 26554e3 # Semi-major axis (m)
e = 0.74 # Eccentricity
i = 63.4 # Inclination
# Convert to Cartesian state
oe = np . array ([ a , e , i , 0.0 , 0.0 , 0.0 ])
state0 = bh . state_koe_to_eci ( oe , bh . AngleFormat . DEGREES )
# Orbital period
period = bh . orbital_period ( a )
# Two-body dynamics
def dynamics ( t , state ):
mu = bh . GM_EARTH
r = state [ 0 : 3 ]
v = state [ 3 : 6 ]
r_norm = np . linalg . norm ( r )
a = - mu / r_norm ** 3 * r
return np . concatenate ([ v , a ])
print ( "High-Precision HEO Orbit Propagation" )
print ( f "Semi-major axis: { a / 1e3 : .1f } km" )
print ( f "Eccentricity: { e } " )
print ( f "Period: { period / 3600 : .2f } hours \n " )
# Create RKN1210 integrator with very tight tolerances
abs_tol = 1e-14
rel_tol = 1e-13
config = bh . IntegratorConfig . adaptive ( abs_tol = abs_tol , rel_tol = rel_tol )
integrator = bh . RKN1210Integrator ( 6 , dynamics , config = config )
print ( f "Using RKN1210 with tol= { abs_tol : .0e } " )
print ( "Propagating for one orbital period... \n " )
# Propagate for one orbit
t = 0.0
state = state0 . copy ()
dt = 60.0
steps = 0
total_error = 0.0
while t < period :
result = integrator . step ( t , state , min ( dt , period - t ))
t += result . dt_used
state = result . state
dt = result . dt_next
steps += 1
total_error += result . error_estimate
# Print at apogee and perigee
r_norm = np . linalg . norm ( state [ 0 : 3 ])
if steps % 10 == 1 :
print (
f "t= { t / 3600 : 6.2f } h r= { r_norm / 1e3 : 8.1f } km dt= { result . dt_used : 6.1f } s err= { result . error_estimate : .2e } "
)
print ( " \n Propagation complete!" )
print ( f "Total steps: { steps } " )
print ( f "Average step: { period / steps : .1f } s" )
print ( f "Cumulative error estimate: { total_error : .2e } " )
# Verify orbit closure (should return close to initial state)
final_oe = bh . state_eci_to_koe ( state , bh . AngleFormat . DEGREES )
initial_oe = bh . state_eci_to_koe ( state0 , bh . AngleFormat . DEGREES )
print ( " \n Orbit element errors after one period:" )
print ( f " Semi-major axis: { abs ( final_oe [ 0 ] - initial_oe [ 0 ]) : .3e } m" )
print ( f " Eccentricity: { abs ( final_oe [ 1 ] - initial_oe [ 1 ]) : .3e } " )
# Expected Output:
# High-Precision HEO Orbit Propagation
# Semi-major axis: 26554.0 km
# Eccentricity: 0.74
# Period: 11.96 hours
# Using RKN1210 with tol=1e-14
# Propagating for one orbital period...
# t= 0.02h r= 6915.2km dt= 60.0s err=1.14e-03
# t= 0.61h r= 14336.8km dt= 308.4s err=2.37e-02
# t= 2.54h r= 34813.8km dt= 900.0s err=1.52e-03
# t= 5.04h r= 45404.2km dt= 900.0s err=1.65e-03
# t= 7.54h r= 44004.3km dt= 900.0s err=0.00e+00
# t= 10.04h r= 29862.5km dt= 900.0s err=6.33e-03
# t= 11.66h r= 9739.4km dt= 291.4s err=2.59e-01
# Propagation complete!
# Total steps: 67
# Average step: 642.7 s
# Cumulative error estimate: 5.85e+00
# Orbit element errors after one period:
# Semi-major axis: 3.725e-08 m
# Eccentricity: 4.441e-16
use brahe as bh ;
use brahe ::{ constants :: * , integrators :: * };
use nalgebra :: DVector ;
fn main () {
// Initialize EOP data
bh :: initialize_eop (). unwrap ();
// Define HEO orbit (Molniya-type)
let a = 26554e3 ; // Semi-major axis (m)
let e = 0.74 ; // Eccentricity
let i = 63.4 ; // Inclination
// Convert to Cartesian state
let oe = [ a , e , i , 0.0 , 0.0 , 0.0 ];
let state0 = bh :: state_koe_to_eci ( oe . into (), bh :: AngleFormat :: Degrees );
let state0_dv = DVector :: from_vec ( state0 . as_slice (). to_vec ());
// Orbital period
let period = bh :: orbital_period ( a );
// Two-body dynamics
let dynamics = | _t : f64 , state : & DVector < f64 > , _params : Option <& DVector < f64 >>| -> DVector < f64 > {
let r = nalgebra :: Vector3 :: new ( state [ 0 ], state [ 1 ], state [ 2 ]);
let v = nalgebra :: Vector3 :: new ( state [ 3 ], state [ 4 ], state [ 5 ]);
let r_norm = r . norm ();
let a = - GM_EARTH / r_norm . powi ( 3 ) * r ;
DVector :: from_vec ( vec! [ v [ 0 ], v [ 1 ], v [ 2 ], a [ 0 ], a [ 1 ], a [ 2 ]])
};
println! ( "High-Precision HEO Orbit Propagation" );
println! ( "Semi-major axis: {:.1} km" , a / 1e3 );
println! ( "Eccentricity: {}" , e );
println! ( "Period: {:.2} hours \n " , period / 3600.0 );
// Create RKN1210 integrator with very tight tolerances
let abs_tol = 1e-14 ;
let rel_tol = 1e-13 ;
let config = IntegratorConfig :: adaptive ( abs_tol , rel_tol );
let integrator = RKN1210DIntegrator :: with_config ( 6 , Box :: new ( dynamics ), None , None , None , config );
println! ( "Using RKN1210 with tol={:.0e}" , abs_tol );
println! ( "Propagating for one orbital period... \n " );
// Propagate for one orbit
let mut t = 0.0 ;
let mut state = state0_dv . clone ();
let mut dt : f64 = 60.0 ;
let mut steps = 0 ;
let mut total_error = 0.0 ;
while t < period {
let result = integrator . step ( t , state , None , Some ( dt . min ( period - t )));
let dt_used = result . dt_used ;
t += dt_used ;
state = result . state ;
dt = result . dt_next ;
steps += 1 ;
total_error += result . error_estimate . unwrap ();
// Print at intervals
if steps % 10 == 1 {
let r = nalgebra :: Vector3 :: new ( state [ 0 ], state [ 1 ], state [ 2 ]);
let r_norm = r . norm ();
println! ( "t={:6.2}h r={:8.1}km dt={:6.1}s err={:.2e}" ,
t / 3600.0 , r_norm / 1e3 , dt_used , result . error_estimate . unwrap ());
}
}
println! ( " \n Propagation complete!" );
println! ( "Total steps: {}" , steps );
println! ( "Average step: {:.1} s" , period / steps as f64 );
println! ( "Cumulative error estimate: {:.2e}" , total_error );
// Verify orbit closure
let final_state = [ state [ 0 ], state [ 1 ], state [ 2 ], state [ 3 ], state [ 4 ], state [ 5 ]];
let final_oe = bh :: state_eci_to_koe ( final_state . into (), bh :: AngleFormat :: Degrees );
let initial_oe = bh :: state_eci_to_koe ( state0 , bh :: AngleFormat :: Degrees );
println! ( " \n Orbit element errors after one period:" );
println! ( " Semi-major axis: {:.3e} m" , ( final_oe [ 0 ] - initial_oe [ 0 ]). abs ());
println! ( " Eccentricity: {:.3e}" , ( final_oe [ 1 ] - initial_oe [ 1 ]). abs ());
}
// Expected output:
// High-Precision HEO Orbit Propagation
// Semi-major axis: 26554.0 km
// Eccentricity: 0.74
// Period: 11.96 hours
// Using RKN1210 with tol=1e-14
// Propagating for one orbital period...
// t= 0.02h r= 6915.2km dt= 60.0s err=1.14e-03
// t= 0.61h r= 14336.8km dt= 308.4s err=2.37e-02
// t= 2.54h r= 34813.8km dt= 900.0s err=1.52e-03
// t= 5.04h r= 45404.2km dt= 900.0s err=1.65e-03
// t= 7.54h r= 44004.3km dt= 900.0s err=0.00e+00
// t= 10.04h r= 29862.5km dt= 900.0s err=6.33e-03
// t= 11.66h r= 9739.4km dt= 291.4s err=2.59e-01
// Propagation complete!
// Total steps: 67
// Average step: 642.7 s
// Cumulative error estimate: 5.85e+00
// Orbit element errors after one period:
// Semi-major axis: 3.725e-08 m
// Eccentricity: 4.441e-16
See Also