#![allow(clippy::needless_range_loop, clippy::too_many_arguments)]
use crate::core::scalar::ControlScalar;
pub fn compute_hamiltonian<S, const N: usize, const I: usize>(
x: &[S; N],
u: &[S; I],
lambda: &[S; N],
f: impl Fn(&[S; N], &[S; I]) -> [S; N],
l: impl Fn(&[S; N], &[S; I]) -> S,
) -> S
where
S: ControlScalar,
{
let running = l(x, u);
let fx = f(x, u);
let costate_inner: S = lambda
.iter()
.zip(fx.iter())
.fold(S::ZERO, |acc, (&lam_i, &fi)| acc + lam_i * fi);
running + costate_inner
}
pub fn compute_costate_derivative<S, const N: usize, const I: usize>(
x: &[S; N],
u: &[S; I],
lambda: &[S; N],
f: impl Fn(&[S; N], &[S; I]) -> [S; N] + Copy,
l: impl Fn(&[S; N], &[S; I]) -> S + Copy,
eps: S,
) -> [S; N]
where
S: ControlScalar,
{
let two_eps = eps + eps;
core::array::from_fn(|i| {
let mut x_plus = *x;
x_plus[i] += eps;
let mut x_minus = *x;
x_minus[i] -= eps;
let h_plus = compute_hamiltonian(&x_plus, u, lambda, f, l);
let h_minus = compute_hamiltonian(&x_minus, u, lambda, f, l);
-(h_plus - h_minus) / two_eps
})
}
pub fn bang_bang_control<S, const N: usize>(
u_min: S,
u_max: S,
costate: &[S; N],
b_column: &[S; N],
) -> S
where
S: ControlScalar,
{
let sigma: S = costate
.iter()
.zip(b_column.iter())
.fold(S::ZERO, |acc, (&lam_i, &bi)| acc + lam_i * bi);
if sigma > S::ZERO {
u_min
} else if sigma < S::ZERO {
u_max
} else {
S::HALF * (u_min + u_max)
}
}
pub fn shooting_gradient<S, const N: usize, const I: usize, const M: usize, const MI: usize>(
x0: &[S; N],
u_seq: &[S; MI],
dynamics: impl Fn(&[S; N], &[S; I]) -> [S; N] + Copy,
stage_cost: impl Fn(&[S; N], &[S; I]) -> S + Copy,
terminal_cost: impl Fn(&[S; N]) -> S + Copy,
dt: S,
ode_steps: usize,
eps: S,
) -> [S; MI]
where
S: ControlScalar,
{
use crate::optimal::ode_solver::{OdeSolver, RungeKutta4};
let solver = RungeKutta4::<S, N>::new();
let sub_dt = dt / S::from_f64(ode_steps as f64);
let two_eps = eps + eps;
let mut x_nodes = [[S::ZERO; N]; M];
{
let mut x = *x0;
for k in 0..M {
let u: [S; I] = core::array::from_fn(|j| u_seq[k * I + j]);
let f = |xv: &[S; N], _t: S| -> [S; N] { dynamics(xv, &u) };
for _ in 0..ode_steps {
x = solver.step(f, &x, S::ZERO, sub_dt);
}
x_nodes[k] = x;
}
}
let x_terminal = x_nodes[M - 1];
let mut lambda: [S; N] = core::array::from_fn(|i| {
let mut xp = x_terminal;
xp[i] += eps;
let mut xm = x_terminal;
xm[i] -= eps;
(terminal_cost(&xp) - terminal_cost(&xm)) / two_eps
});
let mut grad = [S::ZERO; MI];
for k in (0..M).rev() {
let x_k = if k == 0 { *x0 } else { x_nodes[k - 1] };
let u: [S; I] = core::array::from_fn(|j| u_seq[k * I + j]);
for j in 0..I {
let mut u_plus = u;
u_plus[j] += eps;
let mut u_minus = u;
u_minus[j] -= eps;
let h_plus = compute_hamiltonian(&x_k, &u_plus, &lambda, dynamics, stage_cost);
let h_minus = compute_hamiltonian(&x_k, &u_minus, &lambda, dynamics, stage_cost);
grad[k * I + j] = (h_plus - h_minus) / two_eps;
}
let dlambda = compute_costate_derivative(&x_k, &u, &lambda, dynamics, stage_cost, eps);
lambda = core::array::from_fn(|i| lambda[i] + dt * dlambda[i]);
}
grad
}
#[cfg(test)]
mod tests {
use super::*;
fn lq_dynamics(x: &[f64; 1], u: &[f64; 1]) -> [f64; 1] {
[-x[0] + u[0]]
}
fn lq_stage(x: &[f64; 1], u: &[f64; 1]) -> f64 {
x[0] * x[0] + u[0] * u[0]
}
#[test]
fn hamiltonian_lq_manual() {
let x = [1.0_f64];
let u = [0.0_f64];
let lambda = [1.0_f64];
let h = compute_hamiltonian(&x, &u, &lambda, lq_dynamics, lq_stage);
assert!((h - 0.0).abs() < 1e-12, "H={:.6}", h);
}
#[test]
fn hamiltonian_lq_with_control() {
let x = [2.0_f64];
let u = [1.0_f64];
let lambda = [3.0_f64];
let h = compute_hamiltonian(&x, &u, &lambda, lq_dynamics, lq_stage);
assert!((h - 2.0).abs() < 1e-12, "H={:.6}", h);
}
#[test]
fn costate_derivative_lq() {
let x = [1.0_f64];
let u = [0.0_f64];
let lambda = [1.0_f64];
let dlambda = compute_costate_derivative(&x, &u, &lambda, lq_dynamics, lq_stage, 1e-7);
let expected = lambda[0] - 2.0 * x[0]; assert!(
(dlambda[0] - expected).abs() < 1e-5,
"λ̇={:.8} expected={:.8}",
dlambda[0],
expected
);
}
#[test]
fn costate_derivative_matches_sign() {
let x = [3.0_f64];
let u = [1.0_f64];
let lambda = [0.0_f64];
let dlambda = compute_costate_derivative(&x, &u, &lambda, lq_dynamics, lq_stage, 1e-7);
let expected = -2.0 * x[0]; assert!(
(dlambda[0] - expected).abs() < 1e-5,
"λ̇={:.8} expected={:.8}",
dlambda[0],
expected
);
}
#[test]
fn bang_bang_selects_min_when_sigma_positive() {
let costate = [2.0_f64, 1.0];
let b = [1.0_f64, 0.0];
let u = bang_bang_control(-1.0_f64, 1.0_f64, &costate, &b);
assert!((u - (-1.0)).abs() < 1e-12, "u={:.4}", u);
}
#[test]
fn bang_bang_selects_max_when_sigma_negative() {
let costate = [-3.0_f64, 0.0];
let b = [1.0_f64, 0.0];
let u = bang_bang_control(-1.0_f64, 1.0_f64, &costate, &b);
assert!((u - 1.0).abs() < 1e-12, "u={:.4}", u);
}
#[test]
fn bang_bang_singular_arc_is_midpoint() {
let costate = [0.0_f64];
let b = [1.0_f64];
let u = bang_bang_control(-2.0_f64, 4.0_f64, &costate, &b);
assert!((u - 1.0).abs() < 1e-12, "u={:.4}", u);
}
#[test]
fn shooting_gradient_lq_direction() {
fn lq_terminal(x: &[f64; 1]) -> f64 {
x[0] * x[0]
}
let x0 = [1.0_f64];
let u_seq = [0.0_f64; 5]; let grad = shooting_gradient::<f64, 1, 1, 5, 5>(
&x0,
&u_seq,
lq_dynamics,
lq_stage,
lq_terminal,
0.1,
2,
1e-6,
);
let grad_norm: f64 = grad.iter().map(|&g| g * g).sum::<f64>().sqrt();
assert!(grad_norm > 1e-6, "Gradient should be non-zero: {:?}", grad);
}
#[test]
fn shooting_gradient_zero_at_equilibrium() {
fn lq_terminal(x: &[f64; 1]) -> f64 {
x[0] * x[0]
}
let x0 = [0.0_f64];
let u_seq = [0.0_f64; 3]; let grad = shooting_gradient::<f64, 1, 1, 3, 3>(
&x0,
&u_seq,
lq_dynamics,
lq_stage,
lq_terminal,
0.1,
2,
1e-6,
);
for (k, &g) in grad.iter().enumerate() {
assert!(
g.abs() < 1e-6,
"grad[{}]={:.2e} should be ~0 at equilibrium",
k,
g
);
}
}
}