use symplex::control::{StateSpace, TransferFunction, is_routh_stable, routh_array};
use symplex::matrix::Matrix;
use symplex::prelude::*;
#[test]
fn state_space_dimensions() {
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);
assert_eq!(ss.num_states(), 2);
assert_eq!(ss.num_inputs(), 1);
assert_eq!(ss.num_outputs(), 1);
}
#[test]
fn state_space_poles_2x2() {
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 poles = ss.poles();
assert_eq!(poles.len(), 2, "Expected 2 poles, got {}", poles.len());
let mut pole_strs: Vec<String> = poles.iter().map(|p| format!("{p}")).collect();
pole_strs.sort();
assert_eq!(
pole_strs,
vec!["-1", "-2"],
"Poles should be -1 and -2, got: {pole_strs:?}"
);
}
#[test]
fn state_space_char_poly() {
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 s = ctx.symbol("s");
let cp = ss.char_poly(&s);
let at_neg1 = cp.subs(&s, &ctx.int(-1)).simplify();
assert_eq!(
format!("{at_neg1}"),
"0",
"char_poly(-1) should be 0, got: {at_neg1}"
);
let at_neg2 = cp.subs(&s, &ctx.int(-2)).simplify();
assert_eq!(
format!("{at_neg2}"),
"0",
"char_poly(-2) should be 0, got: {at_neg2}"
);
}
#[test]
fn state_space_controllability() {
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);
assert!(ss.is_controllable(), "System should be controllable");
}
#[test]
fn state_space_not_controllable() {
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);
assert!(!ss.is_controllable(), "System should NOT be controllable");
}
#[test]
fn state_space_observability() {
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);
assert!(ss.is_observable(), "System should be observable");
}
#[test]
fn state_space_stable() {
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 stability = ss.is_stable();
assert_eq!(stability, Some(true), "System should be stable");
}
#[test]
fn state_space_unstable() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(1), ctx.int(0)],
vec![ctx.int(0), ctx.int(-1)],
])
.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 stability = ss.is_stable();
assert_eq!(stability, Some(false), "System should be unstable");
}
#[test]
fn transfer_function_poles() {
let ctx = Context::new();
let s = ctx.symbol("s");
let tf = TransferFunction::new(ctx.int(1), &s * &s + &s * 3 + 2, s.clone());
let poles = tf.poles();
assert_eq!(poles.len(), 2, "Expected 2 poles, got {}", poles.len());
let mut pole_strs: Vec<String> = poles.iter().map(|p| format!("{p}")).collect();
pole_strs.sort();
assert_eq!(
pole_strs,
vec!["-1", "-2"],
"Poles should be -1 and -2, got: {pole_strs:?}"
);
}
#[test]
fn transfer_function_zeros() {
let ctx = Context::new();
let s = ctx.symbol("s");
let tf = TransferFunction::new(&s + 1, &s * &s + &s * 3 + 2, s.clone());
let zeros = tf.zeros();
assert_eq!(zeros.len(), 1, "Expected 1 zero, got {}", zeros.len());
assert_eq!(
format!("{}", zeros[0]),
"-1",
"Zero should be at -1, got: {}",
zeros[0]
);
}
#[test]
fn transfer_function_dc_gain() {
let ctx = Context::new();
let s = ctx.symbol("s");
let tf = TransferFunction::new(ctx.int(5), &s + 2, s.clone());
let gain = tf.dc_gain();
let val = gain.eval_f64().unwrap();
assert!(
(val - 2.5).abs() < 1e-10,
"DC gain should be 5/2 = 2.5, got: {val}"
);
}
#[test]
fn transfer_function_series() {
let ctx = Context::new();
let s = ctx.symbol("s");
let g1 = TransferFunction::new(ctx.int(1), &s + 1, s.clone());
let g2 = TransferFunction::new(ctx.int(1), &s + 2, s.clone());
let gs = g1.series(&g2);
let val = gs.eval_at(&ctx.int(0)).eval_f64().unwrap();
assert!(
(val - 0.5).abs() < 1e-10,
"Series DC gain should be 0.5, got: {val}"
);
let val_1 = gs.eval_at(&ctx.int(1)).eval_f64().unwrap();
assert!(
(val_1 - 1.0 / 6.0).abs() < 1e-10,
"Series at s=1 should be 1/6, got: {val_1}"
);
}
#[test]
fn transfer_function_parallel() {
let ctx = Context::new();
let s = ctx.symbol("s");
let g1 = TransferFunction::new(ctx.int(1), &s + 1, s.clone());
let g2 = TransferFunction::new(ctx.int(1), &s + 2, s.clone());
let gp = g1.parallel(&g2);
let val = gp.eval_at(&ctx.int(0)).eval_f64().unwrap();
assert!(
(val - 1.5).abs() < 1e-10,
"Parallel DC gain should be 1.5, got: {val}"
);
}
#[test]
fn transfer_function_feedback() {
let ctx = Context::new();
let s = ctx.symbol("s");
let g = TransferFunction::new(ctx.int(10), &s + 1, s.clone());
let gcl = g.feedback();
let val = gcl.eval_at(&ctx.int(0)).eval_f64().unwrap();
let expected = 10.0 / 11.0;
assert!(
(val - expected).abs() < 1e-10,
"Feedback DC gain should be 10/11 ≈ {expected}, got: {val}"
);
}
#[test]
fn routh_array_stable() {
let ctx = Context::new();
let coeffs = vec![ctx.int(1), ctx.int(2), ctx.int(3), ctx.int(4)];
let table = routh_array(&coeffs);
assert_eq!(table.len(), 4, "Routh array should have 4 rows");
let stability = is_routh_stable(&coeffs);
assert_eq!(
stability,
Some(true),
"s^3 + 2s^2 + 3s + 4 should be Routh-stable"
);
}
#[test]
fn routh_array_unstable() {
let ctx = Context::new();
let coeffs = vec![ctx.int(1), ctx.int(2), ctx.int(1), ctx.int(8)];
let stability = is_routh_stable(&coeffs);
assert_eq!(
stability,
Some(false),
"s^3 + 2s^2 + s + 8 should be Routh-unstable"
);
}
#[test]
fn controllability_matrix_size() {
let ctx = Context::new();
let a = Matrix::new(vec![
vec![ctx.int(1), ctx.int(0), ctx.int(0)],
vec![ctx.int(0), ctx.int(2), ctx.int(0)],
vec![ctx.int(0), ctx.int(0), ctx.int(3)],
])
.unwrap();
let b = Matrix::new(vec![
vec![ctx.int(1), ctx.int(0)],
vec![ctx.int(0), ctx.int(1)],
vec![ctx.int(0), ctx.int(0)],
])
.unwrap();
let c = Matrix::new(vec![vec![ctx.int(1), ctx.int(0), ctx.int(0)]]).unwrap();
let d = Matrix::new(vec![vec![ctx.int(0), ctx.int(0)]]).unwrap();
let ss = StateSpace::new(a, b, c, d);
let cm = ss.controllability_matrix().unwrap();
assert_eq!(cm.nrows(), 3, "Controllability matrix should have 3 rows");
assert_eq!(
cm.ncols(),
6,
"Controllability matrix should have 3*2=6 cols"
);
}
#[test]
fn observability_matrix_size() {
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)],
vec![ctx.int(0), ctx.int(1)],
])
.unwrap();
let d = Matrix::new(vec![vec![ctx.int(0)], vec![ctx.int(0)]]).unwrap();
let ss = StateSpace::new(a, b, c, d);
let om = ss.observability_matrix().unwrap();
assert_eq!(om.nrows(), 4, "Observability matrix should have 2*2=4 rows");
assert_eq!(om.ncols(), 2, "Observability matrix should have 2 cols");
}
#[test]
fn transfer_function_eval_at() {
let ctx = Context::new();
let s = ctx.symbol("s");
let tf = TransferFunction::new(&s + 3, &s + 1, s.clone());
let val0 = tf.eval_at(&ctx.int(0)).eval_f64().unwrap();
assert!((val0 - 3.0).abs() < 1e-10, "G(0) should be 3, got: {val0}");
let val1 = tf.eval_at(&ctx.int(1)).eval_f64().unwrap();
assert!((val1 - 2.0).abs() < 1e-10, "G(1) should be 2, got: {val1}");
let val2 = tf.eval_at(&ctx.int(2)).eval_f64().unwrap();
assert!(
(val2 - 5.0 / 3.0).abs() < 1e-10,
"G(2) should be 5/3, got: {val2}"
);
}
#[test]
fn transfer_function_feedback_with() {
let ctx = Context::new();
let s = ctx.symbol("s");
let g = TransferFunction::new(ctx.int(10), &s + 1, s.clone());
let h = TransferFunction::new(ctx.int(2), &s + 5, s.clone());
let gcl = g.feedback_with(&h);
let val = gcl.eval_at(&ctx.int(0)).eval_f64().unwrap();
assert!(
(val - 2.0).abs() < 1e-10,
"Feedback_with DC gain should be 2.0, got: {val}"
);
}
#[test]
fn transfer_function_display() {
let ctx = Context::new();
let s = ctx.symbol("s");
let tf = TransferFunction::new(ctx.int(1), &s + 1, s.clone());
let display = format!("{tf}");
assert!(
display.contains('/'),
"Display should show fraction: {display}"
);
}
#[test]
fn routh_array_second_order_stable() {
let ctx = Context::new();
let coeffs = vec![ctx.int(1), ctx.int(3), ctx.int(2)];
let stability = is_routh_stable(&coeffs);
assert_eq!(stability, Some(true), "s^2 + 3s + 2 should be Routh-stable");
}
#[test]
fn routh_array_single_coeff() {
let ctx = Context::new();
let coeffs = vec![ctx.int(5)];
let table = routh_array(&coeffs);
assert_eq!(table.len(), 1);
let stability = is_routh_stable(&coeffs);
assert_eq!(stability, Some(true));
}
#[test]
fn state_space_char_poly_nonzero_at_non_root() {
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 s = ctx.symbol("s");
let cp = ss.char_poly(&s);
let at_zero = cp.subs(&s, &ctx.int(0)).simplify();
let val = at_zero.eval_f64().unwrap();
assert!(
(val - 2.0).abs() < 1e-10,
"char_poly(0) should be 2, got: {val}"
);
}
#[test]
fn state_space_1x1_system() {
let ctx = Context::new();
let a = Matrix::new(vec![vec![ctx.int(-2)]]).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);
assert_eq!(ss.num_states(), 1);
assert_eq!(ss.num_inputs(), 1);
assert_eq!(ss.num_outputs(), 1);
assert!(ss.is_controllable());
assert!(ss.is_observable());
let poles = ss.poles();
assert_eq!(poles.len(), 1);
assert_eq!(format!("{}", poles[0]), "-2");
}
#[test]
fn transfer_function_constant_dc_gain() {
let ctx = Context::new();
let s = ctx.symbol("s");
let tf = TransferFunction::new(ctx.int(5), ctx.int(1), s.clone());
let gain = tf.dc_gain().eval_f64().unwrap();
assert!(
(gain - 5.0).abs() < 1e-10,
"Constant TF DC gain should be 5, got: {gain}"
);
}
#[test]
fn state_space_display() {
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 display = format!("{ss}");
assert!(
display.contains("StateSpace"),
"Display should include 'StateSpace': {display}"
);
assert!(
display.contains("n=2"),
"Display should include state count: {display}"
);
}
#[test]
fn routh_first_order() {
let ctx = Context::new();
let coeffs = vec![ctx.int(1), ctx.int(3)];
let stability = is_routh_stable(&coeffs);
assert_eq!(stability, Some(true), "s + 3 should be stable");
}
#[test]
fn state_space_not_observable() {
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);
assert!(!ss.is_observable(), "System should NOT be observable");
}