use super::FreqError;
use crate::core::scalar::ControlScalar;
use crate::core::transfer_fn::TransferFn;
use heapless::Vec as HVec;
#[derive(Debug, Clone, Copy)]
pub struct Complex<S: ControlScalar> {
pub re: S,
pub im: S,
}
impl<S: ControlScalar> Complex<S> {
pub fn new(re: S, im: S) -> Self {
Self { re, im }
}
pub fn from_real(re: S) -> Self {
Self { re, im: S::ZERO }
}
pub fn multiply(&self, other: &Self) -> Self {
Self {
re: self.re * other.re - self.im * other.im,
im: self.re * other.im + self.im * other.re,
}
}
pub fn add(&self, other: &Self) -> Self {
Self {
re: self.re + other.re,
im: self.im + other.im,
}
}
pub fn sub(&self, other: &Self) -> Self {
Self {
re: self.re - other.re,
im: self.im - other.im,
}
}
pub fn magnitude(&self) -> S {
(self.re * self.re + self.im * self.im).sqrt()
}
pub fn magnitude_sq(&self) -> S {
self.re * self.re + self.im * self.im
}
pub fn phase(&self) -> S {
self.im.atan2(self.re)
}
pub fn magnitude_db(&self) -> S {
let mag_sq = self.magnitude_sq();
if mag_sq > S::ZERO {
S::from_f64(20.0) * mag_sq.sqrt().log10()
} else {
S::from_f64(-120.0)
}
}
pub fn reciprocal(&self) -> Option<Self> {
let mag_sq = self.magnitude_sq();
if mag_sq < S::EPSILON {
None
} else {
Some(Self {
re: self.re / mag_sq,
im: -self.im / mag_sq,
})
}
}
pub fn divide(&self, other: &Self) -> Option<Self> {
let denom = other.magnitude_sq();
if denom < S::EPSILON {
return None;
}
Some(Self {
re: (self.re * other.re + self.im * other.im) / denom,
im: (self.im * other.re - self.re * other.im) / denom,
})
}
}
fn eval_tf_complex<S: ControlScalar, const N: usize>(
tf: &TransferFn<S, N>,
omega: S,
) -> Complex<S> {
let b = tf.b();
let a = tf.a();
let mut num_re = S::ZERO;
let mut num_im = S::ZERO;
for (k, &b_k) in b.iter().enumerate().take(N) {
let angle = -(omega * S::from_f64(k as f64));
let (sin_a, cos_a) = angle.sin_cos();
num_re += b_k * cos_a;
num_im += b_k * sin_a;
}
let mut den_re = S::ONE;
let mut den_im = S::ZERO;
for (k, &a_k) in a.iter().enumerate().take(N) {
let angle = -(omega * S::from_f64((k + 1) as f64));
let (sin_a, cos_a) = angle.sin_cos();
den_re += a_k * cos_a;
den_im += a_k * sin_a;
}
let num = Complex::new(num_re, num_im);
let den = Complex::new(den_re, den_im);
num.divide(&den).unwrap_or(Complex::from_real(S::ZERO))
}
#[derive(Debug, Clone, Copy)]
pub struct SensitivityPoint<S: ControlScalar> {
pub omega: S,
pub sensitivity_db: S,
pub comp_sensitivity_db: S,
pub control_sensitivity_db: S,
}
pub struct LoopShaping<S: ControlScalar, const NP: usize, const NC: usize> {
plant: TransferFn<S, NP>,
controller: TransferFn<S, NC>,
}
impl<S: ControlScalar, const NP: usize, const NC: usize> LoopShaping<S, NP, NC> {
pub fn new(plant: TransferFn<S, NP>, controller: TransferFn<S, NC>) -> Self {
Self { plant, controller }
}
pub fn loop_gain_at(&self, omega: S) -> Complex<S> {
let p = eval_tf_complex(&self.plant, omega);
let c = eval_tf_complex(&self.controller, omega);
p.multiply(&c)
}
pub fn sensitivity_at(&self, omega: S) -> Complex<S> {
let l = self.loop_gain_at(omega);
let one_plus_l = Complex::new(S::ONE + l.re, l.im);
Complex::from_real(S::ONE)
.divide(&one_plus_l)
.unwrap_or(Complex::from_real(S::ZERO))
}
pub fn comp_sensitivity_at(&self, omega: S) -> Complex<S> {
let l = self.loop_gain_at(omega);
let one_plus_l = Complex::new(S::ONE + l.re, l.im);
l.divide(&one_plus_l).unwrap_or(Complex::from_real(S::ZERO))
}
pub fn control_sensitivity_at(&self, omega: S) -> Complex<S> {
let c = eval_tf_complex(&self.controller, omega);
let l = self.loop_gain_at(omega);
let one_plus_l = Complex::new(S::ONE + l.re, l.im);
c.divide(&one_plus_l).unwrap_or(Complex::from_real(S::ZERO))
}
pub fn compute_sensitivity_response<const N: usize>(
&self,
omega_min: S,
omega_max: S,
) -> Result<SensitivityData<S, N>, FreqError> {
if N < 2 {
return Err(FreqError::InsufficientPoints);
}
if omega_min <= S::ZERO || omega_min >= omega_max {
return Err(FreqError::InvalidFrequencyRange);
}
let mut data = SensitivityData::<S, N> {
points: HVec::new(),
};
let ln_min = omega_min.ln();
let ln_max = omega_max.ln();
let ln_range = ln_max - ln_min;
let n_minus_one = S::from_f64((N - 1) as f64);
for i in 0..N {
let t = S::from_f64(i as f64) / n_minus_one;
let omega = (ln_min + t * ln_range).exp();
let s_val = self.sensitivity_at(omega);
let t_val = self.comp_sensitivity_at(omega);
let q_val = self.control_sensitivity_at(omega);
let pt = SensitivityPoint {
omega,
sensitivity_db: s_val.magnitude_db(),
comp_sensitivity_db: t_val.magnitude_db(),
control_sensitivity_db: q_val.magnitude_db(),
};
let _ = data.points.push(pt);
}
Ok(data)
}
}
pub struct SensitivityData<S: ControlScalar, const N: usize> {
pub points: HVec<SensitivityPoint<S>, N>,
}
impl<S: ControlScalar, const N: usize> SensitivityData<S, N> {
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
}
pub fn peak_sensitivity<S: ControlScalar, const N: usize>(data: &SensitivityData<S, N>) -> S {
let mut peak = S::ZERO;
for pt in data.points.iter() {
let linear = S::from_f64(10.0_f64).powf(pt.sensitivity_db / S::from_f64(20.0));
if linear > peak {
peak = linear;
}
}
peak
}
pub fn bandwidth<S: ControlScalar, const N: usize>(data: &SensitivityData<S, N>) -> Option<S> {
let pts = &data.points;
if pts.is_empty() {
return None;
}
let dc_db = pts[0].comp_sensitivity_db;
let threshold_db = dc_db - S::from_f64(3.0);
for i in 0..(pts.len() - 1) {
let m0 = pts[i].comp_sensitivity_db;
let m1 = pts[i + 1].comp_sensitivity_db;
if m0 >= threshold_db && m1 < threshold_db {
let dm = m1 - m0;
if dm.abs() < S::EPSILON {
return Some(pts[i].omega);
}
let t = (threshold_db - m0) / dm;
return Some(pts[i].omega + t * (pts[i + 1].omega - pts[i].omega));
}
}
None
}
pub fn sensitivity_crossover<S: ControlScalar, const N: usize>(
data: &SensitivityData<S, N>,
) -> Option<S> {
let pts = &data.points;
if pts.len() < 2 {
return None;
}
for i in 0..(pts.len() - 1) {
let diff0 = pts[i].sensitivity_db - pts[i].comp_sensitivity_db;
let diff1 = pts[i + 1].sensitivity_db - pts[i + 1].comp_sensitivity_db;
if (diff0 >= S::ZERO && diff1 <= S::ZERO) || (diff0 <= S::ZERO && diff1 >= S::ZERO) {
let dd = diff1 - diff0;
if dd.abs() < S::EPSILON {
return Some(pts[i].omega);
}
let t = -diff0 / dd;
return Some(pts[i].omega + t * (pts[i + 1].omega - pts[i].omega));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::transfer_fn::TransferFn;
#[test]
fn sensitivity_plus_comp_equals_one() {
let plant = TransferFn::<f64, 1>::new([0.5], [0.0]);
let ctrl = TransferFn::<f64, 1>::new([1.0], [0.0]);
let ls = LoopShaping::new(plant, ctrl);
let test_omegas = [0.01, 0.1, 0.5, 1.0, 2.0];
for &omega in &test_omegas {
let s_val = ls.sensitivity_at(omega);
let t_val = ls.comp_sensitivity_at(omega);
let sum_re = s_val.re + t_val.re;
let sum_im = s_val.im + t_val.im;
assert!(
(sum_re - 1.0).abs() < 1e-10,
"S+T real should be 1, got {} at omega={}",
sum_re,
omega
);
assert!(
sum_im.abs() < 1e-10,
"S+T imag should be 0, got {} at omega={}",
sum_im,
omega
);
}
}
#[test]
fn peak_sensitivity_at_least_one() {
let plant = TransferFn::<f64, 1>::new([0.0], [0.0]);
let ctrl = TransferFn::<f64, 1>::new([1.0], [0.0]);
let ls = LoopShaping::new(plant, ctrl);
let data = ls
.compute_sensitivity_response::<32>(1e-3, core::f64::consts::PI)
.expect("sensitivity ok");
let ps = peak_sensitivity(&data);
assert!(
ps >= 1.0 - 1e-9,
"Peak sensitivity should be >= 1, got {}",
ps
);
}
#[test]
fn stable_loop_peak_sensitivity_ge_one() {
let plant = TransferFn::<f64, 1>::new([0.0], [0.0]); let ctrl = TransferFn::<f64, 1>::new([5.0], [0.0]); let ls = LoopShaping::new(plant, ctrl);
let data = ls
.compute_sensitivity_response::<64>(1e-3, core::f64::consts::PI)
.expect("sensitivity ok");
let ps = peak_sensitivity(&data);
assert!(
(ps - 1.0).abs() < 1e-6,
"Zero plant peak sensitivity should be exactly 1, got {}",
ps
);
}
#[test]
fn sensitivity_point_count() {
let plant = TransferFn::<f64, 1>::new([1.0], [0.0]);
let ctrl = TransferFn::<f64, 1>::new([1.0], [0.0]);
let ls = LoopShaping::new(plant, ctrl);
let data = ls
.compute_sensitivity_response::<32>(1e-3, core::f64::consts::PI)
.expect("sensitivity ok");
assert_eq!(data.len(), 32, "Should have 32 sensitivity points");
}
#[test]
fn complex_multiply_divide_roundtrip() {
let a = Complex::<f64>::new(3.0, 4.0);
let b = Complex::<f64>::new(1.0, -2.0);
let ab = a.multiply(&b);
let back = ab.divide(&b).expect("division ok");
assert!((back.re - a.re).abs() < 1e-10, "Real part roundtrip failed");
assert!((back.im - a.im).abs() < 1e-10, "Imag part roundtrip failed");
}
#[test]
fn complex_magnitude() {
let c = Complex::<f64>::new(3.0, 4.0);
assert!((c.magnitude() - 5.0).abs() < 1e-12, "Magnitude should be 5");
}
#[test]
fn sensitivity_crossover_static_unity() {
let plant = TransferFn::<f64, 1>::new([1.0], [0.0]);
let ctrl = TransferFn::<f64, 1>::new([1.0], [0.0]);
let ls = LoopShaping::new(plant, ctrl);
let data = ls
.compute_sensitivity_response::<32>(1e-3, core::f64::consts::PI)
.expect("sensitivity ok");
let _crossover = sensitivity_crossover(&data);
}
}