use super::sensitivity::Complex;
use super::FreqError;
use crate::core::scalar::ControlScalar;
use crate::core::transfer_fn::TransferFn;
use heapless::Vec as HVec;
#[derive(Debug, Clone)]
pub struct RootLocusPoint<S: ControlScalar> {
pub gain: S,
pub poles: HVec<Complex<S>, 8>,
}
pub struct RootLocusData<S: ControlScalar, const N: usize> {
pub points: HVec<RootLocusPoint<S>, N>,
}
impl<S: ControlScalar, const N: usize> RootLocusData<S, N> {
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
}
pub fn stability_region<S: ControlScalar>(poles: &HVec<Complex<S>, 8>) -> bool {
for pole in poles.iter() {
let mag_sq = pole.re * pole.re + pole.im * pole.im;
if mag_sq >= S::ONE {
return false;
}
}
true
}
fn compute_closed_loop_poles<S: ControlScalar, const N: usize>(
tf: &TransferFn<S, N>,
k: S,
) -> HVec<Complex<S>, 8> {
let b = tf.b();
let a = tf.a();
let mut poly = [S::ZERO; 9];
poly[0] = S::ONE + k * b[0];
for i in 1..N {
poly[i] = a[i - 1] + k * b[i];
}
poly[N] = a[N - 1];
find_polynomial_roots(&poly[..=N])
}
fn find_polynomial_roots<S: ControlScalar>(coeffs: &[S]) -> HVec<Complex<S>, 8> {
let mut roots: HVec<Complex<S>, 8> = HVec::new();
let mut start = 0;
while start < coeffs.len() && coeffs[start].abs() < S::EPSILON {
start += 1;
}
if start >= coeffs.len() {
return roots; }
let degree = coeffs.len() - 1 - start;
let lead = coeffs[start];
match degree {
0 => {
}
1 => {
let z = -(coeffs[start + 1] / lead);
let _ = roots.push(Complex::new(z, S::ZERO));
}
2 => {
let c0 = lead;
let c1 = coeffs[start + 1];
let c2 = coeffs[start + 2];
let disc = c1 * c1 - S::from_f64(4.0) * c0 * c2;
let two_c0 = S::TWO * c0;
if disc >= S::ZERO {
let sq = disc.sqrt();
let _ = roots.push(Complex::new((-c1 + sq) / two_c0, S::ZERO));
let _ = roots.push(Complex::new((-c1 - sq) / two_c0, S::ZERO));
} else {
let sq = (-disc).sqrt();
let _ = roots.push(Complex::new(-c1 / two_c0, sq / two_c0));
let _ = roots.push(Complex::new(-c1 / two_c0, -(sq / two_c0)));
}
}
3 => {
let c0 = lead;
let c1 = coeffs[start + 1] / c0;
let c2 = coeffs[start + 2] / c0;
let c3 = coeffs[start + 3] / c0;
compute_cubic_roots(c1, c2, c3, &mut roots);
}
4 => {
let normalized: [S; 5] = [
S::ONE,
coeffs[start + 1] / lead,
coeffs[start + 2] / lead,
coeffs[start + 3] / lead,
coeffs[start + 4] / lead,
];
compute_quartic_roots_companion(&normalized, &mut roots);
}
_ => {
companion_matrix_roots(coeffs, start, degree, lead, &mut roots);
}
}
roots
}
fn compute_cubic_roots<S: ControlScalar>(c1: S, c2: S, c3: S, roots: &mut HVec<Complex<S>, 8>) {
let third = S::from_f64(1.0 / 3.0);
let p = c2 - c1 * c1 * third;
let q = S::from_f64(2.0 / 27.0) * c1 * c1 * c1 - third * c1 * c2 + c3;
let disc = q * q / S::from_f64(4.0) + p * p * p / S::from_f64(27.0);
let shift = c1 * third;
if disc >= S::ZERO {
let sq = disc.sqrt();
let u_arg = -q / S::TWO + sq;
let v_arg = -q / S::TWO - sq;
let u = cbrt_signed(u_arg);
let v = cbrt_signed(v_arg);
let root1 = u + v - shift;
let _ = roots.push(Complex::new(root1, S::ZERO));
if disc.abs() < S::EPSILON {
let root2 = -(u + v) / S::TWO - shift;
let _ = roots.push(Complex::new(root2, S::ZERO));
let _ = roots.push(Complex::new(root2, S::ZERO));
} else {
let re_part = -(u + v) / S::TWO - shift;
let im_part = S::from_f64(libm::sqrt(3.0_f64) / 2.0) * (u - v);
let _ = roots.push(Complex::new(re_part, im_part));
let _ = roots.push(Complex::new(re_part, -im_part));
}
} else {
let m = S::TWO * ((-p) / S::from_f64(3.0)).sqrt();
let theta = (S::from_f64(3.0) * q / (p * m)).acos() / S::from_f64(3.0);
let two_pi_thirds = S::from_f64(2.0 * core::f64::consts::PI / 3.0);
let root1 = m * theta.cos() - shift;
let root2 = m * (theta - two_pi_thirds).cos() - shift;
let root3 = m * (theta + two_pi_thirds).cos() - shift;
let _ = roots.push(Complex::new(root1, S::ZERO));
let _ = roots.push(Complex::new(root2, S::ZERO));
let _ = roots.push(Complex::new(root3, S::ZERO));
}
}
fn cbrt_signed<S: ControlScalar>(x: S) -> S {
if x >= S::ZERO {
x.powf(S::from_f64(1.0 / 3.0))
} else {
-((-x).powf(S::from_f64(1.0 / 3.0)))
}
}
fn compute_quartic_roots_companion<S: ControlScalar>(
coeffs: &[S; 5],
roots: &mut HVec<Complex<S>, 8>,
) {
let all_coeffs = [coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4]];
companion_matrix_roots(&all_coeffs, 0, 4, S::ONE, roots);
}
fn companion_matrix_roots<S: ControlScalar>(
coeffs: &[S],
start: usize,
degree: usize,
lead: S,
roots: &mut HVec<Complex<S>, 8>,
) {
if degree == 0 || degree > 8 {
return;
}
let mut norm = [S::ZERO; 8];
#[allow(clippy::needless_range_loop)]
for i in 0..degree {
norm[i] = coeffs[start + 1 + i] / lead;
}
let mut z: [Complex<S>; 8] = [Complex::new(S::ZERO, S::ZERO); 8];
#[allow(clippy::needless_range_loop)]
for i in 0..degree {
let angle = S::from_f64(2.0 * core::f64::consts::PI * i as f64 / degree as f64);
z[i] = Complex::new(angle.cos(), angle.sin());
}
for _iter in 0..200 {
let mut max_change = S::ZERO;
for i in 0..degree {
let p_val = eval_poly_complex(&norm[..degree], z[i]);
let mut denom = Complex::new(S::ONE, S::ZERO);
for j in 0..degree {
if j != i {
denom = denom.multiply(&z[i].sub(&z[j]));
}
}
let denom_mag_sq = denom.magnitude_sq();
if denom_mag_sq < S::EPSILON {
continue;
}
let update = p_val
.divide(&denom)
.unwrap_or(Complex::new(S::ZERO, S::ZERO));
let change = update.magnitude();
if change > max_change {
max_change = change;
}
z[i] = z[i].sub(&update);
}
if max_change < S::from_f64(1e-12) {
break;
}
}
for z_val in z.iter().take(degree) {
if roots.len() < 8 {
let _ = roots.push(*z_val);
}
}
}
fn eval_poly_complex<S: ControlScalar>(norm: &[S], z: Complex<S>) -> Complex<S> {
let n = norm.len();
if n == 0 {
return Complex::new(S::ONE, S::ZERO);
}
let mut acc = Complex::new(S::ONE, S::ZERO); for &n_coeff in norm.iter().take(n) {
acc = acc.multiply(&z);
acc.re += n_coeff;
}
acc
}
pub fn compute_root_locus<S: ControlScalar, const TF_ORDER: usize, const N: usize>(
open_loop_tf: &TransferFn<S, TF_ORDER>,
k_max: S,
) -> Result<RootLocusData<S, N>, FreqError> {
if N < 2 {
return Err(FreqError::InsufficientPoints);
}
if k_max <= S::ZERO {
return Err(FreqError::InvalidParameter);
}
if TF_ORDER > 8 {
return Err(FreqError::InvalidParameter);
}
let mut data = RootLocusData::<S, N> {
points: HVec::new(),
};
let n_minus_one = S::from_f64((N - 1) as f64);
for i in 0..N {
let k = k_max * S::from_f64(i as f64) / n_minus_one;
let poles = compute_closed_loop_poles(open_loop_tf, k);
let point = RootLocusPoint { gain: k, poles };
let _ = data.points.push(point);
}
Ok(data)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::transfer_fn::TransferFn;
#[test]
fn root_locus_zero_gain_gives_open_loop_poles() {
let alpha = 0.5_f64;
let tf = TransferFn::<f64, 1>::new([1.0 - alpha], [-alpha]);
let data = compute_root_locus::<f64, 1, 8>(&tf, 1.0).expect("root locus ok");
let first = &data.points[0];
assert!(
(first.gain).abs() < 1e-10,
"First gain should be 0, got {}",
first.gain
);
if let Some(pole) = first.poles.first() {
assert!(
(pole.re - alpha).abs() < 1e-6,
"Open-loop pole should be at α={}, got re={}",
alpha,
pole.re
);
assert!(
pole.im.abs() < 1e-6,
"Open-loop pole should be real, got im={}",
pole.im
);
}
}
#[test]
fn root_locus_point_count() {
let tf = TransferFn::<f64, 1>::new([1.0], [0.0]);
let data = compute_root_locus::<f64, 1, 16>(&tf, 1.0).expect("root locus ok");
assert_eq!(data.len(), 16, "Should have 16 root locus points");
}
#[test]
fn stability_region_inside_unit_circle() {
let alpha = 0.3_f64;
let tf = TransferFn::<f64, 1>::new([1.0 - alpha], [-alpha]);
let data = compute_root_locus::<f64, 1, 8>(&tf, 0.1).expect("root locus ok");
for pt in data.points.iter() {
let stable = stability_region(&pt.poles);
assert!(
stable,
"Poles should be inside unit circle for small gain k={}, poles: {:?}",
pt.gain, pt.poles
);
}
}
#[test]
fn stability_region_detects_unstable() {
let mut poles: HVec<Complex<f64>, 8> = HVec::new();
let _ = poles.push(Complex::new(1.5, 0.0)); assert!(!stability_region(&poles), "Pole at 1.5 should be unstable");
}
#[test]
fn stability_region_detects_stable() {
let mut poles: HVec<Complex<f64>, 8> = HVec::new();
let _ = poles.push(Complex::new(0.5, 0.0));
let _ = poles.push(Complex::new(-0.3, 0.2));
assert!(
stability_region(&poles),
"Poles inside unit circle should be stable"
);
}
#[test]
fn quadratic_roots_real() {
let coeffs = [1.0_f64, 0.0, -1.0];
let roots = find_polynomial_roots(&coeffs);
assert_eq!(roots.len(), 2, "Should find 2 roots");
let mut reals: [f64; 2] = [roots[0].re, roots[1].re];
reals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
assert!((reals[0] - (-1.0)).abs() < 1e-10, "Root should be -1");
assert!((reals[1] - 1.0).abs() < 1e-10, "Root should be 1");
}
#[test]
fn quadratic_roots_complex() {
let coeffs = [1.0_f64, 0.0, 1.0];
let roots = find_polynomial_roots(&coeffs);
assert_eq!(roots.len(), 2, "Should find 2 roots");
for root in roots.iter() {
assert!(
root.re.abs() < 1e-10,
"Real part should be 0, got {}",
root.re
);
assert!(
(root.im.abs() - 1.0).abs() < 1e-10,
"Imag part magnitude should be 1, got {}",
root.im
);
}
}
#[test]
fn root_locus_invalid_kmax() {
let tf = TransferFn::<f64, 1>::new([1.0], [0.0]);
let result = compute_root_locus::<f64, 1, 8>(&tf, -1.0);
assert!(
matches!(result, Err(FreqError::InvalidParameter)),
"Negative k_max should return InvalidParameter"
);
}
}