#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
#[derive(Debug, Clone, Copy, PartialEq)]
struct Cplx {
re: f64,
im: f64,
}
impl Cplx {
fn new(re: f64, im: f64) -> Self {
Self { re, im }
}
fn abs(self) -> f64 {
(self.re * self.re + self.im * self.im).sqrt()
}
fn arg(self) -> f64 {
self.im.atan2(self.re)
}
fn mul(self, other: Self) -> Self {
Self {
re: self.re * other.re - self.im * other.im,
im: self.re * other.im + self.im * other.re,
}
}
fn add(self, other: Self) -> Self {
Self {
re: self.re + other.re,
im: self.im + other.im,
}
}
fn sub(self, other: Self) -> Self {
Self {
re: self.re - other.re,
im: self.im - other.im,
}
}
fn div(self, other: Self) -> Self {
let denom = other.re * other.re + other.im * other.im;
if denom.abs() < 1e-300 {
return Self::new(f64::NAN, f64::NAN);
}
Self {
re: (self.re * other.re + self.im * other.im) / denom,
im: (self.im * other.re - self.re * other.im) / denom,
}
}
fn scale(self, s: f64) -> Self {
Self {
re: self.re * s,
im: self.im * s,
}
}
fn conj(self) -> Self {
Self {
re: self.re,
im: -self.im,
}
}
}
fn poly_eval_cplx(coeffs: &[f64], s: Cplx) -> Cplx {
let mut result = Cplx::new(0.0, 0.0);
for &c in coeffs {
result = result.mul(s).add(Cplx::new(c, 0.0));
}
result
}
fn mat_mul(a: &[f64], b: &[f64], r: usize, k: usize, c: usize) -> Vec<f64> {
let mut out = vec![0.0f64; r * c];
for i in 0..r {
for j in 0..c {
let mut s = 0.0;
for p in 0..k {
s += a[i * k + p] * b[p * c + j];
}
out[i * c + j] = s;
}
}
out
}
fn mat_add(a: &[f64], b: &[f64]) -> Vec<f64> {
a.iter().zip(b.iter()).map(|(x, y)| x + y).collect()
}
fn mat_sub(a: &[f64], b: &[f64]) -> Vec<f64> {
a.iter().zip(b.iter()).map(|(x, y)| x - y).collect()
}
fn mat_transpose(a: &[f64], n: usize, m: usize) -> Vec<f64> {
let mut out = vec![0.0f64; n * m];
for i in 0..n {
for j in 0..m {
out[j * n + i] = a[i * m + j];
}
}
out
}
fn mat_scale(a: &[f64], s: f64) -> Vec<f64> {
a.iter().map(|x| x * s).collect()
}
fn mat_eye(n: usize) -> Vec<f64> {
let mut out = vec![0.0f64; n * n];
for i in 0..n {
out[i * n + i] = 1.0;
}
out
}
fn mat_inv2(a: &[f64]) -> Option<Vec<f64>> {
let det = a[0] * a[3] - a[1] * a[2];
if det.abs() < 1e-300 {
return None;
}
Some(vec![a[3] / det, -a[1] / det, -a[2] / det, a[0] / det])
}
fn mat_inv(a: &[f64], n: usize) -> Option<Vec<f64>> {
let mut m = a.to_vec();
let mut inv = mat_eye(n);
for col in 0..n {
let mut max_row = col;
let mut max_val = m[col * n + col].abs();
for row in (col + 1)..n {
let v = m[row * n + col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < 1e-300 {
return None;
}
if max_row != col {
for j in 0..n {
m.swap(col * n + j, max_row * n + j);
inv.swap(col * n + j, max_row * n + j);
}
}
let pivot = m[col * n + col];
for j in 0..n {
m[col * n + j] /= pivot;
inv[col * n + j] /= pivot;
}
for row in 0..n {
if row != col {
let factor = m[row * n + col];
for j in 0..n {
let mv = m[col * n + j];
let iv = inv[col * n + j];
m[row * n + j] -= factor * mv;
inv[row * n + j] -= factor * iv;
}
}
}
}
Some(inv)
}
fn mat_frob(a: &[f64]) -> f64 {
a.iter().map(|x| x * x).sum::<f64>().sqrt()
}
#[derive(Debug, Clone)]
pub struct PidController {
pub kp: f64,
pub ki: f64,
pub kd: f64,
pub n: f64,
pub integral_limit: f64,
pub out_min: f64,
pub out_max: f64,
pub integral: f64,
pub deriv_filter: f64,
pub prev_error: f64,
}
impl PidController {
pub fn new(kp: f64, ki: f64, kd: f64) -> Self {
Self {
kp,
ki,
kd,
n: 10.0,
integral_limit: 1e9,
out_min: f64::NEG_INFINITY,
out_max: f64::INFINITY,
integral: 0.0,
deriv_filter: 0.0,
prev_error: 0.0,
}
}
pub fn with_n(mut self, n: f64) -> Self {
self.n = n;
self
}
pub fn with_integral_limit(mut self, limit: f64) -> Self {
self.integral_limit = limit;
self
}
pub fn with_output_limits(mut self, min: f64, max: f64) -> Self {
self.out_min = min;
self.out_max = max;
self
}
pub fn update(&mut self, error: f64, dt: f64) -> f64 {
if dt <= 0.0 {
return 0.0;
}
let p = self.kp * error;
self.integral += error * dt;
self.integral = self
.integral
.clamp(-self.integral_limit, self.integral_limit);
let i = self.ki * self.integral;
let alpha = 1.0 / (1.0 + self.n * dt);
let raw_deriv = (error - self.prev_error) / dt;
self.deriv_filter = alpha * self.deriv_filter + (1.0 - alpha) * raw_deriv;
let d = self.kd * self.deriv_filter;
self.prev_error = error;
(p + i + d).clamp(self.out_min, self.out_max)
}
pub fn reset(&mut self) {
self.integral = 0.0;
self.deriv_filter = 0.0;
self.prev_error = 0.0;
}
}
#[derive(Debug, Clone)]
pub struct StateSpaceModel {
pub a: Vec<f64>,
pub b: Vec<f64>,
pub c: Vec<f64>,
pub d: Vec<f64>,
pub n: usize,
pub m: usize,
pub p: usize,
pub state: Vec<f64>,
}
impl StateSpaceModel {
pub fn new(
a: Vec<f64>,
b: Vec<f64>,
c: Vec<f64>,
d: Vec<f64>,
n: usize,
m: usize,
p: usize,
) -> Self {
assert_eq!(a.len(), n * n, "A must be n×n");
assert_eq!(b.len(), n * m, "B must be n×m");
assert_eq!(c.len(), p * n, "C must be p×n");
assert_eq!(d.len(), p * m, "D must be p×m");
Self {
a,
b,
c,
d,
n,
m,
p,
state: vec![0.0; n],
}
}
pub fn step(&mut self, u: &[f64]) -> Vec<f64> {
assert_eq!(u.len(), self.m);
let ax = mat_mul(&self.a, &self.state, self.n, self.n, 1);
let bu = mat_mul(&self.b, u, self.n, self.m, 1);
let x_new = mat_add(&ax, &bu);
let cx = mat_mul(&self.c, &self.state, self.p, self.n, 1);
let du = mat_mul(&self.d, u, self.p, self.m, 1);
let y = mat_add(&cx, &du);
self.state = x_new;
y
}
pub fn step_response(&mut self, steps: usize) -> Vec<(f64, f64)> {
self.state = vec![0.0; self.n];
let u = vec![1.0; self.m];
(0..steps)
.map(|k| {
let y = self.step(&u);
(k as f64, y[0])
})
.collect()
}
pub fn is_stable(&self) -> bool {
let spectral_radius = spectral_radius_power_iter(&self.a, self.n, 200, 1e-8);
spectral_radius < 1.0
}
pub fn reset(&mut self) {
self.state = vec![0.0; self.n];
}
}
fn spectral_radius_power_iter(a: &[f64], n: usize, max_iter: usize, tol: f64) -> f64 {
if n == 0 {
return 0.0;
}
let mut v = vec![1.0f64; n];
let mut lambda = 0.0f64;
for _iter in 0..max_iter {
let w = mat_mul(a, &v, n, n, 1);
let norm: f64 = w.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm < 1e-300 {
return 0.0;
}
let new_lambda = norm;
v = w.iter().map(|x| x / norm).collect();
if (new_lambda - lambda).abs() < tol {
return new_lambda;
}
lambda = new_lambda;
}
lambda
}
#[derive(Debug, Clone)]
pub struct LqrController {
pub k: Vec<f64>,
pub n: usize,
pub m: usize,
}
impl LqrController {
pub fn solve(
a: &[f64],
b: &[f64],
q: &[f64],
r: &[f64],
n: usize,
m: usize,
max_iter: usize,
tol: f64,
) -> Option<Self> {
let at = mat_transpose(a, n, n);
let bt = mat_transpose(b, n, m);
let mut p = q.to_vec();
for _iter in 0..max_iter {
let _p_a = mat_mul(&p, a, n, n, n); let at_p = mat_mul(&at, &p, n, n, n); let at_p_a = mat_mul(&at_p, a, n, n, n); let at_p_b = mat_mul(&at_p, b, n, n, m); let bt_p = mat_mul(&bt, &p, m, n, n); let bt_p_b = mat_mul(&bt_p, b, m, n, m); let r_bpb = mat_add(r, &bt_p_b);
let r_bpb_inv = mat_inv(&r_bpb, m)?;
let at_p_b_rinv = mat_mul(&at_p_b, &r_bpb_inv, n, m, m); let correction = mat_mul(&at_p_b_rinv, &mat_transpose(&at_p_b, n, m), n, m, n);
let p_new = mat_sub(&mat_add(q, &at_p_a), &correction);
let diff = mat_frob(&mat_sub(&p_new, &p));
p = p_new;
if diff < tol {
break;
}
}
let at = mat_transpose(a, n, n);
let bt = mat_transpose(b, n, m);
let bt_p = mat_mul(&bt, &p, m, n, n);
let bt_p_b = mat_mul(&bt_p, b, m, n, m);
let r_bpb = mat_add(r, &bt_p_b);
let r_bpb_inv = mat_inv(&r_bpb, m)?;
let bt_p_a = mat_mul(&bt_p, a, m, n, n);
let k = mat_mul(&r_bpb_inv, &bt_p_a, m, m, n);
let _ = &at;
Some(Self { k, n, m })
}
pub fn control(&self, x: &[f64]) -> Vec<f64> {
let kx = mat_mul(&self.k, x, self.m, self.n, 1);
kx.iter().map(|v| -v).collect()
}
}
#[derive(Debug, Clone)]
pub struct KalmanFilter {
pub a: Vec<f64>,
pub b: Vec<f64>,
pub c: Vec<f64>,
pub q: Vec<f64>,
pub r: Vec<f64>,
pub x: Vec<f64>,
pub p: Vec<f64>,
pub n: usize,
pub m: usize,
pub p_dim: usize,
}
impl KalmanFilter {
pub fn new(
a: Vec<f64>,
b: Vec<f64>,
c: Vec<f64>,
q: Vec<f64>,
r: Vec<f64>,
n: usize,
m: usize,
p_dim: usize,
) -> Self {
let p_init = mat_eye(n);
Self {
a,
b,
c,
q,
r,
x: vec![0.0; n],
p: p_init,
n,
m,
p_dim,
}
}
pub fn predict(&mut self, u: &[f64]) {
let ax = mat_mul(&self.a, &self.x, self.n, self.n, 1);
let bu = mat_mul(&self.b, u, self.n, self.m, 1);
self.x = mat_add(&ax, &bu);
let at = mat_transpose(&self.a, self.n, self.n);
let apa = mat_mul(
&mat_mul(&self.a, &self.p, self.n, self.n, self.n),
&at,
self.n,
self.n,
self.n,
);
self.p = mat_add(&apa, &self.q);
}
pub fn update(&mut self, y: &[f64]) -> Option<()> {
let ct = mat_transpose(&self.c, self.p_dim, self.n);
let pct = mat_mul(&self.p, &ct, self.n, self.n, self.p_dim);
let cpct = mat_mul(&self.c, &pct, self.p_dim, self.n, self.p_dim);
let s = mat_add(&cpct, &self.r);
let s_inv = mat_inv(&s, self.p_dim)?;
let k = mat_mul(&pct, &s_inv, self.n, self.p_dim, self.p_dim);
let cx = mat_mul(&self.c, &self.x, self.p_dim, self.n, 1);
let innov: Vec<f64> = y.iter().zip(cx.iter()).map(|(yi, ci)| yi - ci).collect();
let k_innov = mat_mul(&k, &innov, self.n, self.p_dim, 1);
self.x = mat_add(&self.x, &k_innov);
let kc = mat_mul(&k, &self.c, self.n, self.p_dim, self.n);
let i_kc = mat_sub(&mat_eye(self.n), &kc);
self.p = mat_mul(&i_kc, &self.p, self.n, self.n, self.n);
Some(())
}
pub fn step(&mut self, u: &[f64], y: &[f64]) -> Option<Vec<f64>> {
self.predict(u);
self.update(y)?;
Some(self.x.clone())
}
}
#[derive(Debug, Clone)]
pub struct ExtendedKalmanFilter {
pub x: Vec<f64>,
pub p: Vec<f64>,
pub q: Vec<f64>,
pub r: Vec<f64>,
pub n: usize,
pub p_dim: usize,
}
impl ExtendedKalmanFilter {
pub fn new(q: Vec<f64>, r: Vec<f64>, n: usize, p_dim: usize) -> Self {
Self {
x: vec![0.0; n],
p: mat_eye(n),
q,
r,
n,
p_dim,
}
}
pub fn predict<F, J>(&mut self, f: F, jac_f: J, u: &[f64])
where
F: Fn(&[f64], &[f64]) -> Vec<f64>,
J: Fn(&[f64], &[f64]) -> Vec<f64>,
{
let f_val = jac_f(&self.x, u);
let ft = mat_transpose(&f_val, self.n, self.n);
let fp = mat_mul(&f_val, &self.p, self.n, self.n, self.n);
let fpft = mat_mul(&fp, &ft, self.n, self.n, self.n);
self.p = mat_add(&fpft, &self.q);
self.x = f(&self.x, u);
}
pub fn update<H, J>(&mut self, h: H, jac_h: J, y: &[f64]) -> Option<()>
where
H: Fn(&[f64]) -> Vec<f64>,
J: Fn(&[f64]) -> Vec<f64>,
{
let h_jac = jac_h(&self.x);
let ht = mat_transpose(&h_jac, self.p_dim, self.n);
let pht = mat_mul(&self.p, &ht, self.n, self.n, self.p_dim);
let hpht = mat_mul(&h_jac, &pht, self.p_dim, self.n, self.p_dim);
let s = mat_add(&hpht, &self.r);
let s_inv = mat_inv(&s, self.p_dim)?;
let k = mat_mul(&pht, &s_inv, self.n, self.p_dim, self.p_dim);
let h_x = h(&self.x);
let innov: Vec<f64> = y.iter().zip(h_x.iter()).map(|(yi, hi)| yi - hi).collect();
let k_innov = mat_mul(&k, &innov, self.n, self.p_dim, 1);
self.x = mat_add(&self.x, &k_innov);
let kh = mat_mul(&k, &h_jac, self.n, self.p_dim, self.n);
let i_kh = mat_sub(&mat_eye(self.n), &kh);
self.p = mat_mul(&i_kh, &self.p, self.n, self.n, self.n);
Some(())
}
}
#[derive(Debug, Clone)]
pub struct BodeAnalysis {
pub num: Vec<f64>,
pub den: Vec<f64>,
}
#[derive(Debug, Clone, Copy)]
pub struct BodePoint {
pub omega: f64,
pub gain_db: f64,
pub phase_deg: f64,
}
impl BodeAnalysis {
pub fn new(num: Vec<f64>, den: Vec<f64>) -> Self {
Self { num, den }
}
pub fn eval_at(&self, omega: f64) -> (f64, f64) {
let s = Cplx::new(0.0, omega);
let num_val = poly_eval_cplx(&self.num, s);
let den_val = poly_eval_cplx(&self.den, s);
let g = num_val.div(den_val);
let gain = g.abs();
let phase_deg = g.arg().to_degrees();
(gain, phase_deg)
}
pub fn compute(&self, omegas: &[f64]) -> Vec<BodePoint> {
omegas
.iter()
.map(|&omega| {
let (gain, phase_deg) = self.eval_at(omega);
let gain_db = if gain > 1e-300 {
20.0 * gain.log10()
} else {
-300.0
};
BodePoint {
omega,
gain_db,
phase_deg,
}
})
.collect()
}
pub fn compute_log(&self, omega_min: f64, omega_max: f64, n_points: usize) -> Vec<BodePoint> {
if n_points == 0 {
return vec![];
}
let log_min = omega_min.log10();
let log_max = omega_max.log10();
let omegas: Vec<f64> = (0..n_points)
.map(|i| {
let t = i as f64 / (n_points - 1).max(1) as f64;
10f64.powf(log_min + t * (log_max - log_min))
})
.collect();
self.compute(&omegas)
}
pub fn margins(&self, omega_min: f64, omega_max: f64, n_points: usize) -> (f64, f64) {
let points = self.compute_log(omega_min, omega_max, n_points);
if points.is_empty() {
return (f64::NAN, f64::NAN);
}
let mut phase_cross_gain_db = f64::NAN;
for i in 1..points.len() {
let p0 = &points[i - 1];
let p1 = &points[i];
if (p0.phase_deg + 180.0) * (p1.phase_deg + 180.0) <= 0.0 {
let frac = (-(p0.phase_deg + 180.0)) / (p1.phase_deg - p0.phase_deg + 1e-300);
phase_cross_gain_db = p0.gain_db + frac * (p1.gain_db - p0.gain_db);
break;
}
}
let mut gain_cross_phase_margin = f64::NAN;
for i in 1..points.len() {
let p0 = &points[i - 1];
let p1 = &points[i];
if p0.gain_db * p1.gain_db <= 0.0 {
let frac = (-p0.gain_db) / (p1.gain_db - p0.gain_db + 1e-300);
let phase = p0.phase_deg + frac * (p1.phase_deg - p0.phase_deg);
gain_cross_phase_margin = phase + 180.0;
break;
}
}
let gain_margin_db = -phase_cross_gain_db;
(gain_margin_db, gain_cross_phase_margin)
}
}
#[derive(Debug, Clone)]
pub struct RootLocus {
pub num: Vec<f64>,
pub den: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct RootLocusPoint {
pub gain: f64,
pub poles: Vec<(f64, f64)>,
}
impl RootLocus {
pub fn new(num: Vec<f64>, den: Vec<f64>) -> Self {
Self { num, den }
}
pub fn compute(&self, gains: &[f64]) -> Vec<RootLocusPoint> {
gains
.iter()
.map(|&k| {
let poles = self.poles_at_gain(k);
RootLocusPoint { gain: k, poles }
})
.collect()
}
pub fn poles_at_gain(&self, k: f64) -> Vec<(f64, f64)> {
let n_deg = self.den.len().max(self.num.len());
let mut char_poly = vec![0.0f64; n_deg];
for (i, &c) in self.den.iter().enumerate() {
let offset = n_deg - self.den.len();
char_poly[i + offset] += c;
}
for (i, &c) in self.num.iter().enumerate() {
let offset = n_deg - self.num.len();
char_poly[i + offset] += k * c;
}
find_polynomial_roots(&char_poly)
}
pub fn breakaway_points(&self, s_min: f64, s_max: f64, n_samples: usize) -> Vec<f64> {
let ds = (s_max - s_min) / (n_samples as f64);
let mut points = Vec::new();
let k_at = |s_real: f64| {
let s = Cplx::new(s_real, 0.0);
let d = poly_eval_cplx(&self.den, s);
let n = poly_eval_cplx(&self.num, s);
if n.abs() < 1e-12 {
f64::NAN
} else {
-d.re / n.re
}
};
let mut prev_k = k_at(s_min);
let mut prev_dk = f64::NAN;
let mut prev_s = s_min;
for i in 1..=n_samples {
let s = s_min + i as f64 * ds;
let cur_k = k_at(s);
let dk = (cur_k - prev_k) / ds;
if !prev_dk.is_nan() && !dk.is_nan() && prev_dk * dk < 0.0 {
let s_break = prev_s + (-prev_dk) / (dk - prev_dk + 1e-300) * ds;
points.push(s_break);
}
prev_dk = dk;
prev_k = cur_k;
prev_s = s;
}
points
}
}
fn find_polynomial_roots(coeffs: &[f64]) -> Vec<(f64, f64)> {
let start = coeffs.iter().position(|&c| c.abs() > 1e-300).unwrap_or(0);
let c = &coeffs[start..];
let degree = c.len().saturating_sub(1);
if degree == 0 {
return vec![];
}
let leading = c[0];
let n = degree;
let mut comp = vec![0.0f64; n * n];
for i in 0..(n - 1) {
comp[i * n + (i + 1)] = 1.0;
}
for j in 0..n {
comp[(n - 1) * n + j] = -c[n - j] / leading;
}
qr_eigenvalues(&comp, n, 200)
}
fn qr_eigenvalues(a: &[f64], n: usize, max_iter: usize) -> Vec<(f64, f64)> {
if n == 1 {
return vec![(a[0], 0.0)];
}
if n == 2 {
let trace = a[0] + a[3];
let det = a[0] * a[3] - a[1] * a[2];
let disc = trace * trace - 4.0 * det;
if disc >= 0.0 {
let sqrt_d = disc.sqrt();
return vec![((trace + sqrt_d) / 2.0, 0.0), ((trace - sqrt_d) / 2.0, 0.0)];
} else {
let sqrt_d = (-disc).sqrt();
return vec![(trace / 2.0, sqrt_d / 2.0), (trace / 2.0, -sqrt_d / 2.0)];
}
}
let mut h = a.to_vec();
let _result: Vec<(f64, f64)> = vec![];
for _iter in 0..max_iter {
let mu = h[(n - 1) * n + (n - 1)];
for i in 0..n {
h[i * n + i] -= mu;
}
let (q, r) = qr_decomp_gs(&h, n);
h = mat_mul(&r, &q, n, n, n);
for i in 0..n {
h[i * n + i] += mu;
}
let off = h
.iter()
.enumerate()
.filter(|(idx, _)| {
let row = idx / n;
let col = idx % n;
col < row
})
.map(|(_, &v)| v * v)
.sum::<f64>()
.sqrt();
if off < 1e-10 {
break;
}
}
let mut eigs = Vec::new();
let mut i = 0;
while i < n {
if i + 1 < n && h[i * n + (i + 1)].abs() > 1e-8 && h[(i + 1) * n + i].abs() > 1e-8 {
let a00 = h[i * n + i];
let a01 = h[i * n + (i + 1)];
let a10 = h[(i + 1) * n + i];
let a11 = h[(i + 1) * n + (i + 1)];
let trace = a00 + a11;
let det = a00 * a11 - a01 * a10;
let disc = trace * trace - 4.0 * det;
if disc >= 0.0 {
let sqrt_d = disc.sqrt();
eigs.push(((trace + sqrt_d) / 2.0, 0.0));
eigs.push(((trace - sqrt_d) / 2.0, 0.0));
} else {
let sqrt_d = (-disc).sqrt();
eigs.push((trace / 2.0, sqrt_d / 2.0));
eigs.push((trace / 2.0, -sqrt_d / 2.0));
}
i += 2;
} else {
eigs.push((h[i * n + i], 0.0));
i += 1;
}
}
eigs
}
fn qr_decomp_gs(a: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
let mut q = vec![0.0f64; n * n];
let mut r = vec![0.0f64; n * n];
let col = |m: &[f64], j: usize| -> Vec<f64> { (0..n).map(|i| m[i * n + j]).collect() };
let dot = |u: &[f64], v: &[f64]| -> f64 { u.iter().zip(v.iter()).map(|(a, b)| a * b).sum() };
let norm = |u: &[f64]| -> f64 { dot(u, u).sqrt() };
let mut q_cols: Vec<Vec<f64>> = Vec::new();
for j in 0..n {
let mut v = col(a, j);
for (k, qk) in q_cols.iter().enumerate() {
let proj = dot(&v, qk);
r[k * n + j] = proj;
for i in 0..n {
v[i] -= proj * qk[i];
}
}
let nrm = norm(&v);
r[j * n + j] = nrm;
let qj: Vec<f64> = if nrm < 1e-300 {
vec![0.0; n]
} else {
v.iter().map(|x| x / nrm).collect()
};
for i in 0..n {
q[i * n + j] = qj[i];
}
q_cols.push(qj);
}
(q, r)
}
#[derive(Debug, Clone, Copy)]
pub struct ZieglerNichols {
pub ku: f64,
pub tu: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct ZnGains {
pub kp: f64,
pub ki: f64,
pub kd: f64,
}
impl ZieglerNichols {
pub fn new(ku: f64, tu: f64) -> Self {
Self { ku, tu }
}
pub fn pid(&self) -> ZnGains {
let kp = 0.6 * self.ku;
let ti = self.tu / 2.0;
let td = self.tu / 8.0;
ZnGains {
kp,
ki: kp / ti,
kd: kp * td,
}
}
pub fn pi(&self) -> ZnGains {
let kp = 0.45 * self.ku;
let ti = self.tu / 1.2;
ZnGains {
kp,
ki: kp / ti,
kd: 0.0,
}
}
pub fn p_only(&self) -> ZnGains {
ZnGains {
kp: 0.5 * self.ku,
ki: 0.0,
kd: 0.0,
}
}
pub fn simc(&self) -> ZnGains {
let kp = 0.6 * self.ku;
let ti = 0.5 * self.tu;
let td = 0.125 * self.tu;
ZnGains {
kp,
ki: kp / ti,
kd: kp * td,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct LeadLagCompensator {
pub kc: f64,
pub z: f64,
pub p: f64,
}
impl LeadLagCompensator {
pub fn new(kc: f64, z: f64, p: f64) -> Self {
Self { kc, z, p }
}
pub fn eval_at(&self, omega: f64) -> (f64, f64) {
let num = Cplx::new(self.z, omega);
let den = Cplx::new(self.p, omega);
let c = num.div(den).scale(self.kc);
(c.abs(), c.arg().to_degrees())
}
pub fn max_phase_lead(&self) -> (f64, f64) {
let omega_max = (self.z * self.p).sqrt();
let phi_max = ((self.p - self.z) / (self.p + self.z)).asin().to_degrees();
(omega_max, phi_max)
}
pub fn dc_gain(&self) -> f64 {
self.kc * self.z / self.p
}
pub fn hf_gain(&self) -> f64 {
self.kc
}
pub fn bode(&self, omega_min: f64, omega_max: f64, n: usize) -> Vec<BodePoint> {
if n == 0 {
return vec![];
}
let log_min = omega_min.log10();
let log_max = omega_max.log10();
(0..n)
.map(|i| {
let t = i as f64 / (n - 1).max(1) as f64;
let omega = 10f64.powf(log_min + t * (log_max - log_min));
let (mag, phase) = self.eval_at(omega);
let gain_db = if mag > 1e-300 {
20.0 * mag.log10()
} else {
-300.0
};
BodePoint {
omega,
gain_db,
phase_deg: phase,
}
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct PolesZeros {
pub gain: f64,
pub zeros: Vec<(f64, f64)>,
pub poles: Vec<(f64, f64)>,
}
impl PolesZeros {
pub fn new(gain: f64, zeros: Vec<(f64, f64)>, poles: Vec<(f64, f64)>) -> Self {
Self { gain, zeros, poles }
}
pub fn eval_at(&self, omega: f64) -> (f64, f64) {
let s = Cplx::new(0.0, omega);
let mut num = Cplx::new(self.gain, 0.0);
for &(zr, zi) in &self.zeros {
num = num.mul(s.sub(Cplx::new(zr, zi)));
}
let mut den = Cplx::new(1.0, 0.0);
for &(pr, pi) in &self.poles {
den = den.mul(s.sub(Cplx::new(pr, pi)));
}
let g = num.div(den);
(g.abs(), g.arg().to_degrees())
}
pub fn dc_gain(&self) -> f64 {
let s = Cplx::new(0.0, 0.0);
let mut num = Cplx::new(self.gain, 0.0);
for &(zr, zi) in &self.zeros {
num = num.mul(s.sub(Cplx::new(zr, zi)));
}
let mut den = Cplx::new(1.0, 0.0);
for &(pr, pi) in &self.poles {
den = den.mul(s.sub(Cplx::new(pr, pi)));
}
if den.abs() < 1e-300 {
return f64::INFINITY;
}
num.div(den).abs()
}
pub fn is_stable_continuous(&self) -> bool {
self.poles.iter().all(|&(re, _)| re < 0.0)
}
pub fn is_stable_discrete(&self) -> bool {
self.poles
.iter()
.all(|&(re, im)| (re * re + im * im).sqrt() < 1.0)
}
pub fn step_response_approx(&self, dt: f64, steps: usize) -> Vec<(f64, f64)> {
let n = self.poles.len();
if n == 0 {
return (0..steps).map(|k| (k as f64 * dt, self.gain)).collect();
}
let mut den_poly = vec![1.0f64]; for &(pr, pi) in &self.poles {
if pi.abs() < 1e-10 {
let old = den_poly.clone();
den_poly = vec![0.0; old.len() + 1];
for (i, &c) in old.iter().enumerate() {
den_poly[i] += c;
den_poly[i + 1] += c * (-pr);
}
}
}
let mut num_poly = vec![self.gain];
for &(zr, zi) in &self.zeros {
if zi.abs() < 1e-10 {
let old = num_poly.clone();
num_poly = vec![0.0; old.len() + 1];
for (i, &c) in old.iter().enumerate() {
num_poly[i] += c;
num_poly[i + 1] += c * (-zr);
}
}
}
let _num_poly = num_poly;
let _den_poly = den_poly;
let mut result = Vec::with_capacity(steps);
for k in 0..steps {
let t = k as f64 * dt;
let mut y = self.gain;
for &(pr, pi) in &self.poles {
if pi.abs() < 1e-10 {
if pr.abs() > 1e-12 {
y += (pr * t).exp() * 0.0; }
} else {
let _ = pr + pi; }
}
let mut y_step = 1.0 - 0.0; for &(pr, pi) in &self.poles {
if pi.abs() < 1e-10 && pr < 0.0 {
y_step += -(pr * t).exp() / self.poles.len() as f64;
}
}
let _ = y;
result.push((t, y_step * self.gain));
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pid_proportional_only() {
let mut pid = PidController::new(2.0, 0.0, 0.0);
let out = pid.update(1.0, 0.01);
assert!((out - 2.0).abs() < 1e-10);
}
#[test]
fn test_pid_output_clamping() {
let mut pid = PidController::new(10.0, 0.0, 0.0).with_output_limits(-5.0, 5.0);
let out = pid.update(1.0, 0.01); assert!((out - 5.0).abs() < 1e-10);
}
#[test]
fn test_pid_integral_accumulates() {
let mut pid = PidController::new(0.0, 1.0, 0.0);
pid.update(1.0, 0.1); let out = pid.update(1.0, 0.1); assert!((out - 0.2).abs() < 1e-10);
}
#[test]
fn test_pid_anti_windup() {
let mut pid = PidController::new(0.0, 1.0, 0.0).with_integral_limit(1.0);
for _ in 0..100 {
pid.update(1.0, 0.1); }
assert!(pid.integral.abs() <= 1.0 + 1e-10);
}
#[test]
fn test_pid_reset_clears_state() {
let mut pid = PidController::new(1.0, 1.0, 1.0);
pid.update(1.0, 0.1);
pid.reset();
assert_eq!(pid.integral, 0.0);
assert_eq!(pid.deriv_filter, 0.0);
assert_eq!(pid.prev_error, 0.0);
}
#[test]
fn test_pid_derivative_filter() {
let mut pid = PidController::new(0.0, 0.0, 1.0).with_n(5.0);
let out = pid.update(1.0, 0.1);
assert!(out.abs() > 0.0);
}
#[test]
fn test_pid_zero_dt_returns_zero() {
let mut pid = PidController::new(1.0, 1.0, 1.0);
let out = pid.update(1.0, 0.0);
assert_eq!(out, 0.0);
}
#[test]
fn test_pid_negative_error() {
let mut pid = PidController::new(1.0, 0.0, 0.0);
let out = pid.update(-3.0, 0.01);
assert!((out + 3.0).abs() < 1e-10);
}
#[test]
fn test_state_space_stable_1d() {
let ss = StateSpaceModel::new(vec![0.5], vec![0.0], vec![1.0], vec![0.0], 1, 1, 1);
assert!(ss.is_stable());
}
#[test]
fn test_state_space_unstable_1d() {
let ss = StateSpaceModel::new(vec![1.5], vec![0.0], vec![1.0], vec![0.0], 1, 1, 1);
assert!(!ss.is_stable());
}
#[test]
fn test_state_space_step_response_length() {
let mut ss = StateSpaceModel::new(vec![0.5], vec![1.0], vec![1.0], vec![0.0], 1, 1, 1);
let resp = ss.step_response(20);
assert_eq!(resp.len(), 20);
}
#[test]
fn test_state_space_step_converges() {
let mut ss = StateSpaceModel::new(vec![0.9], vec![0.1], vec![1.0], vec![0.0], 1, 1, 1);
let resp = ss.step_response(100);
let last_y = resp.last().unwrap().1;
assert!((last_y - 1.0).abs() < 0.05, "expected ~1.0, got {}", last_y);
}
#[test]
fn test_state_space_step_produces_output() {
let mut ss = StateSpaceModel::new(vec![0.0], vec![1.0], vec![1.0], vec![0.0], 1, 1, 1);
let y = ss.step(&[1.0]);
assert_eq!(y.len(), 1);
}
#[test]
fn test_state_space_reset() {
let mut ss = StateSpaceModel::new(vec![0.9], vec![0.1], vec![1.0], vec![0.0], 1, 1, 1);
ss.step(&[1.0]);
ss.reset();
assert_eq!(ss.state, vec![0.0]);
}
#[test]
fn test_lqr_1d_stability() {
let a = vec![1.1f64];
let b = vec![1.0f64];
let q = vec![1.0f64];
let r = vec![1.0f64];
let lqr = LqrController::solve(&a, &b, &q, &r, 1, 1, 100, 1e-10).unwrap();
assert!(lqr.k[0] > 0.0, "LQR gain should be positive");
let cl = a[0] - b[0] * lqr.k[0];
assert!(cl.abs() < 1.0, "closed-loop pole |{}| should be < 1", cl);
}
#[test]
fn test_lqr_control_output_sign() {
let a = vec![1.1f64];
let b = vec![1.0f64];
let q = vec![1.0f64];
let r = vec![1.0f64];
let lqr = LqrController::solve(&a, &b, &q, &r, 1, 1, 100, 1e-10).unwrap();
let u = lqr.control(&[1.0]);
assert!(u[0] < 0.0);
}
#[test]
fn test_kalman_predict_state_propagates() {
let mut kf = KalmanFilter::new(
vec![1.0],
vec![0.0],
vec![1.0],
vec![0.01],
vec![0.1],
1,
1,
1,
);
kf.x = vec![5.0];
kf.predict(&[0.0]);
assert!((kf.x[0] - 5.0).abs() < 1e-10);
}
#[test]
fn test_kalman_update_moves_toward_measurement() {
let mut kf = KalmanFilter::new(
vec![1.0],
vec![0.0],
vec![1.0],
vec![0.01],
vec![0.1],
1,
1,
1,
);
kf.x = vec![0.0];
kf.update(&[10.0]);
assert!(kf.x[0] > 0.0, "state should move toward measurement");
}
#[test]
fn test_kalman_step_reduces_error() {
let mut kf = KalmanFilter::new(
vec![1.0],
vec![0.0],
vec![1.0],
vec![0.001],
vec![0.001],
1,
1,
1,
);
kf.x = vec![0.0];
for _ in 0..50 {
kf.step(&[0.0], &[5.0]);
}
assert!(
(kf.x[0] - 5.0).abs() < 0.1,
"should converge to 5.0, got {}",
kf.x[0]
);
}
#[test]
fn test_kalman_covariance_decreases_on_update() {
let mut kf = KalmanFilter::new(
vec![1.0],
vec![0.0],
vec![1.0],
vec![0.01],
vec![0.1],
1,
1,
1,
);
let p_before = kf.p[0];
kf.predict(&[0.0]);
kf.update(&[1.0]);
let p_after = kf.p[0];
assert!(
p_after < p_before,
"covariance should decrease after update"
);
}
#[test]
fn test_ekf_linear_case_matches_kf() {
let mut ekf = ExtendedKalmanFilter::new(vec![0.01], vec![0.1], 1, 1);
ekf.x = vec![0.0];
ekf.predict(
|x, _u| vec![x[0]],
|_x, _u| vec![1.0], &[0.0],
);
ekf.update(
|x| vec![x[0]],
|_x| vec![1.0], &[5.0],
);
assert!(ekf.x[0] > 0.0, "state should move toward measurement");
}
#[test]
fn test_ekf_nonlinear_predict() {
let mut ekf = ExtendedKalmanFilter::new(vec![0.1], vec![0.5], 1, 1);
ekf.x = vec![1.0];
ekf.predict(
|x, _u| vec![x[0] * x[0]],
|x, _u| vec![2.0 * x[0]], &[0.0],
);
assert!((ekf.x[0] - 1.0).abs() < 1e-10);
}
#[test]
fn test_bode_first_order_dc_gain() {
let bode = BodeAnalysis::new(vec![1.0], vec![1.0, 1.0]);
let pt = bode.compute(&[0.001])[0];
assert!((pt.gain_db).abs() < 0.1, "DC gain should be ~0 dB");
}
#[test]
fn test_bode_first_order_rolloff() {
let bode = BodeAnalysis::new(vec![1.0], vec![1.0, 1.0]);
let pt = bode.compute(&[1.0])[0];
assert!(
(pt.gain_db + 3.01).abs() < 0.1,
"at corner freq, gain ~ -3 dB, got {}",
pt.gain_db
);
}
#[test]
fn test_bode_phase_first_order() {
let bode = BodeAnalysis::new(vec![1.0], vec![1.0, 1.0]);
let pt = bode.compute(&[1.0])[0];
assert!(
(pt.phase_deg + 45.0).abs() < 0.5,
"phase at corner freq should be ~-45°"
);
}
#[test]
fn test_bode_log_spacing() {
let bode = BodeAnalysis::new(vec![1.0], vec![1.0, 1.0]);
let pts = bode.compute_log(0.1, 100.0, 50);
assert_eq!(pts.len(), 50);
for i in 1..pts.len() {
assert!(pts[i].omega > pts[i - 1].omega);
}
}
#[test]
fn test_bode_margins_stable_system() {
let bode = BodeAnalysis::new(vec![1.0], vec![1.0, 3.0, 3.0, 1.0]);
let (gm, pm) = bode.margins(0.01, 100.0, 200);
assert!(
pm > 0.0 || pm.is_nan(),
"phase margin should be positive for stable system"
);
let _ = gm;
}
#[test]
fn test_root_locus_poles_count() {
let rl = RootLocus::new(vec![1.0], vec![1.0, 3.0, 2.0]);
let pts = rl.compute(&[0.0, 1.0, 4.0]);
for pt in &pts {
assert!(pt.poles.len() <= 2);
}
}
#[test]
fn test_root_locus_zero_gain_open_loop_poles() {
let rl = RootLocus::new(vec![1.0], vec![1.0, 3.0, 2.0]);
let pt = &rl.compute(&[0.0])[0];
let real_parts: Vec<f64> = pt.poles.iter().map(|&(re, _)| re).collect();
assert!(
real_parts.iter().any(|&re| (re + 1.0).abs() < 0.1),
"should have pole near -1"
);
assert!(
real_parts.iter().any(|&re| (re + 2.0).abs() < 0.1),
"should have pole near -2"
);
}
#[test]
fn test_root_locus_breakaway_search() {
let rl = RootLocus::new(vec![1.0], vec![1.0, 3.0, 2.0]);
let pts = rl.breakaway_points(-3.0, 0.0, 100);
if !pts.is_empty() {
assert!(pts[0] > -3.0 && pts[0] < 0.0);
}
}
#[test]
fn test_zn_pid_gains_positive() {
let zn = ZieglerNichols::new(2.0, 1.0);
let gains = zn.pid();
assert!(gains.kp > 0.0);
assert!(gains.ki > 0.0);
assert!(gains.kd > 0.0);
}
#[test]
fn test_zn_pi_no_derivative() {
let zn = ZieglerNichols::new(2.0, 1.0);
let gains = zn.pi();
assert_eq!(gains.kd, 0.0);
assert!(gains.ki > 0.0);
}
#[test]
fn test_zn_p_only() {
let zn = ZieglerNichols::new(2.0, 1.0);
let gains = zn.p_only();
assert_eq!(gains.ki, 0.0);
assert_eq!(gains.kd, 0.0);
assert!((gains.kp - 1.0).abs() < 1e-10); }
#[test]
fn test_zn_pid_kp_formula() {
let ku = 3.0;
let tu = 2.0;
let zn = ZieglerNichols::new(ku, tu);
let gains = zn.pid();
assert!((gains.kp - 0.6 * ku).abs() < 1e-10);
}
#[test]
fn test_zn_simc_gains() {
let zn = ZieglerNichols::new(4.0, 2.0);
let gains = zn.simc();
assert!(gains.kp > 0.0);
assert!(gains.ki > 0.0);
assert!(gains.kd > 0.0);
}
#[test]
fn test_lead_compensator_dc_gain() {
let comp = LeadLagCompensator::new(1.0, 2.0, 10.0);
let dc = comp.dc_gain();
assert!((dc - 0.2).abs() < 1e-10);
}
#[test]
fn test_lead_compensator_hf_gain() {
let comp = LeadLagCompensator::new(2.0, 1.0, 10.0);
assert!((comp.hf_gain() - 2.0).abs() < 1e-10);
}
#[test]
fn test_lead_compensator_phase_at_max_freq() {
let comp = LeadLagCompensator::new(1.0, 1.0, 10.0);
let (omega_max, phi_max) = comp.max_phase_lead();
assert!((omega_max - (10.0f64).sqrt()).abs() < 1e-10);
assert!(phi_max > 0.0, "lead compensator should add phase");
}
#[test]
fn test_lead_compensator_bode_points() {
let comp = LeadLagCompensator::new(1.0, 1.0, 10.0);
let pts = comp.bode(0.1, 100.0, 20);
assert_eq!(pts.len(), 20);
for pt in &pts {
assert!(pt.omega > 0.0);
}
}
#[test]
fn test_lag_compensator_dc_gain() {
let comp = LeadLagCompensator::new(1.0, 10.0, 1.0);
let dc = comp.dc_gain();
assert!((dc - 10.0).abs() < 1e-10);
}
#[test]
fn test_poles_zeros_dc_gain_simple() {
let pz = PolesZeros::new(1.0, vec![], vec![(-1.0, 0.0)]);
let dc = pz.dc_gain();
assert!((dc - 1.0).abs() < 1e-10);
}
#[test]
fn test_poles_zeros_stability_stable() {
let pz = PolesZeros::new(1.0, vec![], vec![(-1.0, 0.0), (-2.0, 0.0)]);
assert!(pz.is_stable_continuous());
}
#[test]
fn test_poles_zeros_stability_unstable() {
let pz = PolesZeros::new(1.0, vec![], vec![(1.0, 0.0)]);
assert!(!pz.is_stable_continuous());
}
#[test]
fn test_poles_zeros_discrete_stability() {
let pz = PolesZeros::new(1.0, vec![], vec![(0.5, 0.0), (0.3, 0.0)]);
assert!(pz.is_stable_discrete());
}
#[test]
fn test_poles_zeros_eval_at() {
let pz = PolesZeros::new(2.0, vec![], vec![(-1.0, 0.0)]);
let (mag, _phase) = pz.eval_at(0.001); assert!(
(mag - 2.0).abs() < 0.01,
"DC gain should be ~2.0, got {}",
mag
);
}
#[test]
fn test_poles_zeros_step_response_length() {
let pz = PolesZeros::new(1.0, vec![], vec![(-1.0, 0.0)]);
let resp = pz.step_response_approx(0.1, 10);
assert_eq!(resp.len(), 10);
}
#[test]
fn test_poles_zeros_no_poles_const_output() {
let pz = PolesZeros::new(3.0, vec![], vec![]);
let resp = pz.step_response_approx(0.1, 5);
for (_, y) in &resp {
assert!((y - 3.0).abs() < 1e-10);
}
}
#[test]
fn test_mat_mul_identity() {
let i2 = mat_eye(2);
let a = vec![1.0, 2.0, 3.0, 4.0];
let result = mat_mul(&i2, &a, 2, 2, 2);
for (r, e) in result.iter().zip(a.iter()) {
assert!((r - e).abs() < 1e-10);
}
}
#[test]
fn test_mat_inv2_basic() {
let a = vec![2.0, 1.0, 1.0, 1.0]; let inv = mat_inv2(&a).unwrap();
assert!((inv[0] - 1.0).abs() < 1e-10);
assert!((inv[3] - 2.0).abs() < 1e-10);
}
#[test]
fn test_mat_inv_singular_returns_none() {
let singular = vec![1.0, 2.0, 2.0, 4.0]; assert!(mat_inv(&singular, 2).is_none());
}
#[test]
fn test_spectral_radius_stable_matrix() {
let a = vec![0.5, 0.0, 0.0, 0.5];
let r = spectral_radius_power_iter(&a, 2, 100, 1e-8);
assert!((r - 0.5).abs() < 0.01, "spectral radius should be ~0.5");
}
#[test]
fn test_cplx_basic_ops() {
let a = Cplx::new(3.0, 4.0);
assert!((a.abs() - 5.0).abs() < 1e-10);
let b = Cplx::new(1.0, 2.0);
let prod = a.mul(b);
assert!((prod.re - (3.0 - 8.0)).abs() < 1e-10); assert!((prod.im - (4.0 + 6.0)).abs() < 1e-10); }
}