#![allow(unused)]
use crate::core::matrix::{matmul, Matrix};
use crate::core::scalar::ControlScalar;
pub fn solve_care<S: ControlScalar, const N: usize, const I: usize>(
a: &Matrix<S, N, N>,
b: &Matrix<S, N, I>,
q: &Matrix<S, N, N>,
r: &Matrix<S, I, I>,
max_iter: usize,
) -> Option<Matrix<S, N, N>> {
let r_inv = r.inv()?;
let br_inv = matmul(b, &r_inv);
let br_invbt = matmul(&br_inv, &b.transpose());
let at = a.transpose();
let step = S::from_f64(1e-3); let tol = S::from_f64(1e-8);
let mut p = *q;
for _iter in 0..max_iter {
let atp = matmul(&at, &p);
let pa = matmul(&p, a);
let pbrbt = matmul(&p, &br_invbt);
let pbrbtp = matmul(&pbrbt, &p);
let residual = atp.add_mat(&pa).sub_mat(&pbrbtp).add_mat(q);
let norm = residual.frob_norm();
let p_new = p.add_mat(&residual.scale(step));
let p_sym = Matrix {
data: core::array::from_fn(|r_i| {
core::array::from_fn(|c_i| (p_new.data[r_i][c_i] + p_new.data[c_i][r_i]) * S::HALF)
}),
};
p = p_sym;
if norm < tol {
return Some(p);
}
}
Some(p)
}
pub struct ContinuousLqr<S: ControlScalar, const N: usize, const I: usize> {
pub gain: Matrix<S, I, N>,
pub p_matrix: Matrix<S, N, N>,
}
impl<S: ControlScalar, const N: usize, const I: usize> ContinuousLqr<S, N, I> {
pub fn new(gain: Matrix<S, I, N>, p_matrix: Matrix<S, N, N>) -> Self {
Self { gain, p_matrix }
}
pub fn design(
a: &Matrix<S, N, N>,
b: &Matrix<S, N, I>,
q: &Matrix<S, N, N>,
r: &Matrix<S, I, I>,
max_iter: usize,
) -> Option<Self> {
let p = solve_care(a, b, q, r, max_iter)?;
let r_inv = r.inv()?;
let btp = matmul(&b.transpose(), &p);
let gain = matmul(&r_inv, &btp);
Some(Self { gain, p_matrix: p })
}
pub fn control(&self, x: &Matrix<S, N, 1>) -> Matrix<S, I, 1> {
let kx = matmul(&self.gain, x);
kx.neg()
}
pub fn lyapunov_cost(&self, x: &Matrix<S, N, 1>) -> S {
let px = matmul(&self.p_matrix, x);
let xt = x.transpose();
matmul(&xt, &px).data[0][0]
}
}
pub fn discretize_zoh_approx<S: ControlScalar, const N: usize>(
a: &Matrix<S, N, N>,
dt: S,
) -> Matrix<S, N, N> {
let adt = a.scale(dt);
let adt2 = matmul(&adt, &adt);
let half = S::HALF;
let identity = Matrix::<S, N, N>::identity();
identity.add_mat(&adt).add_mat(&adt2.scale(half))
}
#[cfg(test)]
mod tests {
use super::*;
fn double_integrator_cont() -> (Matrix<f64, 2, 2>, Matrix<f64, 2, 1>) {
let mut a = Matrix::<f64, 2, 2>::zeros();
a.data[0][1] = 1.0;
let mut b = Matrix::<f64, 2, 1>::zeros();
b.data[1][0] = 1.0;
(a, b)
}
#[test]
fn care_solves_double_integrator() {
let (a, b) = double_integrator_cont();
let q = Matrix::<f64, 2, 2>::identity();
let mut r = Matrix::<f64, 1, 1>::zeros();
r.data[0][0] = 1.0;
let p = solve_care(&a, &b, &q, &r, 50_000);
assert!(p.is_some(), "CARE should converge");
let p = p.unwrap();
assert!(
p.data[0][0] > 0.0,
"P[0,0] must be positive: {}",
p.data[0][0]
);
assert!(
p.data[1][1] > 0.0,
"P[1,1] must be positive: {}",
p.data[1][1]
);
}
#[test]
fn continuous_lqr_design() {
let (a, b) = double_integrator_cont();
let q = Matrix::<f64, 2, 2>::identity();
let mut r = Matrix::<f64, 1, 1>::zeros();
r.data[0][0] = 1.0;
let lqr = ContinuousLqr::design(&a, &b, &q, &r, 50_000);
assert!(lqr.is_some(), "ContinuousLqr design should succeed");
let lqr = lqr.unwrap();
assert!(
lqr.gain.data[0][0].abs() > 0.0 || lqr.gain.data[0][1].abs() > 0.0,
"Gain should be nonzero"
);
}
#[test]
fn discretize_zoh_identity_dt_zero() {
let a = Matrix::<f64, 2, 2>::identity();
let ad = discretize_zoh_approx(&a, 0.0_f64);
assert!((ad.data[0][0] - 1.0).abs() < 1e-12);
assert!((ad.data[0][1]).abs() < 1e-12);
assert!((ad.data[1][0]).abs() < 1e-12);
assert!((ad.data[1][1] - 1.0).abs() < 1e-12);
}
#[test]
fn discretize_zoh_small_dt() {
let mut a = Matrix::<f64, 2, 2>::zeros();
a.data[0][1] = 1.0; let dt = 0.01_f64;
let ad = discretize_zoh_approx(&a, dt);
assert!((ad.data[0][0] - 1.0).abs() < 1e-10);
assert!((ad.data[0][1] - dt).abs() < 1e-10);
assert!((ad.data[1][0]).abs() < 1e-10);
assert!((ad.data[1][1] - 1.0).abs() < 1e-10);
}
#[test]
fn lyapunov_cost_positive() {
let (a, b) = double_integrator_cont();
let q = Matrix::<f64, 2, 2>::identity();
let mut r = Matrix::<f64, 1, 1>::zeros();
r.data[0][0] = 1.0;
let lqr = ContinuousLqr::design(&a, &b, &q, &r, 50_000).unwrap();
let mut x = Matrix::<f64, 2, 1>::zeros();
x.data[0][0] = 1.0;
let cost = lqr.lyapunov_cost(&x);
assert!(
cost > 0.0,
"Lyapunov cost should be positive for nonzero state: {}",
cost
);
let x0 = Matrix::<f64, 2, 1>::zeros();
let cost0 = lqr.lyapunov_cost(&x0);
assert!(
cost0.abs() < 1e-12,
"Cost should be zero at origin: {}",
cost0
);
}
}