1#![allow(clippy::needless_range_loop)]
6#[allow(unused_imports)]
7use std::f64::consts::PI;
8
9use super::types::{Complex, PidController, StateSpaceModel, TransferFunction};
10
11pub 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}
19pub 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}
27pub 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}
41pub 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}
55pub 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#[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}
140pub 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}
195pub 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}
215pub 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}
233pub 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}
249pub 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}
275pub 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}
282pub 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}
296pub 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}
314pub 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}
345pub 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}
362pub 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}
392pub 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}
405pub 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}
422pub 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}
458pub 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#[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#[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#[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#[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#[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#[allow(dead_code)]
557pub fn mat_scale(a: &[f64], s: f64) -> Vec<f64> {
558 a.iter().map(|&x| x * s).collect()
559}
560pub 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}
641pub 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}
662pub 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}
679pub 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}
714pub 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}
719pub 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}
724pub 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}
741pub 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}
753pub 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}
763pub 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}
803pub fn pole_placement_check(eigs: &[[f64; 2]]) -> bool {
805 eigs.iter().all(|e| e[0] < 0.0)
806}
807pub 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}
816pub 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}
825pub 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}
838pub 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}
854pub 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}
863pub 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}
888pub 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}
909pub 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 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}