use std::borrow::Borrow;
use std::ops::AddAssign;
use num_complex::Complex;
use num_traits::{Float, FloatConst};
use thiserror::Error;
use crate::system::roots_with_multiplicity;
use nalgebra::{ComplexField, RealField};
use super::Method;
use crate::{StateSpace, StateSpaceError, vieta_formula, Continuous, Discrete, Polynomial, TransferFunction, TransferFunctionWithDelay};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ZerosAtInfinity {
MinusOne,
KeepOneDelay,
}
#[derive(Clone, Debug, Error, PartialEq)]
pub enum MatchedZError {
#[error("The transfer function is zero")]
ZeroSystem,
#[error("Improper continuous-time transfer function (more zeros than poles) cannot be matched")]
Improper,
#[error("Pole at z = 0 (pure delay) has no rational continuous-time counterpart; see `to_continuous_with_delay`")]
PoleAtOrigin,
#[error("Pole at z = {re} on the negative real axis has no real continuous-time counterpart")]
NegativeRealPole {
re: f64,
},
#[error(transparent)]
StateSpace(#[from] StateSpaceError),
#[error("Delay {delay} is not a whole number of sampling periods")]
FractionalDelay {
delay: f64,
},
}
const CLUSTER_TOLERANCE: f64 = 1e-6;
const SNAP_TOLERANCE: f64 = 1e-12;
const FACTOR_TOLERANCE: f64 = 1e-10;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MatchedZ(pub ZerosAtInfinity);
impl<T: Float + FloatConst + AddAssign> Method<T, TransferFunction<T, Continuous>> for MatchedZ {
type Output = TransferFunction<T, Discrete>;
type Error = MatchedZError;
fn apply(&self, tf: &TransferFunction<T, Continuous>, ts: T) -> Result<Self::Output, Self::Error> {
matched(tf, ts, self.0)
}
}
impl<T: Float + FloatConst + AddAssign + ComplexField + RealField> Method<T, StateSpace<T, Continuous>> for MatchedZ {
type Output = StateSpace<T, Discrete>;
type Error = MatchedZError;
fn apply(&self, system: &StateSpace<T, Continuous>, ts: T) -> Result<Self::Output, Self::Error> {
let tf = system.transfer_function()?;
Ok(StateSpace::try_from(&matched(&tf, ts, self.0)?)?)
}
}
impl<T: Float + FloatConst + AddAssign> Method<T, TransferFunctionWithDelay<T>> for MatchedZ {
type Output = TransferFunction<T, Discrete>;
type Error = MatchedZError;
fn apply(&self, g: &TransferFunctionWithDelay<T>, ts: T) -> Result<Self::Output, Self::Error> {
let samples = g.delay / ts;
let d = samples.round();
if (samples - d).abs() > T::from(1e-6).unwrap() {
return Err(MatchedZError::FractionalDelay { delay: g.delay.to_f64().unwrap_or(f64::NAN) });
}
let d = d.to_i64().unwrap();
let gz = matched(&g.tf, ts, self.0)?;
let times_z_power = |p: &Polynomial<T>, n: i64| {
let mut c = p.0.clone();
c.extend(std::iter::repeat_n(T::zero(), n.max(0) as usize));
Polynomial(c)
};
Ok(TransferFunction::from_polynomials(times_z_power(&gz.numerator, -d), times_z_power(&gz.denominator, d)))
}
}
fn matched<T>(tf: &TransferFunction<T, Continuous>, ts: T, zeros_at_infinity: ZerosAtInfinity) -> Result<TransferFunction<T, Discrete>, MatchedZError>
where
T: Float + FloatConst + AddAssign,
{
let (numer, a) = split_origin(&tf.numerator).ok_or(MatchedZError::ZeroSystem)?;
let (denom, b) = split_origin(&tf.denominator).ok_or(MatchedZError::ZeroSystem)?;
let degree_numer = numer.len() - 1 + a;
let degree_denom = denom.len() - 1 + b;
if degree_numer > degree_denom {
return Err(MatchedZError::Improper);
}
let relative_degree = degree_denom - degree_numer;
let to_minus_one = match zeros_at_infinity {
ZerosAtInfinity::MinusOne => relative_degree,
ZerosAtInfinity::KeepOneDelay => relative_degree.saturating_sub(1),
};
let map = |p: &Polynomial<T>| -> Vec<Complex<T>> {
roots(p).into_iter().flat_map(|(r, m)| std::iter::repeat_n((r * ts).exp(), m)).collect()
};
let mut zeros = map(&numer);
zeros.extend(std::iter::repeat_n(Complex::new(-T::one(), T::zero()), to_minus_one));
let mut poles = map(&denom);
let k = b as i32 - a as i32;
let asymptote = *numer.last().unwrap() / *denom.last().unwrap();
let gain = asymptote * ts.powi(k) * product(&poles, T::one()) / product(&zeros, T::one());
let one = Complex::new(T::one(), T::zero());
zeros.extend(std::iter::repeat_n(one, a));
poles.extend(std::iter::repeat_n(one, b));
Ok(TransferFunction::from_polynomials(real_polynomial(&zeros, gain), real_polynomial(&poles, T::one())))
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ToContinuousOptions<T> {
pub nyquist_tolerance: T,
}
impl<T: Float> Default for ToContinuousOptions<T> {
fn default() -> Self {
Self { nyquist_tolerance: T::zero() }
}
}
pub fn to_continuous<T, S>(tf: S, ts: T) -> Result<TransferFunction<T, Continuous>, MatchedZError>
where
T: Float + FloatConst + AddAssign,
S: Borrow<TransferFunction<T, Discrete>>,
{
to_continuous_with(tf, ts, &ToContinuousOptions::default())
}
pub fn to_continuous_with<T, S>(tf: S, ts: T, options: &ToContinuousOptions<T>) -> Result<TransferFunction<T, Continuous>, MatchedZError>
where
T: Float + FloatConst + AddAssign,
S: Borrow<TransferFunction<T, Discrete>>,
{
let tf = tf.borrow();
let (numer, _) = split_origin(&tf.numerator).ok_or(MatchedZError::ZeroSystem)?;
let (denom, poles_at_origin) = split_origin(&tf.denominator).ok_or(MatchedZError::ZeroSystem)?;
if poles_at_origin > 0 {
return Err(MatchedZError::PoleAtOrigin);
}
rational_part(&numer, &denom, ts, options)
}
pub fn to_continuous_with_delay<T, S>(tf: S, ts: T, options: &ToContinuousOptions<T>) -> Result<TransferFunctionWithDelay<T>, MatchedZError>
where
T: Float + FloatConst + AddAssign,
S: Borrow<TransferFunction<T, Discrete>>,
{
let tf = tf.borrow();
let (numer, zeros_at_origin) = split_origin(&tf.numerator).ok_or(MatchedZError::ZeroSystem)?;
let (denom, poles_at_origin) = split_origin(&tf.denominator).ok_or(MatchedZError::ZeroSystem)?;
let tf = rational_part(&numer, &denom, ts, options)?;
let shift = poles_at_origin as i32 - zeros_at_origin as i32;
Ok(TransferFunctionWithDelay { tf, delay: ts * T::from(shift).unwrap() })
}
fn rational_part<T>(
numer: &Polynomial<T>,
denom: &Polynomial<T>,
ts: T,
options: &ToContinuousOptions<T>,
) -> Result<TransferFunction<T, Continuous>, MatchedZError>
where
T: Float + FloatConst + AddAssign,
{
let (numer, a) = divide_out(numer, T::one());
let (denom, b) = divide_out(denom, T::one());
let k = b as i32 - a as i32;
let asymptote = horner(&numer, T::one()) / horner(&denom, T::one()) / ts.powi(k);
let (numer, _) = divide_out(&numer, -T::one());
let minus_one = Complex::new(-T::one(), T::zero());
let map = |p: &Polynomial<T>, is_numerator: bool| -> Result<Vec<Complex<T>>, MatchedZError> {
let mut s = Vec::new();
for (q, m) in roots(p) {
if is_numerator && (q - minus_one).norm() <= options.nyquist_tolerance {
continue;
}
if q.re.is_zero() && q.im.is_zero() {
if is_numerator {
continue;
}
return Err(MatchedZError::PoleAtOrigin);
}
if q.im.is_zero() && q.re < T::zero() {
if is_numerator {
continue;
}
return Err(MatchedZError::NegativeRealPole { re: q.re.to_f64().unwrap_or(f64::NAN) });
}
s.extend(std::iter::repeat_n(q.ln() / ts, m));
}
Ok(s)
};
let mut zeros = map(&numer, true)?;
let mut poles = map(&denom, false)?;
let gain = asymptote * product(&poles, T::zero()) / product(&zeros, T::zero());
let origin = Complex::new(T::zero(), T::zero());
zeros.extend(std::iter::repeat_n(origin, a));
poles.extend(std::iter::repeat_n(origin, b));
Ok(TransferFunction::from_polynomials(real_polynomial(&zeros, gain), real_polynomial(&poles, T::one())))
}
fn trim<T: Float>(p: &Polynomial<T>) -> Option<Polynomial<T>> {
let coeffs: Vec<T> = p.iter().copied().skip_while(|c| c.is_zero()).collect();
if coeffs.is_empty() { None } else { Some(Polynomial(coeffs)) }
}
fn split_origin<T: Float>(p: &Polynomial<T>) -> Option<(Polynomial<T>, usize)> {
let mut coeffs = trim(p)?.0;
let mut k = 0;
while coeffs.len() > 1 && coeffs.last().is_some_and(|c| c.is_zero()) {
coeffs.pop();
k += 1;
}
Some((Polynomial(coeffs), k))
}
fn divide_out<T: Float>(p: &Polynomial<T>, c: T) -> (Polynomial<T>, usize) {
let mut coeffs = p.0.clone();
let mut k = 0;
let tol = T::from(FACTOR_TOLERANCE).unwrap();
while coeffs.len() > 1 {
let scale = coeffs.iter().fold(T::zero(), |acc, &x| acc + x.abs());
let mut quotient = Vec::with_capacity(coeffs.len());
let mut acc = T::zero();
for &x in &coeffs {
acc = acc * c + x;
quotient.push(acc);
}
let remainder = quotient.pop().unwrap();
if remainder.abs() > tol * scale {
break;
}
coeffs = quotient;
k += 1;
}
(Polynomial(coeffs), k)
}
fn horner<T: Float>(p: &Polynomial<T>, x: T) -> T {
p.iter().fold(T::zero(), |acc, &c| acc * x + c)
}
fn roots<T: Float>(p: &Polynomial<T>) -> Vec<(Complex<T>, usize)> {
if p.len() <= 1 {
return Vec::new();
}
roots_with_multiplicity(p, T::from(CLUSTER_TOLERANCE).unwrap(), T::from(SNAP_TOLERANCE).unwrap())
}
fn product<T: Float>(roots: &[Complex<T>], x: T) -> T {
let x = Complex::new(x, T::zero());
roots.iter().fold(Complex::new(T::one(), T::zero()), |acc, &r| acc * (x - r)).re
}
fn real_polynomial<T: Float>(roots: &[Complex<T>], gain: T) -> Polynomial<T> {
Polynomial(vieta_formula(roots).0.iter().map(|c| c.re * gain).collect())
}