#![allow(unused, clippy::needless_range_loop)]
use crate::core::matrix::{matmul, Matrix};
use crate::core::scalar::ControlScalar;
#[derive(Debug)]
pub enum RobustMpcError {
NoVertices,
InfeasibleTightening,
}
impl core::fmt::Display for RobustMpcError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
RobustMpcError::NoVertices => write!(f, "Robust MPC: no uncertainty vertices provided"),
RobustMpcError::InfeasibleTightening => {
write!(
f,
"Robust MPC: constraint tightening made problem infeasible"
)
}
}
}
}
#[derive(Clone, Copy)]
pub struct UncertaintyVertex<S: ControlScalar, const N: usize, const I: usize> {
pub a: Matrix<S, N, N>,
pub b: Matrix<S, N, I>,
}
impl<S: ControlScalar, const N: usize, const I: usize> UncertaintyVertex<S, N, I> {
pub fn new(a: Matrix<S, N, N>, b: Matrix<S, N, I>) -> Self {
Self { a, b }
}
pub fn propagate(&self, x: &Matrix<S, N, 1>, u: &Matrix<S, I, 1>) -> Matrix<S, N, 1> {
let ax = matmul(&self.a, x);
let bu = matmul(&self.b, u);
ax.add_mat(&bu)
}
}
#[derive(Clone, Copy, Debug)]
pub struct RobustBoxConstraint<S: ControlScalar> {
pub x_max: S,
pub u_max: S,
}
impl<S: ControlScalar> RobustBoxConstraint<S> {
pub fn new(x_max: S, u_max: S) -> Self {
Self { x_max, u_max }
}
pub fn tighten_state(&self, delta: S) -> Result<S, RobustMpcError> {
let tightened = self.x_max - delta;
if tightened <= S::ZERO {
return Err(RobustMpcError::InfeasibleTightening);
}
Ok(tightened)
}
pub fn tighten_input(&self, delta: S) -> Result<S, RobustMpcError> {
let tightened = self.u_max - delta;
if tightened <= S::ZERO {
return Err(RobustMpcError::InfeasibleTightening);
}
Ok(tightened)
}
}
#[derive(Clone, Copy, Debug)]
pub struct TerminalSet<S: ControlScalar, const N: usize> {
pub p_f_diag: Matrix<S, N, 1>,
pub gamma: S,
}
impl<S: ControlScalar, const N: usize> TerminalSet<S, N> {
pub fn new(p_f_diag: Matrix<S, N, 1>, gamma: S) -> Self {
Self { p_f_diag, gamma }
}
pub fn contains(&self, x: &Matrix<S, N, 1>) -> bool {
let mut val = S::ZERO;
for i in 0..N {
val += self.p_f_diag.data[i][0] * x.data[i][0] * x.data[i][0];
}
val <= self.gamma
}
pub fn terminal_cost(&self, x: &Matrix<S, N, 1>) -> S {
let mut val = S::ZERO;
for i in 0..N {
val += self.p_f_diag.data[i][0] * x.data[i][0] * x.data[i][0];
}
val
}
}
pub struct RobustMpc<
S: ControlScalar,
const N: usize,
const I: usize,
const H: usize,
const V: usize,
> {
pub vertices: [UncertaintyVertex<S, N, I>; V],
pub q: Matrix<S, N, N>,
pub r: Matrix<S, I, I>,
pub terminal_set: Option<TerminalSet<S, N>>,
pub constraints: RobustBoxConstraint<S>,
pub tightening_margin: S,
pub x: Matrix<S, N, 1>,
pub iterations: usize,
pub step_size: S,
}
impl<S: ControlScalar, const N: usize, const I: usize, const H: usize, const V: usize>
RobustMpc<S, N, I, H, V>
{
pub fn new(
vertices: [UncertaintyVertex<S, N, I>; V],
q: Matrix<S, N, N>,
r: Matrix<S, I, I>,
constraints: RobustBoxConstraint<S>,
iterations: usize,
) -> Self {
Self {
vertices,
q,
r,
terminal_set: None,
constraints,
tightening_margin: S::from_f64(0.05),
x: Matrix::zeros(),
iterations,
step_size: S::from_f64(1e-3),
}
}
pub fn with_terminal_set(mut self, ts: TerminalSet<S, N>) -> Self {
self.terminal_set = Some(ts);
self
}
pub fn with_tightening_margin(mut self, margin: S) -> Self {
self.tightening_margin = margin;
self
}
fn stage_cost(&self, x: &Matrix<S, N, 1>, u: &Matrix<S, I, 1>) -> S {
let qx = matmul(&self.q, x);
let xt = x.transpose();
let cx = matmul(&xt, &qx).data[0][0];
let ru = matmul(&self.r, u);
let ut = u.transpose();
let cu = matmul(&ut, &ru).data[0][0];
cx + cu
}
fn vertex_cost(&self, v: usize, u_seq: &[Matrix<S, I, 1>; H]) -> S {
let vertex = &self.vertices[v];
let mut total = S::ZERO;
let mut x = self.x;
for k in 0..H {
total += self.stage_cost(&x, &u_seq[k]);
x = vertex.propagate(&x, &u_seq[k]);
}
if let Some(ref ts) = self.terminal_set {
total += ts.terminal_cost(&x);
} else {
total += self.stage_cost(&x, &Matrix::zeros());
}
total
}
fn worst_case_vertex(&self, u_seq: &[Matrix<S, I, 1>; H]) -> usize {
let mut worst_v = 0;
let mut worst_cost = self.vertex_cost(0, u_seq);
for v in 1..V {
let c = self.vertex_cost(v, u_seq);
if c > worst_cost {
worst_cost = c;
worst_v = v;
}
}
worst_v
}
pub fn worst_case_cost(&self, u_seq: &[Matrix<S, I, 1>; H]) -> S {
let wv = self.worst_case_vertex(u_seq);
self.vertex_cost(wv, u_seq)
}
fn subgradient_uk(&self, k: usize, u_seq: &[Matrix<S, I, 1>; H], eps: S) -> Matrix<S, I, 1> {
let two_eps = S::TWO * eps;
let mut grad = Matrix::<S, I, 1>::zeros();
for i in 0..I {
let mut u_p = *u_seq;
let mut u_m = *u_seq;
u_p[k].data[i][0] += eps;
u_m[k].data[i][0] -= eps;
let cp = self.worst_case_cost(&u_p);
let cm = self.worst_case_cost(&u_m);
grad.data[i][0] = (cp - cm) / two_eps;
}
grad
}
fn project_input(&self, u: Matrix<S, I, 1>, u_bound: S) -> Matrix<S, I, 1> {
let mut out = u;
for i in 0..I {
out.data[i][0] = out.data[i][0].clamp_val(-u_bound, u_bound);
}
out
}
pub fn solve(&mut self) -> Result<Matrix<S, I, 1>, RobustMpcError> {
let u_bound = self.constraints.tighten_input(self.tightening_margin)?;
let eps = S::from_f64(1e-5);
let step = self.step_size;
let mut u_seq: [Matrix<S, I, 1>; H] = [Matrix::zeros(); H];
for _iter in 0..self.iterations {
for k in 0..H {
let g = self.subgradient_uk(k, &u_seq, eps);
for i in 0..I {
u_seq[k].data[i][0] -= step * g.data[i][0];
}
u_seq[k] = self.project_input(u_seq[k], u_bound);
}
}
Ok(u_seq[0])
}
pub fn set_state(&mut self, x: Matrix<S, N, 1>) {
self.x = x;
}
pub fn tightened_state_bound(&self) -> Result<S, RobustMpcError> {
self.constraints.tighten_state(self.tightening_margin)
}
pub fn in_terminal_set(&self) -> bool {
match &self.terminal_set {
Some(ts) => ts.contains(&self.x),
None => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_vertex(a_scale: f64) -> UncertaintyVertex<f64, 2, 1> {
let mut a = Matrix::<f64, 2, 2>::identity();
a.data[0][1] = a_scale * 0.1;
let mut b = Matrix::<f64, 2, 1>::zeros();
b.data[0][0] = 0.005;
b.data[1][0] = 0.1;
UncertaintyVertex::new(a, b)
}
fn make_robust_mpc() -> RobustMpc<f64, 2, 1, 5, 2> {
let v1 = make_vertex(0.9);
let v2 = make_vertex(1.1);
let q = Matrix::<f64, 2, 2>::identity();
let mut r = Matrix::<f64, 1, 1>::zeros();
r.data[0][0] = 0.1;
let constraints = RobustBoxConstraint::new(10.0_f64, 1.0_f64);
RobustMpc::new([v1, v2], q, r, constraints, 30)
}
#[test]
fn robust_mpc_construction() {
let mpc = make_robust_mpc();
assert_eq!(mpc.iterations, 30);
assert!(mpc.terminal_set.is_none());
}
#[test]
fn worst_case_cost_non_negative() {
let mpc = make_robust_mpc();
let u_seq = [Matrix::<f64, 1, 1>::zeros(); 5];
let cost = mpc.worst_case_cost(&u_seq);
assert!(
cost >= 0.0,
"Worst-case cost must be non-negative: {}",
cost
);
}
#[test]
fn worst_case_cost_at_origin_is_zero() {
let mpc = make_robust_mpc();
let u_seq = [Matrix::<f64, 1, 1>::zeros(); 5];
let cost = mpc.worst_case_cost(&u_seq);
assert!(
cost < 1e-12,
"Cost at origin with zero input should be zero: {}",
cost
);
}
#[test]
fn tightened_bound_is_smaller() {
let mpc = make_robust_mpc();
let bound = mpc
.tightened_state_bound()
.expect("tightening should succeed");
assert!(
bound < 10.0,
"Tightened bound should be smaller than original: {}",
bound
);
}
#[test]
fn solve_returns_control() {
let mut mpc = make_robust_mpc();
let mut x0 = Matrix::<f64, 2, 1>::zeros();
x0.data[0][0] = 1.0;
mpc.set_state(x0);
let result = mpc.solve();
assert!(result.is_ok(), "solve should succeed: {:?}", result);
}
#[test]
fn terminal_set_contains_origin() {
let mut p_f = Matrix::<f64, 2, 1>::zeros();
p_f.data[0][0] = 1.0;
p_f.data[1][0] = 1.0;
let ts = TerminalSet::new(p_f, 1.0_f64);
let origin = Matrix::<f64, 2, 1>::zeros();
assert!(ts.contains(&origin), "Terminal set must contain origin");
}
#[test]
fn terminal_set_excludes_far_state() {
let mut p_f = Matrix::<f64, 2, 1>::zeros();
p_f.data[0][0] = 1.0;
p_f.data[1][0] = 1.0;
let ts = TerminalSet::new(p_f, 1.0_f64);
let mut far = Matrix::<f64, 2, 1>::zeros();
far.data[0][0] = 2.0; assert!(
!ts.contains(&far),
"Terminal set must not contain far state"
);
}
#[test]
fn infeasible_tightening_returns_error() {
let constraints = RobustBoxConstraint::new(0.01_f64, 0.01_f64);
let result = constraints.tighten_state(1.0_f64);
assert!(
matches!(result, Err(RobustMpcError::InfeasibleTightening)),
"Expected InfeasibleTightening error"
);
}
}