Shooting Methods
Shooting methods convert an optimal control problem into a finite-dimensional optimization problem by parameterizing the control and using ODE integration to compute the resulting trajectory.
The Optimal Control Problem
Section titled “The Optimal Control Problem”Given a controlled ODE:
find the control that minimizes:
where is the terminal cost and is the running cost.
Single Shooting
Section titled “Single Shooting”Single shooting discretizes the control into piecewise-constant segments and treats the segment values as decision variables:
The trajectory is computed by forward integration, and the objective is minimized by a nonlinear optimizer.
use numra::ocp::{ShootingProblem, ShootingResult};
// Minimize terminal distance for a controlled oscillator// dy1/dt = y2, dy2/dt = u (control is acceleration)let result = ShootingProblem::<f64>::new(2, 1) // 2 states, 1 control .dynamics(|t, y, dydt, u| { dydt[0] = y[1]; dydt[1] = u[0]; }) .initial_state(&[0.0, 0.0]) .time_span(0.0, 2.0) .n_segments(20) .terminal_cost(|y_tf| { // Minimize distance to target (1.0, 0.0) let dx = y_tf[0] - 1.0; let dv = y_tf[1]; dx * dx + dv * dv }) .running_cost(|_t, _y, u| { 0.01 * u[0] * u[0] // penalize control effort }) .control_bounds(0, (-5.0, 5.0)) // bound control magnitude .solve() .unwrap();
println!("Objective: {:.6e}", result.objective);println!("Converged: {}", result.converged);println!("Iterations: {}", result.iterations);println!("Final state: [{:.4}, {:.4}]", result.final_state[0], result.final_state[1]);How It Works
Section titled “How It Works”- Parameterize: Represent as scalar values
- Forward integrate: Given a control vector, integrate the ODE from to
- Compute cost: Sum the terminal cost and discretized running cost
- Optimize: Use BFGS or L-BFGS to minimize the cost over control variables
- Iterate: Repeat until convergence
ShootingResult
Section titled “ShootingResult”The result contains the full solution:
| Field | Type | Description |
|---|---|---|
controls | Vec<S> | Optimal control values (flat) |
final_state | Vec<S> | Terminal state |
objective | S | Optimal cost |
converged | bool | Optimizer convergence |
iterations | usize | Number of optimizer iterations |
t_trajectory | Vec<S> | Time grid |
y_trajectory | Vec<S> | State trajectory (row-major) |
Multiple Shooting
Section titled “Multiple Shooting”Multiple shooting breaks the time horizon into intervals and treats both controls and intermediate states as decision variables. This improves conditioning for problems where single shooting suffers from sensitivity to control perturbations.
use numra::ocp::{MultipleShootingProblem, MultipleShootingResult};
let result = MultipleShootingProblem::<f64>::new(2, 1) .dynamics(|t, y, dydt, u| { dydt[0] = y[1]; dydt[1] = u[0] - y[0]; }) .initial_state(&[1.0, 0.0]) .time_span(0.0, 5.0) .n_segments(10) .terminal_cost(|y| y[0] * y[0] + y[1] * y[1]) .solve() .unwrap();Single vs Multiple Shooting
Section titled “Single vs Multiple Shooting”| Aspect | Single Shooting | Multiple Shooting |
|---|---|---|
| Decision variables | Controls only | Controls + states |
| NLP size | ||
| Conditioning | Poor for long horizons | Better conditioned |
| Constraints | Implicit (via ODE) | Explicit continuity |
| Implementation | Simpler | More complex |
| Parallelism | Sequential | Intervals independent |
- Start simple: Use single shooting first. Switch to multiple shooting if convergence is poor or the time horizon is long.
- Control bounds: Always provide reasonable bounds. Unbounded controls can cause the optimizer to try extreme values that break the ODE integrator.
- Number of segments: More segments = finer control resolution but larger NLP. Start with 10-20 and refine.
- ODE tolerances: The shooting method is only as accurate as its ODE
integration. Use tight tolerances (
rtol=1e-8) for the inner integration.