use super::common;
use symplex::prelude::*;
use symplex::control::StateSpace;
use symplex::matrix::Matrix;
fn assert_matrix_approx(mat: &Matrix, expected: &[f64], tol: f64) {
let (nr, nc) = mat.shape();
assert_eq!(
nr * nc,
expected.len(),
"expected {} elements, matrix has {}",
expected.len(),
nr * nc
);
for i in 0..nr {
for j in 0..nc {
let val = mat.get(i, j).simplify().eval_f64().unwrap_or_else(|e| {
panic!("evalf_f64 failed at ({i},{j}): {e}");
});
let exp = expected[i * nc + j];
assert!(
common::approx_eq(val, exp, tol),
"Mismatch at ({i},{j}): got {val}, expected {exp} (tol={tol})"
);
}
}
}
fn assert_matrix_near_zero(mat: &Matrix, tol: f64) {
let (nr, nc) = mat.shape();
for i in 0..nr {
for j in 0..nc {
let val = mat.get(i, j).simplify().eval_f64().unwrap_or_else(|e| {
panic!("evalf_f64 failed at ({i},{j}): {e}");
});
assert!(
val.abs() < tol,
"Element ({i},{j}) = {val} is not near zero (tol={tol})"
);
}
}
}
#[test]
fn cholesky_2x2() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(4), ctx.int(2)],
vec![ctx.int(2), ctx.int(3)],
])
.unwrap();
let l = a
.cholesky()
.expect("Cholesky should succeed for SPD matrix");
let l01 = l.get(0, 1).simplify().eval_f64().unwrap();
assert!(l01.abs() < 1e-10, "L[0][1] should be 0, got {l01}");
let lt = l.transpose();
let product = l.matmul(<).unwrap();
assert_matrix_approx(&product, &[4.0, 2.0, 2.0, 3.0], 1e-9);
}
#[test]
fn cholesky_3x3() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(4), ctx.int(12), ctx.int(-16)],
vec![ctx.int(12), ctx.int(37), ctx.int(-43)],
vec![ctx.int(-16), ctx.int(-43), ctx.int(98)],
])
.unwrap();
let l = a
.cholesky()
.expect("Cholesky should succeed for SPD matrix");
let lt = l.transpose();
let product = l.matmul(<).unwrap();
assert_matrix_approx(
&product,
&[4.0, 12.0, -16.0, 12.0, 37.0, -43.0, -16.0, -43.0, 98.0],
1e-9,
);
assert_matrix_approx(&l, &[2.0, 0.0, 0.0, 6.0, 1.0, 0.0, -8.0, 5.0, 3.0], 1e-9);
}
#[test]
fn cholesky_not_positive_definite() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(-1), ctx.int(0)],
vec![ctx.int(0), ctx.int(1)],
])
.unwrap();
assert!(
a.cholesky().is_err(),
"Cholesky should return Err for non-positive-definite matrix"
);
}
#[test]
fn cholesky_identity() {
let ctx = Context::new();
let eye = Matrix::identity(&ctx, 3);
let l = eye.cholesky().expect("Cholesky of identity should succeed");
assert_matrix_approx(&l, &[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], 1e-12);
}
#[test]
fn pinv_full_rank() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(1), ctx.int(2)],
vec![ctx.int(3), ctx.int(4)],
])
.unwrap();
let pinv = a.pinv().expect("pinv should succeed for full-rank matrix");
let inv = a.inv().expect("inv should succeed for invertible matrix");
let (nr, nc) = pinv.shape();
assert_eq!((nr, nc), (2, 2));
for i in 0..nr {
for j in 0..nc {
let pv = pinv.get(i, j).simplify().eval_f64().unwrap();
let iv = inv.get(i, j).simplify().eval_f64().unwrap();
assert!(
common::approx_eq(pv, iv, 1e-9),
"pinv[{i},{j}]={pv} != inv[{i},{j}]={iv}"
);
}
}
}
#[test]
fn pinv_overdetermined() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(1), ctx.int(0)],
vec![ctx.int(0), ctx.int(1)],
vec![ctx.int(1), ctx.int(1)],
])
.unwrap();
let pinv = a
.pinv()
.expect("pinv should succeed for full-column-rank matrix");
assert_eq!(pinv.shape(), (2, 3));
let pinv_a = pinv.matmul(&a).unwrap();
assert_matrix_approx(&pinv_a, &[1.0, 0.0, 0.0, 1.0], 1e-9);
}
#[test]
fn riccati_residual_setup() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(0), ctx.int(1)],
vec![ctx.int(-2), ctx.int(-3)],
])
.unwrap();
let b = Matrix::new(vec![vec![ctx.int(0)], vec![ctx.int(1)]]).unwrap();
let c = Matrix::new(vec![vec![ctx.int(1), ctx.int(0)]]).unwrap();
let d = Matrix::new(vec![vec![ctx.int(0)]]).unwrap();
let ss = StateSpace::new(a, b, c, d);
let p = Matrix::identity(&ctx, 2);
let q = Matrix::identity(&ctx, 2);
let r = Matrix::new(vec![vec![ctx.int(1)]]).unwrap();
let residual = ss
.riccati_residual(&p, &q, &r)
.expect("Riccati residual should succeed");
assert_eq!(residual.shape(), (2, 2));
assert_matrix_approx(&residual, &[1.0, -1.0, -1.0, -6.0], 1e-9);
}
#[test]
fn riccati_residual_at_solution() {
let ctx = Context::new();
let a = Matrix::new(vec![vec![ctx.int(0)]]).unwrap();
let b = Matrix::new(vec![vec![ctx.int(1)]]).unwrap();
let c = Matrix::new(vec![vec![ctx.int(1)]]).unwrap();
let d = Matrix::new(vec![vec![ctx.int(0)]]).unwrap();
let ss = StateSpace::new(a, b, c, d);
let p = Matrix::new(vec![vec![ctx.int(1)]]).unwrap();
let q = Matrix::new(vec![vec![ctx.int(1)]]).unwrap();
let r = Matrix::new(vec![vec![ctx.int(1)]]).unwrap();
let residual = ss
.riccati_residual(&p, &q, &r)
.expect("Riccati residual should succeed");
assert_matrix_near_zero(&residual, 1e-10);
}
#[test]
fn ackermann_simple() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(0), ctx.int(1)],
vec![ctx.int(0), ctx.int(0)],
])
.unwrap();
let b = Matrix::new(vec![vec![ctx.int(0)], vec![ctx.int(1)]]).unwrap();
let c = Matrix::new(vec![vec![ctx.int(1), ctx.int(0)]]).unwrap();
let d = Matrix::new(vec![vec![ctx.int(0)]]).unwrap();
let ss = StateSpace::new(a.clone(), b.clone(), c, d);
let desired_poles = vec![ctx.int(-1), ctx.int(-2)];
let k = ss
.ackermann(&desired_poles)
.expect("Ackermann should succeed for controllable SISO system");
assert_eq!(k.shape(), (1, 2));
assert_matrix_approx(&k, &[2.0, 3.0], 1e-9);
let bk = b.matmul(&k).unwrap();
let a_cl = a.sub(&bk).unwrap();
let mut eigs = a_cl.eigenvals().unwrap();
eigs.sort_by(|a, b| {
let va = a.eval_f64().unwrap_or(f64::NAN);
let vb = b.eval_f64().unwrap_or(f64::NAN);
va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
});
assert_eq!(eigs.len(), 2, "Expected 2 eigenvalues");
let e0 = eigs[0].eval_f64().unwrap();
let e1 = eigs[1].eval_f64().unwrap();
assert!(
common::approx_eq(e0, -2.0, 1e-9),
"First eigenvalue should be -2, got {e0}"
);
assert!(
common::approx_eq(e1, -1.0, 1e-9),
"Second eigenvalue should be -1, got {e1}"
);
}
#[test]
fn ackermann_not_controllable_returns_err() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(1), ctx.int(0)],
vec![ctx.int(0), ctx.int(2)],
])
.unwrap();
let b = Matrix::new(vec![vec![ctx.int(1)], vec![ctx.int(0)]]).unwrap();
let c = Matrix::new(vec![vec![ctx.int(1), ctx.int(0)]]).unwrap();
let d = Matrix::new(vec![vec![ctx.int(0)]]).unwrap();
let ss = StateSpace::new(a, b, c, d);
let desired_poles = vec![ctx.int(-1), ctx.int(-2)];
assert!(
ss.ackermann(&desired_poles).is_err(),
"Ackermann should fail for an uncontrollable system"
);
}
#[test]
fn ackermann_wrong_pole_count_returns_err() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(0), ctx.int(1)],
vec![ctx.int(0), ctx.int(0)],
])
.unwrap();
let b = Matrix::new(vec![vec![ctx.int(0)], vec![ctx.int(1)]]).unwrap();
let c = Matrix::new(vec![vec![ctx.int(1), ctx.int(0)]]).unwrap();
let d = Matrix::new(vec![vec![ctx.int(0)]]).unwrap();
let ss = StateSpace::new(a, b, c, d);
let err = ss.ackermann(&[ctx.int(-1)]).unwrap_err();
assert!(
matches!(err, SymplexError::InvalidArgument { .. }),
"one pole for a 2-state system must be rejected, got {err}"
);
}
#[test]
fn ackermann_multi_input_returns_err() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(0), ctx.int(1)],
vec![ctx.int(0), ctx.int(0)],
])
.unwrap();
let b = Matrix::new(vec![
vec![ctx.int(1), ctx.int(0)],
vec![ctx.int(0), ctx.int(1)],
])
.unwrap();
let c = Matrix::identity(&ctx, 2);
let d = Matrix::zeros(&ctx, 2, 2);
let ss = StateSpace::new(a, b, c, d);
let desired_poles = vec![ctx.int(-1), ctx.int(-2)];
assert!(
ss.ackermann(&desired_poles).is_err(),
"Ackermann should fail for a multi-input system"
);
}