use crate::domains::matrix::Matrix;
use crate::prelude::*;
fn poly_coeffs_symbolic(expr: &Ex, var: &Ex) -> Option<Vec<Ex>> {
let var_id = expr.checked_id(var);
let mut inner = expr.inner.write();
let ids =
crate::transforms::solve::symbolic_poly_coeffs(&mut inner.arena, expr.raw_id(), var_id)?;
drop(inner);
Some(ids.into_iter().map(|id| expr.wrap(id)).collect())
}
#[derive(Clone, Debug)]
pub struct StateSpace {
pub a: Matrix,
pub b: Matrix,
pub c: Matrix,
pub d: Matrix,
}
impl StateSpace {
fn ctx(&self) -> crate::api::context::Context {
self.a.get(0, 0).context()
}
pub fn new(a: Matrix, b: Matrix, c: Matrix, d: Matrix) -> Self {
let n = a.nrows();
assert_eq!(
a.ncols(),
n,
"A must be square: got {}×{}",
a.nrows(),
a.ncols()
);
assert_eq!(
b.nrows(),
n,
"B must have {} rows (same as A), got {}",
n,
b.nrows()
);
assert_eq!(
c.ncols(),
n,
"C must have {} cols (same as A), got {}",
n,
c.ncols()
);
let m = b.ncols();
let p = c.nrows();
assert_eq!(
d.nrows(),
p,
"D must have {} rows (same as C), got {}",
p,
d.nrows()
);
assert_eq!(
d.ncols(),
m,
"D must have {} cols (same as B), got {}",
m,
d.ncols()
);
StateSpace { a, b, c, d }
}
pub fn num_states(&self) -> usize {
self.a.nrows()
}
pub fn num_inputs(&self) -> usize {
self.b.ncols()
}
pub fn num_outputs(&self) -> usize {
self.c.nrows()
}
pub fn poles(&self) -> Vec<Ex> {
self.a.eigenvals().unwrap_or_default()
}
pub fn char_poly(&self, s: &Ex) -> Ex {
self.try_char_poly(s).unwrap_or_else(|_| self.ctx().nan())
}
pub fn try_char_poly(&self, s: &Ex) -> Result<Ex, SymplexError> {
let n = self.num_states();
let si = Matrix::identity(&self.ctx(), n).scale(s);
si.sub(&self.a)?.det()
}
pub fn to_transfer_function(&self, s: &Ex) -> Result<TransferFunction, SymplexError> {
if self.num_inputs() != 1 || self.num_outputs() != 1 {
return Err(SymplexError::InvalidArgument {
operation: "StateSpace::to_transfer_function",
reason: format!(
"requires a SISO system, got {} input(s) and {} output(s)",
self.num_inputs(),
self.num_outputs()
),
});
}
let n = self.num_states();
let si_minus_a = Matrix::identity(&self.ctx(), n).scale(s).sub(&self.a)?;
let den = si_minus_a.det()?.expand();
let adj = si_minus_a.adjugate()?;
let c_adj_b = self.c.matmul(&adj)?.matmul(&self.b)?;
let num = (c_adj_b.get(0, 0) + &(self.d.get(0, 0) * &den)).expand();
Ok(TransferFunction::new(num, den, s.clone()))
}
pub fn controllability_matrix(&self) -> Result<Matrix, SymplexError> {
let n = self.num_states();
let mut cols: Vec<Matrix> = vec![self.b.clone()];
let mut ab = self.a.matmul(&self.b)?;
for _ in 1..n {
cols.push(ab.clone());
ab = self.a.matmul(&ab)?;
}
let refs: Vec<&Matrix> = cols.iter().collect();
Matrix::hstack(&refs)
}
pub fn observability_matrix(&self) -> Result<Matrix, SymplexError> {
let n = self.num_states();
let mut rows: Vec<Matrix> = vec![self.c.clone()];
let mut ca = self.c.matmul(&self.a)?;
for _ in 1..n {
rows.push(ca.clone());
ca = ca.matmul(&self.a)?;
}
let refs: Vec<&Matrix> = rows.iter().collect();
Matrix::vstack(&refs)
}
pub fn is_controllable(&self) -> bool {
self.controllability_matrix()
.is_ok_and(|m| m.rank() == self.num_states())
}
pub fn is_observable(&self) -> bool {
self.observability_matrix()
.is_ok_and(|m| m.rank() == self.num_states())
}
pub fn is_stable(&self) -> Option<bool> {
let poles = self.poles();
if poles.len() < self.num_states() {
return None; }
for pole in &poles {
if let Ok(val) = pole.eval_f64() {
if val >= 0.0 {
return Some(false);
}
} else if let Ok((re, _im)) = pole.eval_complex64() {
if re >= 0.0 {
return Some(false);
}
} else {
return None; }
}
Some(true)
}
pub fn discretize_zoh(&self, dt: &Ex, order: usize) -> Result<StateSpace, SymplexError> {
let n = self.num_states();
let a_dt = self.a.scale(dt);
let exp_a_dt = a_dt.exp_series(order)?;
let ctx = self.ctx();
let ident = Matrix::identity(&ctx, n);
let mut b_sum = ident.scale(dt);
let mut a_power = Matrix::identity(&ctx, n);
for k in 2..=order {
a_power = a_power.matmul(&self.a)?;
let factorial: i64 = (1..=k as i64).product();
let coeff = self.ctx().rational(1, factorial);
let dt_power = dt.powi(k as i64);
let term = a_power.scale(&(&coeff * &dt_power));
b_sum = b_sum.add(&term)?;
}
let b_d = b_sum.matmul(&self.b)?;
Ok(StateSpace {
a: exp_a_dt,
b: b_d,
c: self.c.clone(),
d: self.d.clone(),
})
}
pub fn riccati_residual(
&self,
p: &Matrix,
q: &Matrix,
r: &Matrix,
) -> Result<Matrix, SymplexError> {
let at = self.a.transpose();
let r_inv = r.inv()?;
let bt = self.b.transpose();
let term1 = at.matmul(p)?; let term2 = p.matmul(&self.a)?; let term3 = p.matmul(&self.b)?.matmul(&r_inv)?.matmul(&bt)?.matmul(p)?;
term1.add(&term2)?.sub(&term3)?.add(q)
}
pub fn ackermann(&self, desired_poles: &[Ex]) -> Result<Matrix, SymplexError> {
if self.num_inputs() != 1 {
return Err(SymplexError::InvalidArgument {
operation: "StateSpace::ackermann",
reason: format!(
"requires a single-input system, got {} inputs",
self.num_inputs()
),
});
}
let n = self.num_states();
if desired_poles.len() != n {
return Err(SymplexError::InvalidArgument {
operation: "StateSpace::ackermann",
reason: format!("need {n} desired poles, got {}", desired_poles.len()),
});
}
let ctrb = self.controllability_matrix()?;
if ctrb.rank() != n {
return Err(SymplexError::ComputationFailed {
operation: "StateSpace::ackermann",
reason: "system is not controllable".into(),
});
}
let ctrb_inv = ctrb.inv()?;
let mut poly_coeffs: Vec<Ex> = vec![self.ctx().int(1)];
for pole in desired_poles {
let neg_pole = -(pole.clone());
let prev = poly_coeffs;
poly_coeffs = vec![self.ctx().int(0); prev.len() + 1];
for (i, c) in prev.into_iter().enumerate() {
let c_neg_pole = &c * &neg_pole;
poly_coeffs[i] = poly_coeffs[i].clone() + c;
poly_coeffs[i + 1] = poly_coeffs[i + 1].clone() + c_neg_pole;
}
}
let mut p_a = Matrix::zeros(&self.ctx(), n, n);
for (i, coeff) in poly_coeffs.iter().enumerate() {
let power = (poly_coeffs.len() - 1 - i) as u32;
let a_power = self.a.powi(power)?;
p_a = p_a.add(&a_power.scale(coeff))?;
}
let last_row: Vec<Ex> = (0..n).map(|j| ctrb_inv.get(n - 1, j).clone()).collect();
let last_row_mat = Matrix::new(vec![last_row])?;
last_row_mat.matmul(&p_a) }
}
impl std::fmt::Display for StateSpace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"StateSpace(n={}, m={}, p={})\nA = {}\nB = {}\nC = {}\nD = {}",
self.num_states(),
self.num_inputs(),
self.num_outputs(),
self.a,
self.b,
self.c,
self.d
)
}
}
#[derive(Clone, Debug)]
pub struct TransferFunction {
pub num: Ex,
pub den: Ex,
pub var: Ex,
}
impl TransferFunction {
fn ctx(&self) -> crate::api::context::Context {
self.num.context()
}
pub fn new(num: Ex, den: Ex, var: Ex) -> Self {
TransferFunction { num, den, var }
}
pub fn from_coeffs(num_coeffs: &[i64], den_coeffs: &[i64], var: &Ex) -> Self {
let ctx = var.context();
let build_poly = |coeffs: &[i64]| -> Ex {
let mut result = ctx.int(0);
for (i, &c) in coeffs.iter().enumerate() {
if c != 0 {
let term = if i == 0 {
ctx.int(c)
} else {
&ctx.int(c) * &var.powi(i as i64)
};
result = &result + &term;
}
}
result
};
Self::new(build_poly(num_coeffs), build_poly(den_coeffs), var.clone())
}
pub fn poles(&self) -> Vec<Ex> {
self.den.solve_or_empty(&self.var)
}
pub fn zeros(&self) -> Vec<Ex> {
self.num.solve_or_empty(&self.var)
}
pub fn dc_gain(&self) -> Ex {
let zero = self.ctx().int(0);
let num_0 = self.num.subs(&self.var, &zero).eval();
let den_0 = self.den.subs(&self.var, &zero).eval();
&num_0 / &den_0
}
pub fn series(&self, other: &TransferFunction) -> TransferFunction {
TransferFunction {
num: &self.num * &other.num,
den: &self.den * &other.den,
var: self.var.clone(),
}
}
pub fn parallel(&self, other: &TransferFunction) -> TransferFunction {
TransferFunction {
num: &(&self.num * &other.den) + &(&other.num * &self.den),
den: &self.den * &other.den,
var: self.var.clone(),
}
}
pub fn feedback(&self) -> TransferFunction {
TransferFunction {
num: self.num.clone(),
den: &self.den + &self.num,
var: self.var.clone(),
}
}
pub fn feedback_with(&self, h: &TransferFunction) -> TransferFunction {
TransferFunction {
num: &self.num * &h.den,
den: &(&self.den * &h.den) + &(&self.num * &h.num),
var: self.var.clone(),
}
}
pub fn eval_at(&self, s_val: &Ex) -> Ex {
let num = self.num.subs(&self.var, s_val).eval();
let den = self.den.subs(&self.var, s_val).eval();
&num / &den
}
pub fn to_state_space(&self) -> Result<StateSpace, SymplexError> {
let ctx = self.ctx();
let inv = |reason: String| SymplexError::InvalidArgument {
operation: "TransferFunction::to_state_space",
reason,
};
let den = poly_coeffs_symbolic(&self.den, &self.var)
.ok_or_else(|| inv("denominator is not a polynomial in the Laplace variable".into()))?;
let num = poly_coeffs_symbolic(&self.num, &self.var)
.ok_or_else(|| inv("numerator is not a polynomial in the Laplace variable".into()))?;
let n = den.len().saturating_sub(1);
if n == 0 {
return Err(inv("denominator must have degree ≥ 1".into()));
}
if num.len() > den.len() {
return Err(inv(format!(
"improper transfer function: numerator degree {} > denominator degree {}",
num.len() - 1,
n
)));
}
let lead = &den[n];
let a_coef: Vec<Ex> = den.iter().take(n).map(|c| (c / lead).eval()).collect();
let mut b_coef: Vec<Ex> = num.iter().map(|c| (c / lead).eval()).collect();
b_coef.resize(n + 1, ctx.zero());
let bn = b_coef[n].clone();
let a = Matrix::from_fn(n, n, |i, j| {
if i + 1 < n {
if j == i + 1 { ctx.one() } else { ctx.zero() }
} else {
-&a_coef[j]
}
});
let b = Matrix::from_fn(n, 1, |i, _| if i + 1 == n { ctx.one() } else { ctx.zero() });
let c = Matrix::from_fn(1, n, |_, j| (&b_coef[j] - &(&a_coef[j] * &bn)).eval());
let d = Matrix::new(vec![vec![bn]])?;
Ok(StateSpace::new(a, b, c, d))
}
}
impl std::fmt::Display for TransferFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}) / ({})", self.num, self.den)
}
}
pub fn routh_array(coeffs: &[Ex]) -> Vec<Vec<Ex>> {
assert!(
!coeffs.is_empty(),
"routh_array: coefficients must not be empty"
);
let n = coeffs.len();
if n == 1 {
return vec![vec![coeffs[0].clone()]];
}
let num_cols = n.div_ceil(2);
let mut row0: Vec<Ex> = Vec::with_capacity(num_cols);
for i in (0..n).step_by(2) {
row0.push(coeffs[i].clone());
}
while row0.len() < num_cols {
row0.push(coeffs[0].context().int(0));
}
let mut row1: Vec<Ex> = Vec::with_capacity(num_cols);
for i in (1..n).step_by(2) {
row1.push(coeffs[i].clone());
}
while row1.len() < num_cols {
row1.push(coeffs[0].context().int(0));
}
let mut table: Vec<Vec<Ex>> = vec![row0, row1];
let total_rows = n;
for i in 2..total_rows {
let prev = &table[i - 1];
let prev2 = &table[i - 2];
let mut pivot = prev[0].clone();
let pivot_is_zero = pivot.eval_f64().map(|v| v.abs() < 1e-30).unwrap_or(false);
if pivot_is_zero {
pivot = coeffs[0].context().rational(1, 1_000_000_000);
}
let mut new_row: Vec<Ex> = Vec::with_capacity(num_cols);
for j in 0..(num_cols - 1) {
let prev2_j1 = if j + 1 < prev2.len() {
prev2[j + 1].clone()
} else {
coeffs[0].context().int(0)
};
let prev_j1 = if j + 1 < prev.len() {
prev[j + 1].clone()
} else {
coeffs[0].context().int(0)
};
let numerator = &(&pivot * &prev2_j1) - &(&prev2[0] * &prev_j1);
let entry = (&numerator / &pivot).eval();
new_row.push(entry);
}
if new_row.is_empty() {
new_row.push(coeffs[0].context().int(0));
}
table.push(new_row);
}
table
}
pub fn is_routh_stable(coeffs: &[Ex]) -> Option<bool> {
if coeffs.is_empty() {
return Some(false);
}
let table = routh_array(coeffs);
let first_col: Vec<&Ex> = table.iter().map(|row| &row[0]).collect();
let mut values: Vec<f64> = Vec::with_capacity(first_col.len());
for entry in &first_col {
if let Ok(val) = entry.eval_f64() {
values.push(val);
} else {
return None; }
}
if values.is_empty() {
return None;
}
let first_sign = values[0] > 0.0;
for &val in &values[1..] {
if val == 0.0 {
return Some(false);
}
if (val > 0.0) != first_sign {
return Some(false); }
}
Some(true)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn state_space_basic_construction() {
let ctx = crate::api::context::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 transfer_function_basic_display() {
let ctx = crate::api::context::Context::new();
let s = ctx.symbol("s");
let tf = TransferFunction::new(ctx.int(1), &s * &s + &s * 3 + 2, s);
let display = format!("{tf}");
assert!(
display.contains("/"),
"Display should show fraction: {display}"
);
}
#[test]
fn routh_array_row_count() {
let ctx = crate::api::context::Context::new();
let coeffs = vec![ctx.int(1), ctx.int(3), ctx.int(2)];
let table = routh_array(&coeffs);
assert_eq!(table.len(), 3);
}
}