Skip to main content

oxiphysics_core/control_systems/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6#[allow(unused_imports)]
7use std::f64::consts::PI;
8
9use super::types::{Complex, PidController, StateSpaceModel, TransferFunction};
10
11/// Evaluate a polynomial with coefficients `[a_n, a_{n-1}, ..., a_0]` at complex z.
12pub fn poly_eval_complex(coeffs: &[f64], z: Complex) -> Complex {
13    let mut result = Complex::new(0.0, 0.0);
14    for &c in coeffs {
15        result = result.mul(&z).add(&Complex::new(c, 0.0));
16    }
17    result
18}
19/// Evaluate polynomial at real x using Horner's method.
20pub fn poly_eval(coeffs: &[f64], x: f64) -> f64 {
21    let mut r = 0.0;
22    for &c in coeffs {
23        r = r * x + c;
24    }
25    r
26}
27/// Multiply two polynomials (convolution of coefficient arrays).
28pub fn poly_mul(a: &[f64], b: &[f64]) -> Vec<f64> {
29    if a.is_empty() || b.is_empty() {
30        return vec![];
31    }
32    let n = a.len() + b.len() - 1;
33    let mut result = vec![0.0; n];
34    for (i, &ai) in a.iter().enumerate() {
35        for (j, &bj) in b.iter().enumerate() {
36            result[i + j] += ai * bj;
37        }
38    }
39    result
40}
41/// Add two polynomials (may differ in length).
42pub fn poly_add(a: &[f64], b: &[f64]) -> Vec<f64> {
43    let n = a.len().max(b.len());
44    let mut result = vec![0.0; n];
45    let offset_a = n - a.len();
46    let offset_b = n - b.len();
47    for (i, &v) in a.iter().enumerate() {
48        result[i + offset_a] += v;
49    }
50    for (i, &v) in b.iter().enumerate() {
51        result[i + offset_b] += v;
52    }
53    result
54}
55/// Derivative of polynomial coefficients `[a_n, ..., a_0]`.
56pub fn poly_deriv(coeffs: &[f64]) -> Vec<f64> {
57    if coeffs.len() <= 1 {
58        return vec![0.0];
59    }
60    let deg = coeffs.len() - 1;
61    let mut d = Vec::with_capacity(deg);
62    for (i, &c) in coeffs.iter().enumerate().take(deg) {
63        d.push(c * (deg - i) as f64);
64    }
65    d
66}
67/// Find polynomial roots using Durand-Kerner method.
68///
69/// Returns approximate complex roots for polynomials of any degree.
70#[allow(dead_code)]
71pub fn poly_roots(coeffs: &[f64]) -> Vec<Complex> {
72    let mut start = 0;
73    while start < coeffs.len() && coeffs[start].abs() < 1e-300 {
74        start += 1;
75    }
76    let coeffs = &coeffs[start..];
77    if coeffs.len() <= 1 {
78        return vec![];
79    }
80    let n = coeffs.len() - 1;
81    if n == 0 {
82        return vec![];
83    }
84    let lead = coeffs[0];
85    if lead.abs() < 1e-300 {
86        return vec![];
87    }
88    let norm: Vec<f64> = coeffs.iter().map(|&c| c / lead).collect();
89    if n == 1 {
90        return vec![Complex::new(-norm[1], 0.0)];
91    }
92    if n == 2 {
93        let a = 1.0;
94        let b = norm[1];
95        let c = norm[2];
96        let disc = b * b - 4.0 * a * c;
97        if disc >= 0.0 {
98            let sq = disc.sqrt();
99            return vec![
100                Complex::new((-b + sq) / (2.0 * a), 0.0),
101                Complex::new((-b - sq) / (2.0 * a), 0.0),
102            ];
103        } else {
104            let sq = (-disc).sqrt();
105            return vec![
106                Complex::new(-b / (2.0 * a), sq / (2.0 * a)),
107                Complex::new(-b / (2.0 * a), -sq / (2.0 * a)),
108            ];
109        }
110    }
111    let mut roots: Vec<Complex> = (0..n)
112        .map(|k| {
113            let angle = 2.0 * PI * k as f64 / n as f64 + 0.3;
114            Complex::from_polar(0.5 + 0.1 * k as f64, angle)
115        })
116        .collect();
117    for _iter in 0..200 {
118        let mut max_delta = 0.0_f64;
119        for i in 0..n {
120            let mut denom = Complex::new(1.0, 0.0);
121            for j in 0..n {
122                if j != i {
123                    denom = denom.mul(&roots[i].sub(&roots[j]));
124                }
125            }
126            let val = poly_eval_complex(&norm, roots[i]);
127            if denom.norm_sq() < 1e-300 {
128                continue;
129            }
130            let delta = val.div(&denom);
131            roots[i] = roots[i].sub(&delta);
132            max_delta = max_delta.max(delta.abs());
133        }
134        if max_delta < 1e-12 {
135            break;
136        }
137    }
138    roots
139}
140/// Routh-Hurwitz stability test.
141///
142/// Returns `true` if all roots of the polynomial have negative real parts.
143/// The polynomial is given as `[a_n, a_{n-1}, ..., a_0]` (descending order).
144pub fn routh_hurwitz_stable(coeffs: &[f64]) -> bool {
145    if coeffs.is_empty() {
146        return true;
147    }
148    let n = coeffs.len();
149    if n == 1 {
150        return coeffs[0] > 0.0;
151    }
152    let sign = coeffs[0].signum();
153    for &c in coeffs {
154        if c * sign <= 0.0 {
155            return false;
156        }
157    }
158    let rows = n;
159    let cols = n.div_ceil(2);
160    let mut table = vec![vec![0.0; cols]; rows];
161    for j in 0..cols {
162        if 2 * j < n {
163            table[0][j] = coeffs[2 * j];
164        }
165        if 2 * j + 1 < n {
166            table[1][j] = coeffs[2 * j + 1];
167        }
168    }
169    for i in 2..rows {
170        let pivot = table[i - 1][0];
171        if pivot.abs() < 1e-15 {
172            return false;
173        }
174        for j in 0..cols - 1 {
175            let a = table[i - 2][j + 1];
176            let b = if j + 1 < cols {
177                table[i - 1][j + 1]
178            } else {
179                0.0
180            };
181            table[i][j] = (pivot * a - table[i - 2][0] * b) / pivot;
182        }
183    }
184    let first_sign = table[0][0].signum();
185    for row in &table {
186        if row[0] * first_sign < 0.0 {
187            return false;
188        }
189        if row[0].abs() < 1e-15 {
190            return false;
191        }
192    }
193    true
194}
195/// Advance state by one Euler step: x_{k+1} = x_k + dt*(A*x_k + B*u_k).
196pub fn ss_step(model: &StateSpaceModel, state: &[f64], input: &[f64], dt: f64) -> Vec<f64> {
197    let n = model.state_dim();
198    let mut dx = vec![0.0; n];
199    for i in 0..n {
200        for j in 0..n {
201            dx[i] += model.a[i][j] * state[j];
202        }
203        for (j, &u) in input.iter().enumerate() {
204            if j < model.b[i].len() {
205                dx[i] += model.b[i][j] * u;
206            }
207        }
208    }
209    let mut new_state = vec![0.0; n];
210    for i in 0..n {
211        new_state[i] = state[i] + dt * dx[i];
212    }
213    new_state
214}
215/// Compute output: y = C*x + D*u.
216pub fn ss_output(model: &StateSpaceModel, state: &[f64], input: &[f64]) -> Vec<f64> {
217    let p = model.output_dim();
218    let mut y = vec![0.0; p];
219    for i in 0..p {
220        for (j, &s) in state.iter().enumerate() {
221            if j < model.c[i].len() {
222                y[i] += model.c[i][j] * s;
223            }
224        }
225        for (j, &u) in input.iter().enumerate() {
226            if j < model.d[i].len() {
227                y[i] += model.d[i][j] * u;
228            }
229        }
230    }
231    y
232}
233/// Simulate state-space model over time steps, returning state history.
234pub fn ss_simulate(
235    model: &StateSpaceModel,
236    x0: &[f64],
237    inputs: &[Vec<f64>],
238    dt: f64,
239) -> Vec<Vec<f64>> {
240    let mut history = Vec::with_capacity(inputs.len() + 1);
241    let mut x = x0.to_vec();
242    history.push(x.clone());
243    for u in inputs {
244        x = ss_step(model, &x, u, dt);
245        history.push(x.clone());
246    }
247    history
248}
249/// Update PID controller and return the control output.
250///
251/// `setpoint` is the desired value, `measured` is the process variable,
252/// `dt` is the time step.
253pub fn pid_update(ctrl: &mut PidController, setpoint: f64, measured: f64, dt: f64) -> f64 {
254    let error = setpoint - measured;
255    let p_term = ctrl.kp * (ctrl.setpoint_weight_b * setpoint - measured);
256    let anti_windup_correction = ctrl.anti_windup_gain
257        * (ctrl.prev_unclipped - ctrl.prev_unclipped.clamp(ctrl.out_min, ctrl.out_max));
258    ctrl.integral += error * dt - anti_windup_correction * dt;
259    let i_term = ctrl.ki * ctrl.integral;
260    let deriv_input = ctrl.setpoint_weight_c * setpoint - measured;
261    let raw_deriv = if dt > 0.0 {
262        (deriv_input - ctrl.prev_error) / dt
263    } else {
264        0.0
265    };
266    let alpha = ctrl.deriv_filter_coeff;
267    let filtered_deriv = alpha * ctrl.prev_deriv_filtered + (1.0 - alpha) * raw_deriv;
268    let d_term = ctrl.kd * filtered_deriv;
269    ctrl.prev_error = deriv_input;
270    ctrl.prev_deriv_filtered = filtered_deriv;
271    let output_unclipped = p_term + i_term + d_term;
272    ctrl.prev_unclipped = output_unclipped;
273    output_unclipped.clamp(ctrl.out_min, ctrl.out_max)
274}
275/// Reset PID controller state.
276pub fn pid_reset(ctrl: &mut PidController) {
277    ctrl.integral = 0.0;
278    ctrl.prev_error = 0.0;
279    ctrl.prev_deriv_filtered = 0.0;
280    ctrl.prev_unclipped = 0.0;
281}
282/// Generate logarithmically spaced frequencies.
283pub fn logspace_omega(start: f64, end: f64, n: usize) -> Vec<f64> {
284    if n <= 1 {
285        return vec![start];
286    }
287    let log_start = start.ln();
288    let log_end = end.ln();
289    (0..n)
290        .map(|i| {
291            let frac = i as f64 / (n - 1) as f64;
292            (log_start + frac * (log_end - log_start)).exp()
293        })
294        .collect()
295}
296/// Bode plot data: returns (frequencies, magnitude_dB, phase_deg).
297pub fn bode_data(
298    tf: &TransferFunction,
299    omega_start: f64,
300    omega_end: f64,
301    n_points: usize,
302) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
303    let omegas = logspace_omega(omega_start, omega_end, n_points);
304    let mut mag_db = Vec::with_capacity(n_points);
305    let mut phase_deg = Vec::with_capacity(n_points);
306    for &w in &omegas {
307        let h = tf.eval_jw(w);
308        let m = h.abs();
309        mag_db.push(20.0 * m.max(1e-300).log10());
310        phase_deg.push(h.arg() * 180.0 / PI);
311    }
312    (omegas, mag_db, phase_deg)
313}
314/// Compute gain margin and phase margin from Bode data.
315///
316/// Returns `(gain_margin_dB, phase_margin_deg)`.
317pub fn stability_margins(
318    tf: &TransferFunction,
319    omega_start: f64,
320    omega_end: f64,
321    n_points: usize,
322) -> (f64, f64) {
323    let omegas = logspace_omega(omega_start, omega_end, n_points);
324    let mut gain_margin_db = f64::INFINITY;
325    let mut phase_margin_deg = f64::INFINITY;
326    for &w in &omegas {
327        let h = tf.eval_jw(w);
328        let mag = h.abs();
329        let phase = h.arg() * 180.0 / PI;
330        if (mag - 1.0).abs() < 0.05 {
331            let pm = 180.0 + phase;
332            if pm.abs() < phase_margin_deg.abs() {
333                phase_margin_deg = pm;
334            }
335        }
336        if (phase + 180.0).abs() < 5.0 {
337            let gm = -20.0 * mag.max(1e-300).log10();
338            if gm.abs() < gain_margin_db.abs() {
339                gain_margin_db = gm;
340            }
341        }
342    }
343    (gain_margin_db, phase_margin_deg)
344}
345/// Nyquist plot data: returns (real_parts, imag_parts).
346pub fn nyquist_data(
347    tf: &TransferFunction,
348    omega_start: f64,
349    omega_end: f64,
350    n_points: usize,
351) -> (Vec<f64>, Vec<f64>) {
352    let omegas = logspace_omega(omega_start, omega_end, n_points);
353    let mut re_parts = Vec::with_capacity(n_points);
354    let mut im_parts = Vec::with_capacity(n_points);
355    for &w in &omegas {
356        let h = tf.eval_jw(w);
357        re_parts.push(h.re);
358        im_parts.push(h.im);
359    }
360    (re_parts, im_parts)
361}
362/// Check Nyquist stability: counts encirclements of -1+0j.
363///
364/// Returns the number of clockwise encirclements.
365pub fn nyquist_encirclements(
366    tf: &TransferFunction,
367    omega_start: f64,
368    omega_end: f64,
369    n_points: usize,
370) -> i32 {
371    let omegas = logspace_omega(omega_start, omega_end, n_points);
372    let mut total_angle = 0.0;
373    let mut prev_angle = 0.0_f64;
374    for (k, &w) in omegas.iter().enumerate() {
375        let h = tf.eval_jw(w);
376        let shifted = Complex::new(h.re + 1.0, h.im);
377        let angle = shifted.arg();
378        if k > 0 {
379            let mut d = angle - prev_angle;
380            if d > PI {
381                d -= 2.0 * PI;
382            }
383            if d < -PI {
384                d += 2.0 * PI;
385            }
386            total_angle += d;
387        }
388        prev_angle = angle;
389    }
390    (total_angle / (2.0 * PI)).round() as i32
391}
392/// Compute root locus: poles of 1 + K*H(s) for varying gain K.
393///
394/// Returns a vector of (gain, poles) pairs.
395pub fn root_locus(tf: &TransferFunction, gains: &[f64]) -> Vec<(f64, Vec<Complex>)> {
396    let mut results = Vec::with_capacity(gains.len());
397    for &k in gains {
398        let scaled_num: Vec<f64> = tf.num.iter().map(|&c| c * k).collect();
399        let cl_poly = poly_add(&tf.den, &scaled_num);
400        let poles = poly_roots(&cl_poly);
401        results.push((k, poles));
402    }
403    results
404}
405/// Find the break-away/break-in points on the real axis for root locus.
406///
407/// These are points where dK/ds = 0 on the real axis.
408pub fn root_locus_breakpoints(tf: &TransferFunction) -> Vec<f64> {
409    let num_d = poly_deriv(&tf.num);
410    let den_d = poly_deriv(&tf.den);
411    let p1 = poly_mul(&num_d, &tf.den);
412    let p2 = poly_mul(&tf.num, &den_d);
413    let neg_p2: Vec<f64> = p2.iter().map(|&c| -c).collect();
414    let result_poly = poly_add(&p1, &neg_p2);
415    let roots = poly_roots(&result_poly);
416    roots
417        .iter()
418        .filter(|r| r.im.abs() < 1e-6)
419        .map(|r| r.re)
420        .collect()
421}
422/// Discretize a state-space model using Zero-Order Hold (ZOH).
423///
424/// Approximation: Ad = I + A*T + (A*T)^2/2, Bd = (I*T + A*T^2/2)*B
425/// where T is the sampling period.
426pub fn discretize_zoh(model: &StateSpaceModel, dt: f64) -> StateSpaceModel {
427    let n = model.state_dim();
428    let m = model.input_dim();
429    let mut ad = vec![vec![0.0; n]; n];
430    let mut a2 = vec![vec![0.0; n]; n];
431    for i in 0..n {
432        for j in 0..n {
433            for k in 0..n {
434                a2[i][j] += model.a[i][k] * model.a[k][j];
435            }
436        }
437    }
438    for i in 0..n {
439        for j in 0..n {
440            ad[i][j] = model.a[i][j] * dt + a2[i][j] * dt * dt / 2.0;
441            if i == j {
442                ad[i][j] += 1.0;
443            }
444        }
445    }
446    let mut bd = vec![vec![0.0; m]; n];
447    for i in 0..n {
448        for j in 0..m {
449            let mut val = dt * model.b[i][j];
450            for k in 0..n {
451                val += model.a[i][k] * model.b[k][j] * dt * dt / 2.0;
452            }
453            bd[i][j] = val;
454        }
455    }
456    StateSpaceModel::new(ad, bd, model.c.clone(), model.d.clone())
457}
458/// Discretize a transfer function using Tustin (bilinear) transform.
459///
460/// Substitutes s = (2/T)*(z-1)/(z+1) into H(s).
461/// Returns discrete-time transfer function H(z).
462pub fn discretize_tustin(tf: &TransferFunction, dt: f64) -> TransferFunction {
463    let n_num = tf.num.len();
464    let n_den = tf.den.len();
465    let order = (n_num.max(n_den)).saturating_sub(1);
466    let c = 2.0 / dt;
467    let z_minus_1 = vec![1.0, -1.0];
468    let z_plus_1 = vec![1.0, 1.0];
469    let mut pow_zm1 = vec![vec![1.0]; order + 1];
470    let mut pow_zp1 = vec![vec![1.0]; order + 1];
471    for k in 1..=order {
472        pow_zm1[k] = poly_mul(&pow_zm1[k - 1], &z_minus_1);
473        pow_zp1[k] = poly_mul(&pow_zp1[k - 1], &z_plus_1);
474    }
475    let mut num_z = vec![0.0; order + 1];
476    for (i, &coeff) in tf.num.iter().rev().enumerate() {
477        let ck = c.powi(i as i32);
478        let term = poly_mul(&pow_zm1[i], &pow_zp1[order - i]);
479        let expanded = term.iter().map(|&t| t * coeff * ck).collect::<Vec<_>>();
480        if expanded.len() > num_z.len() {
481            num_z.resize(expanded.len(), 0.0);
482        }
483        let offset = num_z.len() - expanded.len();
484        for (j, &v) in expanded.iter().enumerate() {
485            num_z[j + offset] += v;
486        }
487    }
488    let mut den_z = vec![0.0; order + 1];
489    for (i, &coeff) in tf.den.iter().rev().enumerate() {
490        let ck = c.powi(i as i32);
491        let term = poly_mul(&pow_zm1[i], &pow_zp1[order - i]);
492        let expanded = term.iter().map(|&t| t * coeff * ck).collect::<Vec<_>>();
493        if expanded.len() > den_z.len() {
494            den_z.resize(expanded.len(), 0.0);
495        }
496        let offset = den_z.len() - expanded.len();
497        for (j, &v) in expanded.iter().enumerate() {
498            den_z[j + offset] += v;
499        }
500    }
501    let lead = den_z[0];
502    if lead.abs() > 1e-300 {
503        for v in &mut num_z {
504            *v /= lead;
505        }
506        for v in &mut den_z {
507            *v /= lead;
508        }
509    }
510    TransferFunction::new(num_z, den_z)
511}
512/// Multiply n-by-k matrix A by k-by-m matrix B, returning n-by-m.
513#[allow(dead_code)]
514pub fn mat_mul(a: &[f64], b: &[f64], n: usize, k: usize, m: usize) -> Vec<f64> {
515    let mut c = vec![0.0; n * m];
516    for i in 0..n {
517        for j in 0..m {
518            for l in 0..k {
519                c[i * m + j] += a[i * k + l] * b[l * m + j];
520            }
521        }
522    }
523    c
524}
525/// Transpose n-by-m matrix to m-by-n.
526#[allow(dead_code)]
527pub fn mat_transpose(a: &[f64], n: usize, m: usize) -> Vec<f64> {
528    let mut t = vec![0.0; n * m];
529    for i in 0..n {
530        for j in 0..m {
531            t[j * n + i] = a[i * m + j];
532        }
533    }
534    t
535}
536/// Identity matrix of size n.
537#[allow(dead_code)]
538pub fn mat_eye(n: usize) -> Vec<f64> {
539    let mut m = vec![0.0; n * n];
540    for i in 0..n {
541        m[i * n + i] = 1.0;
542    }
543    m
544}
545/// Add two n-by-m matrices.
546#[allow(dead_code)]
547pub fn mat_add(a: &[f64], b: &[f64]) -> Vec<f64> {
548    a.iter().zip(b.iter()).map(|(&x, &y)| x + y).collect()
549}
550/// Subtract two n-by-m matrices: a - b.
551#[allow(dead_code)]
552pub fn mat_sub(a: &[f64], b: &[f64]) -> Vec<f64> {
553    a.iter().zip(b.iter()).map(|(&x, &y)| x - y).collect()
554}
555/// Scale matrix by scalar.
556#[allow(dead_code)]
557pub fn mat_scale(a: &[f64], s: f64) -> Vec<f64> {
558    a.iter().map(|&x| x * s).collect()
559}
560/// Invert a small matrix (up to 3x3) using cofactors.
561pub fn mat_inv_small(a: &[f64], n: usize) -> Vec<f64> {
562    if n == 1 {
563        if a[0].abs() < 1e-300 {
564            return vec![0.0];
565        }
566        return vec![1.0 / a[0]];
567    }
568    if n == 2 {
569        let det = a[0] * a[3] - a[1] * a[2];
570        if det.abs() < 1e-300 {
571            return vec![0.0; 4];
572        }
573        return vec![a[3] / det, -a[1] / det, -a[2] / det, a[0] / det];
574    }
575    if n == 3 {
576        let det = a[0] * (a[4] * a[8] - a[5] * a[7]) - a[1] * (a[3] * a[8] - a[5] * a[6])
577            + a[2] * (a[3] * a[7] - a[4] * a[6]);
578        if det.abs() < 1e-300 {
579            return vec![0.0; 9];
580        }
581        let inv_det = 1.0 / det;
582        return vec![
583            (a[4] * a[8] - a[5] * a[7]) * inv_det,
584            (a[2] * a[7] - a[1] * a[8]) * inv_det,
585            (a[1] * a[5] - a[2] * a[4]) * inv_det,
586            (a[5] * a[6] - a[3] * a[8]) * inv_det,
587            (a[0] * a[8] - a[2] * a[6]) * inv_det,
588            (a[2] * a[3] - a[0] * a[5]) * inv_det,
589            (a[3] * a[7] - a[4] * a[6]) * inv_det,
590            (a[1] * a[6] - a[0] * a[7]) * inv_det,
591            (a[0] * a[4] - a[1] * a[3]) * inv_det,
592        ];
593    }
594    let mut aug = vec![0.0; n * 2 * n];
595    for i in 0..n {
596        for j in 0..n {
597            aug[i * 2 * n + j] = a[i * n + j];
598        }
599        aug[i * 2 * n + n + i] = 1.0;
600    }
601    for col in 0..n {
602        let mut max_row = col;
603        let mut max_val = aug[col * 2 * n + col].abs();
604        for row in (col + 1)..n {
605            let v = aug[row * 2 * n + col].abs();
606            if v > max_val {
607                max_val = v;
608                max_row = row;
609            }
610        }
611        if max_val < 1e-300 {
612            return vec![0.0; n * n];
613        }
614        if max_row != col {
615            for j in 0..(2 * n) {
616                aug.swap(col * 2 * n + j, max_row * 2 * n + j);
617            }
618        }
619        let pivot = aug[col * 2 * n + col];
620        for j in 0..(2 * n) {
621            aug[col * 2 * n + j] /= pivot;
622        }
623        for row in 0..n {
624            if row != col {
625                let factor = aug[row * 2 * n + col];
626                for j in 0..(2 * n) {
627                    let v = aug[col * 2 * n + j];
628                    aug[row * 2 * n + j] -= factor * v;
629                }
630            }
631        }
632    }
633    let mut inv = vec![0.0; n * n];
634    for i in 0..n {
635        for j in 0..n {
636            inv[i * n + j] = aug[i * 2 * n + n + j];
637        }
638    }
639    inv
640}
641/// Compute the controllability matrix \[B, AB, A^2B, ...\].
642///
643/// `a` is n-by-n, `b` is n-by-m (flat row-major). Returns n-by-(n*m).
644pub fn controllability_matrix(a: &[f64], b: &[f64], n: usize, m: usize) -> Vec<f64> {
645    let mut result = Vec::with_capacity(n * n * m);
646    let mut ab = b.to_vec();
647    for _k in 0..n {
648        result.extend_from_slice(&ab);
649        ab = mat_mul(a, &ab, n, n, m);
650    }
651    let cols = n * m;
652    let mut matrix = vec![0.0; n * cols];
653    for k in 0..n {
654        for i in 0..n {
655            for j in 0..m {
656                matrix[i * cols + k * m + j] = result[k * n * m + i * m + j];
657            }
658        }
659    }
660    matrix
661}
662/// Compute the observability matrix \[C; CA; CA^2; ...\].
663///
664/// `a` is n-by-n, `c` is p-by-n (flat row-major). Returns (n*p)-by-n.
665pub fn observability_matrix(a: &[f64], c: &[f64], n: usize, p: usize) -> Vec<f64> {
666    let rows = n * p;
667    let mut result = vec![0.0; rows * n];
668    let mut ca = c.to_vec();
669    for k in 0..n {
670        for i in 0..p {
671            for j in 0..n {
672                result[(k * p + i) * n + j] = ca[i * n + j];
673            }
674        }
675        ca = mat_mul(&ca, a, p, n, n);
676    }
677    result
678}
679/// Estimate the rank of an n-by-m matrix via column pivoting.
680pub fn matrix_rank(mat: &[f64], n: usize, m: usize) -> usize {
681    let mut work = mat.to_vec();
682    let min_dim = n.min(m);
683    let mut rank = 0;
684    for col in 0..min_dim {
685        let mut max_val = 0.0_f64;
686        let mut max_row = col;
687        for row in col..n {
688            let v = work[row * m + col].abs();
689            if v > max_val {
690                max_val = v;
691                max_row = row;
692            }
693        }
694        if max_val < 1e-10 {
695            continue;
696        }
697        rank += 1;
698        if max_row != col {
699            for j in 0..m {
700                work.swap(col * m + j, max_row * m + j);
701            }
702        }
703        let pivot = work[col * m + col];
704        for row in (col + 1)..n {
705            let factor = work[row * m + col] / pivot;
706            for j in col..m {
707                let v = work[col * m + j];
708                work[row * m + j] -= factor * v;
709            }
710        }
711    }
712    rank
713}
714/// Check if the system is controllable (rank of controllability matrix == n).
715pub fn is_controllable(a: &[f64], b: &[f64], n: usize, m: usize) -> bool {
716    let cm = controllability_matrix(a, b, n, m);
717    matrix_rank(&cm, n, n * m) == n
718}
719/// Check if the system is observable (rank of observability matrix == n).
720pub fn is_observable(a: &[f64], c: &[f64], n: usize, p: usize) -> bool {
721    let om = observability_matrix(a, c, n, p);
722    matrix_rank(&om, n * p, n) == n
723}
724/// Compute the LQR cost: J = x'Qx + u'Ru.
725pub fn lqr_cost(x: &[f64], u: &[f64], q: &[Vec<f64>], r: &[Vec<f64>]) -> f64 {
726    let n = x.len();
727    let m = u.len();
728    let mut cost = 0.0;
729    for i in 0..n {
730        for j in 0..n {
731            cost += x[i] * q[i][j] * x[j];
732        }
733    }
734    for i in 0..m {
735        for j in 0..m {
736            cost += u[i] * r[i][j] * u[j];
737        }
738    }
739    cost
740}
741/// Design a lead compensator for a desired phase margin boost at crossover frequency.
742///
743/// Returns `(zero_freq, pole_freq)` for the compensator C(s) = (s + z) / (s + p).
744pub fn lead_compensator(crossover_freq: f64, phase_margin_deg: f64) -> (f64, f64) {
745    let phi_max = phase_margin_deg * PI / 180.0;
746    let sin_phi = phi_max.sin();
747    let alpha = ((1.0 - sin_phi) / (1.0 + sin_phi)).max(1e-10);
748    let wm = crossover_freq;
749    let zero = wm * alpha.sqrt();
750    let pole = wm / alpha.sqrt();
751    (zero, pole)
752}
753/// Design a lag compensator for steady-state error reduction.
754///
755/// Returns `(zero_freq, pole_freq)`.
756pub fn lag_compensator(crossover_freq: f64, dc_gain_boost: f64) -> (f64, f64) {
757    let beta = dc_gain_boost.max(1.0);
758    let wm = crossover_freq;
759    let zero = wm / 10.0;
760    let pole = zero / beta;
761    (zero, pole)
762}
763/// Observer gain via Ackermann's formula (works for 1-D systems).
764///
765/// For higher dimensions, returns a heuristic gain vector.
766pub fn observer_gain_ackermann(
767    a: &[Vec<f64>],
768    c: &[Vec<f64>],
769    desired_poles: &[[f64; 2]],
770) -> Vec<f64> {
771    let n = a.len();
772    if n == 0 {
773        return vec![];
774    }
775    if n == 1 {
776        let a_val = a[0][0];
777        let c_val = if !c.is_empty() && !c[0].is_empty() {
778            c[0][0]
779        } else {
780            1.0
781        };
782        let desired_re = if !desired_poles.is_empty() {
783            desired_poles[0][0]
784        } else {
785            -1.0
786        };
787        if c_val.abs() < 1e-300 {
788            return vec![0.0];
789        }
790        return vec![(a_val - desired_re) / c_val];
791    }
792    let mut l = vec![0.0; n];
793    for i in 0..n {
794        let desired_re = if i < desired_poles.len() {
795            desired_poles[i][0]
796        } else {
797            -1.0
798        };
799        l[i] = a[i][i] - desired_re;
800    }
801    l
802}
803/// Check if all poles have negative real part (stable).
804pub fn pole_placement_check(eigs: &[[f64; 2]]) -> bool {
805    eigs.iter().all(|e| e[0] < 0.0)
806}
807/// Approximate the H-infinity norm (peak of |H(jw)|) over given frequencies.
808pub fn h_infinity_norm_bound(tf: &TransferFunction, omegas: &[f64]) -> f64 {
809    let mut max_mag = 0.0_f64;
810    for &w in omegas {
811        let h = tf.eval_jw(w);
812        max_mag = max_mag.max(h.abs());
813    }
814    max_mag
815}
816/// Ziegler-Nichols PID tuning from ultimate gain and period.
817///
818/// Returns `(Kp, Ki, Kd)`.
819pub fn ziegler_nichols_pid(ku: f64, tu: f64) -> (f64, f64, f64) {
820    let kp = 0.6 * ku;
821    let ki = 2.0 * kp / tu;
822    let kd = kp * tu / 8.0;
823    (kp, ki, kd)
824}
825/// Cohen-Coon PID tuning from plant parameters.
826///
827/// `k` is the static gain, `tau` is the time constant, `theta` is the dead time.
828/// Returns `(Kp, Ki, Kd)`.
829pub fn cohen_coon_pid(k: f64, tau: f64, theta: f64) -> (f64, f64, f64) {
830    let r = theta / tau;
831    let kp = (1.0 / k) * (tau / theta) * (1.33 + r / 4.0);
832    let ti = theta * (32.0 + 6.0 * r) / (13.0 + 8.0 * r);
833    let td = theta * 4.0 / (11.0 + 2.0 * r);
834    let ki = kp / ti;
835    let kd = kp * td;
836    (kp, ki, kd)
837}
838/// Check Lyapunov stability for a 2x2 system by examining eigenvalues of A.
839///
840/// Returns `true` if all eigenvalues have negative real part.
841pub fn lyapunov_stable_2x2(a: &[f64; 4]) -> bool {
842    let tr = a[0] + a[3];
843    let det = a[0] * a[3] - a[1] * a[2];
844    let disc = tr * tr - 4.0 * det;
845    if disc >= 0.0 {
846        let sq = disc.sqrt();
847        let l1 = (tr + sq) / 2.0;
848        let l2 = (tr - sq) / 2.0;
849        l1 < 0.0 && l2 < 0.0
850    } else {
851        tr < 0.0
852    }
853}
854/// Solve continuous Lyapunov equation A*P + P*A' + Q = 0 for 1x1 system.
855///
856/// Returns P\[0,0\].
857pub fn solve_lyapunov_1x1(a: f64, q: f64) -> f64 {
858    if a.abs() < 1e-300 {
859        return 0.0;
860    }
861    -q / (2.0 * a)
862}
863/// Step response of a transfer function (via state-space simulation).
864///
865/// Returns `(time_vec, output_vec)`.
866pub fn step_response(tf: &TransferFunction, t_final: f64, dt: f64) -> (Vec<f64>, Vec<f64>) {
867    let n_steps = (t_final / dt).ceil() as usize;
868    let mut times = Vec::with_capacity(n_steps);
869    let mut outputs = Vec::with_capacity(n_steps);
870    for k in 0..n_steps {
871        let t = k as f64 * dt;
872        times.push(t);
873        let h0 = tf.dc_gain();
874        let poles = tf.poles();
875        let mut y = h0;
876        for pole in &poles {
877            if pole.re.abs() < 1e-12 && pole.im.abs() < 1e-12 {
878                continue;
879            }
880            let decay = (pole.re * t).exp();
881            let osc = (pole.im * t).cos();
882            y -= h0 * decay * osc / poles.len() as f64;
883        }
884        outputs.push(y);
885    }
886    (times, outputs)
887}
888/// Impulse response of a transfer function.
889///
890/// Returns `(time_vec, output_vec)`.
891pub fn impulse_response(tf: &TransferFunction, t_final: f64, dt: f64) -> (Vec<f64>, Vec<f64>) {
892    let n_steps = (t_final / dt).ceil() as usize;
893    let mut times = Vec::with_capacity(n_steps);
894    let mut outputs = Vec::with_capacity(n_steps);
895    for k in 0..n_steps {
896        let t = k as f64 * dt;
897        times.push(t);
898        let poles = tf.poles();
899        let mut y = 0.0;
900        for pole in &poles {
901            let decay = (pole.re * t).exp();
902            let osc = (pole.im * t).cos();
903            y += decay * osc;
904        }
905        outputs.push(y);
906    }
907    (times, outputs)
908}
909/// Compute the H2 norm of a stable SISO system via numerical integration.
910///
911/// H2 = sqrt( (1/2pi) * integral |H(jw)|^2 dw )
912pub fn h2_norm(tf: &TransferFunction, omega_max: f64, n_points: usize) -> f64 {
913    let dw = omega_max / n_points as f64;
914    let mut integral = 0.0;
915    for k in 0..n_points {
916        let w = (k as f64 + 0.5) * dw;
917        let h = tf.eval_jw(w);
918        integral += h.norm_sq() * dw;
919    }
920    (integral / PI).sqrt()
921}
922#[cfg(test)]
923mod tests {
924    use super::*;
925    use crate::control::KalmanFilter;
926    use crate::control::LqrController;
927    use crate::control_systems::MpcController;
928    #[test]
929    fn test_complex_abs() {
930        let z = Complex::new(3.0, 4.0);
931        assert!((z.abs() - 5.0).abs() < 1e-10);
932    }
933    #[test]
934    fn test_complex_arg() {
935        let z = Complex::new(1.0, 1.0);
936        assert!((z.arg() - PI / 4.0).abs() < 1e-10);
937    }
938    #[test]
939    fn test_complex_mul() {
940        let a = Complex::new(1.0, 2.0);
941        let b = Complex::new(3.0, 4.0);
942        let c = a.mul(&b);
943        assert!((c.re - (-5.0)).abs() < 1e-10);
944        assert!((c.im - 10.0).abs() < 1e-10);
945    }
946    #[test]
947    fn test_complex_div() {
948        let a = Complex::new(1.0, 0.0);
949        let b = Complex::new(0.0, 1.0);
950        let c = a.div(&b);
951        assert!((c.re).abs() < 1e-10);
952        assert!((c.im - (-1.0)).abs() < 1e-10);
953    }
954    #[test]
955    fn test_complex_from_polar() {
956        let z = Complex::from_polar(2.0, PI / 2.0);
957        assert!((z.re).abs() < 1e-10);
958        assert!((z.im - 2.0).abs() < 1e-10);
959    }
960    #[test]
961    fn test_poly_eval_constant() {
962        assert!((poly_eval(&[5.0], 10.0) - 5.0).abs() < 1e-10);
963    }
964    #[test]
965    fn test_poly_eval_linear() {
966        assert!((poly_eval(&[2.0, 1.0], 3.0) - 7.0).abs() < 1e-10);
967    }
968    #[test]
969    fn test_poly_mul_degree() {
970        let a = vec![1.0, 1.0];
971        let b = vec![1.0, -1.0];
972        let c = poly_mul(&a, &b);
973        assert_eq!(c.len(), 3);
974        assert!((c[0] - 1.0).abs() < 1e-10);
975        assert!((c[1]).abs() < 1e-10);
976        assert!((c[2] + 1.0).abs() < 1e-10);
977    }
978    #[test]
979    fn test_poly_add_different_lengths() {
980        let a = vec![1.0, 2.0, 3.0];
981        let b = vec![4.0, 5.0];
982        let c = poly_add(&a, &b);
983        assert_eq!(c.len(), 3);
984        assert!((c[0] - 1.0).abs() < 1e-10);
985        assert!((c[1] - 6.0).abs() < 1e-10);
986        assert!((c[2] - 8.0).abs() < 1e-10);
987    }
988    #[test]
989    fn test_poly_deriv() {
990        let d = poly_deriv(&[3.0, 2.0, 1.0]);
991        assert_eq!(d.len(), 2);
992        assert!((d[0] - 6.0).abs() < 1e-10);
993        assert!((d[1] - 2.0).abs() < 1e-10);
994    }
995    #[test]
996    fn test_tf_dc_gain_first_order() {
997        let tf = TransferFunction::first_order(3.0, 2.0);
998        assert!((tf.dc_gain() - 3.0).abs() < 1e-10);
999    }
1000    #[test]
1001    fn test_tf_dc_gain_second_order() {
1002        let tf = TransferFunction::second_order(5.0, 0.7);
1003        assert!((tf.dc_gain() - 1.0).abs() < 1e-10);
1004    }
1005    #[test]
1006    fn test_tf_is_stable_first_order() {
1007        let tf = TransferFunction::first_order(1.0, 1.0);
1008        assert!(tf.is_stable());
1009    }
1010    #[test]
1011    fn test_tf_is_unstable() {
1012        let tf = TransferFunction::new(vec![1.0, 1.0], vec![1.0, -1.0]);
1013        assert!(!tf.is_stable());
1014    }
1015    #[test]
1016    fn test_tf_series() {
1017        let h1 = TransferFunction::first_order(2.0, 1.0);
1018        let h2 = TransferFunction::first_order(3.0, 1.0);
1019        let hs = TransferFunction::series(&h1, &h2);
1020        assert!((hs.dc_gain() - 6.0).abs() < 1e-10);
1021    }
1022    #[test]
1023    fn test_tf_poles_first_order() {
1024        let tf = TransferFunction::first_order(1.0, 2.0);
1025        let poles = tf.poles();
1026        assert_eq!(poles.len(), 1);
1027        assert!((poles[0].re - (-0.5)).abs() < 1e-6);
1028    }
1029    #[test]
1030    fn test_tf_poles_second_order_underdamped() {
1031        let tf = TransferFunction::second_order(10.0, 0.3);
1032        let poles = tf.poles();
1033        assert_eq!(poles.len(), 2);
1034        for p in &poles {
1035            assert!(p.re < 0.0, "poles should have negative real part");
1036        }
1037    }
1038    #[test]
1039    fn test_routh_hurwitz_stable_first_order() {
1040        assert!(routh_hurwitz_stable(&[1.0, 2.0]));
1041    }
1042    #[test]
1043    fn test_routh_hurwitz_unstable_negative_coeff() {
1044        assert!(!routh_hurwitz_stable(&[1.0, -1.0]));
1045    }
1046    #[test]
1047    fn test_routh_hurwitz_stable_second_order() {
1048        assert!(routh_hurwitz_stable(&[1.0, 3.0, 2.0]));
1049    }
1050    #[test]
1051    fn test_routh_hurwitz_stable_third_order() {
1052        assert!(routh_hurwitz_stable(&[1.0, 6.0, 11.0, 6.0]));
1053    }
1054    #[test]
1055    fn test_routh_hurwitz_unstable_third_order() {
1056        assert!(!routh_hurwitz_stable(&[1.0, 1.0, 1.0, 10.0]));
1057    }
1058    #[test]
1059    fn test_ss_step_integrator() {
1060        let model = StateSpaceModel::new(
1061            vec![vec![0.0]],
1062            vec![vec![1.0]],
1063            vec![vec![1.0]],
1064            vec![vec![0.0]],
1065        );
1066        let new_state = ss_step(&model, &[0.0], &[2.0], 0.5);
1067        assert!((new_state[0] - 1.0).abs() < 1e-10);
1068    }
1069    #[test]
1070    fn test_ss_output_passthrough() {
1071        let model = StateSpaceModel::new(
1072            vec![vec![0.0]],
1073            vec![vec![0.0]],
1074            vec![vec![0.0]],
1075            vec![vec![2.0]],
1076        );
1077        let y = ss_output(&model, &[0.0], &[3.0]);
1078        assert!((y[0] - 6.0).abs() < 1e-10);
1079    }
1080    #[test]
1081    fn test_ss_step_2x2() {
1082        let model = StateSpaceModel::new(
1083            vec![vec![0.0, 1.0], vec![-1.0, 0.0]],
1084            vec![vec![0.0], vec![1.0]],
1085            vec![vec![1.0, 0.0]],
1086            vec![vec![0.0]],
1087        );
1088        let new_state = ss_step(&model, &[1.0, 0.0], &[0.0], 0.01);
1089        assert_eq!(new_state.len(), 2);
1090    }
1091    #[test]
1092    fn test_ss_simulate_length() {
1093        let model = StateSpaceModel::new(
1094            vec![vec![0.0]],
1095            vec![vec![1.0]],
1096            vec![vec![1.0]],
1097            vec![vec![0.0]],
1098        );
1099        let inputs = vec![vec![1.0]; 10];
1100        let history = ss_simulate(&model, &[0.0], &inputs, 0.1);
1101        assert_eq!(history.len(), 11);
1102    }
1103    #[test]
1104    fn test_ss_dimensions() {
1105        let model = StateSpaceModel::new(
1106            vec![vec![0.0, 1.0], vec![-1.0, 0.0]],
1107            vec![vec![0.0], vec![1.0]],
1108            vec![vec![1.0, 0.0], vec![0.0, 1.0]],
1109            vec![vec![0.0], vec![0.0]],
1110        );
1111        assert_eq!(model.state_dim(), 2);
1112        assert_eq!(model.input_dim(), 1);
1113        assert_eq!(model.output_dim(), 2);
1114    }
1115    #[test]
1116    fn test_pid_proportional_only() {
1117        let mut ctrl = PidController::new(2.0, 0.0, 0.0);
1118        let out = pid_update(&mut ctrl, 5.0, 3.0, 0.1);
1119        assert!((out - 4.0).abs() < 1e-10);
1120    }
1121    #[test]
1122    fn test_pid_clamped() {
1123        let mut ctrl = PidController::new(100.0, 0.0, 0.0).with_limits(-1.0, 1.0);
1124        let out = pid_update(&mut ctrl, 10.0, 0.0, 0.1);
1125        assert!((out - 1.0).abs() < 1e-10);
1126    }
1127    #[test]
1128    fn test_pid_reset_clears() {
1129        let mut ctrl = PidController::new(1.0, 1.0, 0.0);
1130        pid_update(&mut ctrl, 1.0, 0.0, 0.5);
1131        pid_reset(&mut ctrl);
1132        assert!(ctrl.integral.abs() < 1e-12);
1133        assert!(ctrl.prev_error.abs() < 1e-12);
1134    }
1135    #[test]
1136    fn test_pid_setpoint_weighting() {
1137        let mut ctrl = PidController::new(2.0, 0.0, 0.0).with_setpoint_weights(0.5, 0.0);
1138        let out = pid_update(&mut ctrl, 10.0, 0.0, 0.1);
1139        assert!((out - 10.0).abs() < 1e-10);
1140    }
1141    #[test]
1142    fn test_pid_derivative_filter() {
1143        let mut ctrl = PidController::new(0.0, 0.0, 1.0).with_deriv_filter(0.5);
1144        let _out1 = pid_update(&mut ctrl, 0.0, 0.0, 0.1);
1145        let _out2 = pid_update(&mut ctrl, 1.0, 0.0, 0.1);
1146        assert!(ctrl.prev_deriv_filtered.is_finite());
1147    }
1148    #[test]
1149    fn test_pid_anti_windup() {
1150        let mut ctrl = PidController::new(1.0, 10.0, 0.0)
1151            .with_limits(-1.0, 1.0)
1152            .with_anti_windup(1.0);
1153        for _ in 0..100 {
1154            pid_update(&mut ctrl, 100.0, 0.0, 0.1);
1155        }
1156        assert!(
1157            ctrl.integral.abs() < 1e6,
1158            "anti-windup should limit integral growth"
1159        );
1160    }
1161    #[test]
1162    fn test_logspace_length() {
1163        let omegas = logspace_omega(0.1, 1000.0, 50);
1164        assert_eq!(omegas.len(), 50);
1165    }
1166    #[test]
1167    fn test_logspace_endpoints() {
1168        let omegas = logspace_omega(1.0, 100.0, 10);
1169        assert!((omegas[0] - 1.0).abs() < 1e-10);
1170        assert!((omegas[9] - 100.0).abs() < 1e-6);
1171    }
1172    #[test]
1173    fn test_bode_data_length() {
1174        let tf = TransferFunction::first_order(1.0, 1.0);
1175        let (f, m, p) = bode_data(&tf, 0.01, 100.0, 50);
1176        assert_eq!(f.len(), 50);
1177        assert_eq!(m.len(), 50);
1178        assert_eq!(p.len(), 50);
1179    }
1180    #[test]
1181    fn test_nyquist_data_length() {
1182        let tf = TransferFunction::first_order(1.0, 1.0);
1183        let (re, im) = nyquist_data(&tf, 0.01, 100.0, 50);
1184        assert_eq!(re.len(), 50);
1185        assert_eq!(im.len(), 50);
1186    }
1187    #[test]
1188    fn test_root_locus_returns_correct_count() {
1189        let tf = TransferFunction::first_order(1.0, 1.0);
1190        let gains: Vec<f64> = (0..10).map(|i| i as f64 * 0.5).collect();
1191        let rl = root_locus(&tf, &gains);
1192        assert_eq!(rl.len(), 10);
1193    }
1194    #[test]
1195    fn test_root_locus_zero_gain_gives_open_loop_poles() {
1196        let tf = TransferFunction::new(vec![1.0], vec![1.0, 2.0]);
1197        let rl = root_locus(&tf, &[0.0]);
1198        assert_eq!(rl.len(), 1);
1199        let poles = &rl[0].1;
1200        assert_eq!(poles.len(), 1);
1201        assert!((poles[0].re - (-2.0)).abs() < 1e-6);
1202    }
1203    #[test]
1204    fn test_root_locus_breakpoints() {
1205        let tf = TransferFunction::new(vec![1.0], vec![1.0, 4.0, 3.0]);
1206        let bp = root_locus_breakpoints(&tf);
1207        assert!(!bp.is_empty(), "should find breakpoint");
1208        let has_near_minus2 = bp.iter().any(|&x| (x + 2.0).abs() < 0.5);
1209        assert!(has_near_minus2, "breakpoint near -2 expected, got {:?}", bp);
1210    }
1211    #[test]
1212    fn test_discretize_zoh_dimensions() {
1213        let model = StateSpaceModel::new(
1214            vec![vec![0.0, 1.0], vec![-2.0, -3.0]],
1215            vec![vec![0.0], vec![1.0]],
1216            vec![vec![1.0, 0.0]],
1217            vec![vec![0.0]],
1218        );
1219        let disc = discretize_zoh(&model, 0.01);
1220        assert_eq!(disc.a.len(), 2);
1221        assert_eq!(disc.a[0].len(), 2);
1222        assert_eq!(disc.b.len(), 2);
1223        assert_eq!(disc.b[0].len(), 1);
1224    }
1225    #[test]
1226    fn test_discretize_zoh_identity_for_zero_a() {
1227        let model = StateSpaceModel::new(
1228            vec![vec![0.0]],
1229            vec![vec![1.0]],
1230            vec![vec![1.0]],
1231            vec![vec![0.0]],
1232        );
1233        let disc = discretize_zoh(&model, 0.1);
1234        assert!((disc.a[0][0] - 1.0).abs() < 1e-10);
1235    }
1236    #[test]
1237    fn test_discretize_tustin_preserves_dc() {
1238        let tf = TransferFunction::first_order(2.0, 1.0);
1239        let disc = discretize_tustin(&tf, 0.01);
1240        let h_z1 = poly_eval(&disc.num, 1.0) / poly_eval(&disc.den, 1.0);
1241        assert!(
1242            (h_z1 - 2.0).abs() < 0.01,
1243            "Tustin should preserve DC gain, got {:.6}",
1244            h_z1
1245        );
1246    }
1247    #[test]
1248    fn test_controllable_system() {
1249        let a = vec![0.0, 1.0, -2.0, -3.0];
1250        let b = vec![0.0, 1.0];
1251        assert!(is_controllable(&a, &b, 2, 1));
1252    }
1253    #[test]
1254    fn test_uncontrollable_system() {
1255        let a = vec![1.0, 0.0, 0.0, 2.0];
1256        let b = vec![1.0, 0.0];
1257        assert!(!is_controllable(&a, &b, 2, 1));
1258    }
1259    #[test]
1260    fn test_observable_system() {
1261        let a = vec![0.0, 1.0, -2.0, -3.0];
1262        let c = vec![1.0, 0.0];
1263        assert!(is_observable(&a, &c, 2, 1));
1264    }
1265    #[test]
1266    fn test_matrix_rank() {
1267        let m = vec![1.0, 0.0, 0.0, 1.0];
1268        assert_eq!(matrix_rank(&m, 2, 2), 2);
1269    }
1270    #[test]
1271    fn test_matrix_rank_deficient() {
1272        let m = vec![1.0, 2.0, 2.0, 4.0];
1273        assert_eq!(matrix_rank(&m, 2, 2), 1);
1274    }
1275    #[test]
1276    fn test_lqr_zero_state_zero_control() {
1277        let a = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
1278        let b = vec![vec![1.0], vec![0.0]];
1279        let q = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
1280        let r = vec![vec![1.0]];
1281        if let Some(lqr) = LqrController::solve(&a, &b, q, r) {
1282            let u = lqr.compute(&[0.0, 0.0]);
1283            assert_eq!(u[0], 0.0);
1284        }
1285    }
1286    #[test]
1287    fn test_lqr_cost_zero() {
1288        let q = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
1289        let r = vec![vec![1.0]];
1290        let cost = lqr_cost(&[0.0, 0.0], &[0.0], &q, &r);
1291        assert!(cost.abs() < 1e-12);
1292    }
1293    #[test]
1294    fn test_lqr_cost_non_negative() {
1295        let q = vec![vec![2.0, 0.0], vec![0.0, 3.0]];
1296        let r = vec![vec![1.0]];
1297        let cost = lqr_cost(&[0.5, -0.3], &[0.1], &q, &r);
1298        assert!(cost >= 0.0);
1299    }
1300    #[test]
1301    fn test_kalman_initial_state() {
1302        let x0 = vec![0.0];
1303        let p0 = vec![vec![1.0]];
1304        let kf = KalmanFilter::new(x0, p0);
1305        assert_eq!(kf.x_hat[0], 0.0);
1306    }
1307    #[test]
1308    fn test_kalman_prediction_preserves_state() {
1309        let x0 = vec![5.0];
1310        let p0 = vec![vec![1.0]];
1311        let mut kf = KalmanFilter::new(x0, p0);
1312        // A = I, B = [0], u = [0], Q = [0.01]
1313        let a = vec![vec![1.0]];
1314        let b = vec![vec![0.0]];
1315        let q = vec![vec![0.01]];
1316        kf.predict(&a, &b, &[0.0], &q);
1317        assert!((kf.x_hat[0] - 5.0).abs() < 1e-10);
1318    }
1319    #[test]
1320    fn test_kalman_update_moves_toward_measurement() {
1321        let x0 = vec![0.0];
1322        let p0 = vec![vec![1.0]];
1323        let mut kf = KalmanFilter::new(x0, p0);
1324        let a = vec![vec![1.0]];
1325        let b = vec![vec![0.0]];
1326        let q = vec![vec![0.01]];
1327        kf.predict(&a, &b, &[0.0], &q);
1328        let h = vec![vec![1.0]];
1329        let r = vec![vec![0.1]];
1330        let _ = kf.update(&h, &r, &[10.0]);
1331        assert!(kf.x_hat[0] > 0.0);
1332    }
1333    #[test]
1334    fn test_kalman_predict_then_update() {
1335        let x0 = vec![0.0];
1336        let p0 = vec![vec![1.0]];
1337        let mut kf = KalmanFilter::new(x0, p0);
1338        let a = vec![vec![1.0]];
1339        let b = vec![vec![0.0]];
1340        let q = vec![vec![0.01]];
1341        let h = vec![vec![1.0]];
1342        let r = vec![vec![0.1]];
1343        kf.predict(&a, &b, &[0.0], &q);
1344        let _ = kf.update(&h, &r, &[5.0]);
1345        assert_eq!(kf.x_hat.len(), 1);
1346        assert!(kf.x_hat[0] > 0.0);
1347    }
1348    #[test]
1349    fn test_lead_compensator_zero_lt_pole() {
1350        let (z, p) = lead_compensator(10.0, 45.0);
1351        assert!(z < p);
1352    }
1353    #[test]
1354    fn test_lead_compensator_positive() {
1355        let (z, p) = lead_compensator(5.0, 30.0);
1356        assert!(z > 0.0 && p > 0.0);
1357    }
1358    #[test]
1359    fn test_lag_compensator_pole_lt_zero() {
1360        let (z, p) = lag_compensator(10.0, 5.0);
1361        assert!(p < z);
1362    }
1363    #[test]
1364    fn test_observer_gain_1d() {
1365        let a = vec![vec![2.0]];
1366        let c = vec![vec![1.0]];
1367        let desired = [[-1.0, 0.0]];
1368        let l = observer_gain_ackermann(&a, &c, &desired);
1369        assert!((l[0] - 3.0).abs() < 1e-10);
1370    }
1371    #[test]
1372    fn test_observer_gain_empty() {
1373        let l = observer_gain_ackermann(&[], &[], &[]);
1374        assert!(l.is_empty());
1375    }
1376    #[test]
1377    fn test_pole_placement_stable() {
1378        let eigs = [[-1.0, 0.0], [-2.0, 0.5]];
1379        assert!(pole_placement_check(&eigs));
1380    }
1381    #[test]
1382    fn test_pole_placement_unstable() {
1383        let eigs = [[-1.0, 0.0], [0.1, 0.0]];
1384        assert!(!pole_placement_check(&eigs));
1385    }
1386    #[test]
1387    fn test_h_infinity_norm_positive() {
1388        let tf = TransferFunction::first_order(2.0, 1.0);
1389        let omegas = logspace_omega(0.01, 100.0, 50);
1390        let norm = h_infinity_norm_bound(&tf, &omegas);
1391        assert!(norm > 0.0);
1392    }
1393    #[test]
1394    fn test_h2_norm_positive() {
1395        let tf = TransferFunction::first_order(1.0, 1.0);
1396        let norm = h2_norm(&tf, 100.0, 1000);
1397        assert!(norm > 0.0);
1398    }
1399    #[test]
1400    fn test_mat_inv_2x2() {
1401        let a = vec![1.0, 0.0, 0.0, 2.0];
1402        let inv = mat_inv_small(&a, 2);
1403        assert!((inv[0] - 1.0).abs() < 1e-10);
1404        assert!((inv[3] - 0.5).abs() < 1e-10);
1405    }
1406    #[test]
1407    fn test_mpc_returns_correct_length() {
1408        let a = vec![1.0, 0.1, 0.0, 1.0];
1409        let b = vec![0.005, 0.1];
1410        let q = vec![1.0, 0.0, 0.0, 1.0];
1411        let r = vec![0.1];
1412        let mpc = MpcController::new(a, b, 2, 1, 3, q, r);
1413        let u = mpc.compute(&[1.0, 0.0], &[0.0, 0.0]);
1414        assert_eq!(u.len(), 1);
1415    }
1416    #[test]
1417    fn test_ziegler_nichols() {
1418        let (kp, ki, kd) = ziegler_nichols_pid(10.0, 2.0);
1419        assert!((kp - 6.0).abs() < 1e-10);
1420        assert!((ki - 6.0).abs() < 1e-10);
1421        assert!((kd - 1.5).abs() < 1e-10);
1422    }
1423    #[test]
1424    fn test_cohen_coon_positive_gains() {
1425        let (kp, ki, kd) = cohen_coon_pid(1.0, 5.0, 1.0);
1426        assert!(kp > 0.0);
1427        assert!(ki > 0.0);
1428        assert!(kd > 0.0);
1429    }
1430    #[test]
1431    fn test_lyapunov_stable_2x2() {
1432        assert!(lyapunov_stable_2x2(&[-1.0, 0.0, 0.0, -2.0]));
1433    }
1434    #[test]
1435    fn test_lyapunov_unstable_2x2() {
1436        assert!(!lyapunov_stable_2x2(&[1.0, 0.0, 0.0, -1.0]));
1437    }
1438    #[test]
1439    fn test_solve_lyapunov_1x1() {
1440        let p = solve_lyapunov_1x1(-2.0, 1.0);
1441        assert!((p - 0.25).abs() < 1e-10);
1442    }
1443    #[test]
1444    fn test_step_response_length() {
1445        let tf = TransferFunction::first_order(1.0, 1.0);
1446        let (t, y) = step_response(&tf, 5.0, 0.01);
1447        assert_eq!(t.len(), y.len());
1448        assert!(t.len() > 400);
1449    }
1450    #[test]
1451    fn test_impulse_response_length() {
1452        let tf = TransferFunction::first_order(1.0, 1.0);
1453        let (t, y) = impulse_response(&tf, 5.0, 0.01);
1454        assert_eq!(t.len(), y.len());
1455        assert!(t.len() > 400);
1456    }
1457    #[test]
1458    fn test_stability_margins_finite() {
1459        let tf = TransferFunction::new(vec![1.0], vec![1.0, 3.0, 3.0, 1.0]);
1460        let (gm, pm) = stability_margins(&tf, 0.01, 100.0, 500);
1461        assert!(gm.is_finite());
1462        assert!(pm.is_finite());
1463    }
1464    #[test]
1465    fn test_nyquist_encirclements_stable_system() {
1466        let tf = TransferFunction::first_order(0.5, 1.0);
1467        let enc = nyquist_encirclements(&tf, 0.01, 100.0, 500);
1468        assert_eq!(enc, 0);
1469    }
1470    #[test]
1471    fn test_feedback_dc_gain() {
1472        let plant = TransferFunction::first_order(10.0, 1.0);
1473        let ctrl = TransferFunction::new(vec![1.0], vec![1.0]);
1474        let cl = TransferFunction::feedback(&plant, &ctrl);
1475        let dc = cl.dc_gain();
1476        assert!(
1477            (dc - 10.0 / 11.0).abs() < 0.1,
1478            "expected ~0.909, got {:.6}",
1479            dc
1480        );
1481    }
1482}