use dsmc::discretize::matched_z_transform::{
to_continuous, to_continuous_with, to_continuous_with_delay, to_discrete, to_discrete_with_delay, ContinuousWithDelay,
MatchedZError, ToContinuousOptions, ZerosAtInfinity,
};
use dsmc::{tf, Continuous, Discrete, Polynomial, TransferFunction};
use num_complex::Complex;
const TS: f64 = 1e-3;
fn nyquist(tolerance: f64) -> ToContinuousOptions<f64> {
ToContinuousOptions { nyquist_tolerance: tolerance }
}
fn eval(p: &Polynomial<f64>, x: Complex<f64>) -> Complex<f64> {
p.iter().fold(Complex::new(0.0, 0.0), |acc, &c| acc * x + c)
}
fn response_s(g: &TransferFunction<f64, Continuous>, w: f64) -> Complex<f64> {
let s = Complex::new(0.0, w);
eval(&g.numerator, s) / eval(&g.denominator, s)
}
fn response_z(g: &TransferFunction<f64, Discrete>, w: f64) -> Complex<f64> {
let z = Complex::new(0.0, w * TS).exp();
eval(&g.numerator, z) / eval(&g.denominator, z)
}
fn normalized<D>(g: &TransferFunction<f64, D>) -> (Vec<f64>, Vec<f64>) {
let lead = g.denominator.iter().copied().find(|c| *c != 0.0).unwrap();
let trim = |p: &Polynomial<f64>| p.iter().copied().skip_while(|c| c.abs() < 1e-300).map(|c| c / lead).collect::<Vec<_>>();
(trim(&g.numerator), trim(&g.denominator))
}
fn assert_coeffs_close(actual: &[f64], expected: &[f64], tol: f64) {
assert_eq!(actual.len(), expected.len(), "{actual:?} vs {expected:?}");
for (a, e) in actual.iter().zip(expected) {
assert!((a - e).abs() <= tol * e.abs().max(1.0), "{actual:?} != {expected:?}");
}
}
fn plants() -> Vec<TransferFunction<f64, Continuous>> {
vec![
tf!("100 / (s + 100)"),
tf!("(s + 50) / ((s + 10) (s^2 + 40s + 10000))"),
tf!("(1 - 0.01s) / ((s + 20)^2)"), tf!("1 / (s (0.02s + 1))"), tf!("(s + 5) / (s^2 (s + 100))"), tf!("10 / (s + 0.001)"), tf!("s / ((s + 10) (s + 30))"), ]
}
#[test]
fn first_order() {
let a: f64 = 100.0;
let p = (-a * TS).exp();
let g = to_discrete(tf!("100 / (s + 100)"), TS, ZerosAtInfinity::MinusOne).unwrap();
let (n, d) = normalized(&g);
assert_coeffs_close(&n, &[(1.0 - p) / 2.0, (1.0 - p) / 2.0], 1e-12);
assert_coeffs_close(&d, &[1.0, -p], 1e-12);
let g = to_discrete(tf!("100 / (s + 100)"), TS, ZerosAtInfinity::KeepOneDelay).unwrap();
let (n, d) = normalized(&g);
assert_coeffs_close(&n, &[1.0 - p], 1e-12);
assert_coeffs_close(&d, &[1.0, -p], 1e-12);
}
#[test]
fn round_trip_s_z_s() {
for g in plants() {
for option in [ZerosAtInfinity::MinusOne, ZerosAtInfinity::KeepOneDelay] {
let back = to_continuous(to_discrete(&g, TS, option).unwrap(), TS).unwrap();
let (n0, d0) = normalized(&g);
let (n1, d1) = normalized(&back);
assert_coeffs_close(&n1, &n0, 1e-7);
assert_coeffs_close(&d1, &d0, 1e-7);
}
}
}
#[test]
fn round_trip_z_s_z() {
for g in plants() {
for option in [ZerosAtInfinity::MinusOne, ZerosAtInfinity::KeepOneDelay] {
let gz = to_discrete(&g, TS, option).unwrap();
let again = to_discrete(to_continuous(&gz, TS).unwrap(), TS, option).unwrap();
let (n0, d0) = normalized(&gz);
let (n1, d1) = normalized(&again);
assert_coeffs_close(&n1, &n0, 1e-7);
assert_coeffs_close(&d1, &d0, 1e-7);
}
}
}
#[test]
fn low_frequency_response() {
for g in plants() {
let gz = to_discrete(&g, TS, ZerosAtInfinity::MinusOne).unwrap();
for w in [0.1, 1.0, 10.0] {
let (hs, hz) = (response_s(&g, w), response_z(&gz, w));
assert!((hs - hz).norm() <= 2e-2 * hs.norm(), "{g:?} at {w}: {hs} vs {hz}");
}
}
}
#[test]
fn errors() {
let delay = TransferFunction::<f64, Discrete>::from_polynomials(Polynomial(vec![1.0]), Polynomial(vec![1.0, 0.0]));
assert_eq!(to_continuous(&delay, TS).unwrap_err(), MatchedZError::PoleAtOrigin);
let negative = TransferFunction::<f64, Discrete>::from_polynomials(Polynomial(vec![1.0]), Polynomial(vec![1.0, 0.5]));
assert_eq!(to_continuous(&negative, TS).unwrap_err(), MatchedZError::NegativeRealPole { re: -0.5 });
let improper = TransferFunction::<f64, Continuous>::from_polynomials(Polynomial(vec![1.0, 1.0, 1.0]), Polynomial(vec![1.0, 2.0]));
assert_eq!(to_discrete(&improper, TS, ZerosAtInfinity::MinusOne).unwrap_err(), MatchedZError::Improper);
}
#[test]
fn perturbed_nyquist_zeros() {
let g = tf!("1000 / (s^2 + 20s + 1000)");
let gz = to_discrete(&g, TS, ZerosAtInfinity::MinusOne).unwrap();
let mut numer = gz.numerator.clone();
let last = numer.len() - 1;
numer[last] += 1e-6 * numer[0];
let perturbed = TransferFunction::<f64, Discrete>::from_polynomials(numer, gz.denominator.clone());
let spurious = to_continuous(&perturbed, TS).unwrap();
assert_eq!(normalized(&spurious).0.len(), 3, "a spurious zero pair near the Nyquist frequency: {spurious:?}");
let back = to_continuous_with(&perturbed, TS, &nyquist(1e-2)).unwrap();
let (n0, d0) = normalized(&g);
let (n1, d1) = normalized(&back);
assert_coeffs_close(&d1, &d0, 1e-9);
assert_coeffs_close(&n1, &n0, 1e-5);
}
#[test]
fn unmappable_zeros_are_dropped() {
let g = tf!("1000 / (s^2 + 20s + 1000)");
let gz = to_discrete(&g, TS, ZerosAtInfinity::MinusOne).unwrap();
let dc = gz.numerator.iter().sum::<f64>();
let numer = Polynomial(vec![dc / 103.0, dc * 102.0 / 103.0, 0.0]);
let mismatched = TransferFunction::<f64, Discrete>::from_polynomials(numer, gz.denominator.clone());
let back = to_continuous(&mismatched, TS).unwrap();
let (n0, d0) = normalized(&g);
let (n1, d1) = normalized(&back);
assert_coeffs_close(&d1, &d0, 1e-9);
assert_coeffs_close(&n1, &n0, 1e-9);
let pole_negative = TransferFunction::<f64, Discrete>::from_polynomials(Polynomial(vec![1.0]), Polynomial(vec![1.0, 0.5]));
assert!(matches!(to_continuous(&pole_negative, TS), Err(MatchedZError::NegativeRealPole { .. })));
}
#[test]
fn dead_time() {
let g = tf!("(1 - 0.01s) / ((s + 20)^2)");
let delayed = ContinuousWithDelay { tf: g.clone(), delay: 3.0 * TS };
let gz = to_discrete_with_delay(&delayed, TS, ZerosAtInfinity::KeepOneDelay).unwrap();
let plain = to_discrete(&g, TS, ZerosAtInfinity::KeepOneDelay).unwrap();
assert_eq!(gz.denominator.len(), plain.denominator.len() + 3);
assert_eq!(to_continuous(&gz, TS).unwrap_err(), MatchedZError::PoleAtOrigin);
let back = to_continuous_with_delay(&gz, TS, &ToContinuousOptions::default()).unwrap();
assert!((back.delay - 3.0 * TS).abs() < 1e-15);
let (n0, d0) = normalized(&g);
let (n1, d1) = normalized(&back.tf);
assert_coeffs_close(&n1, &n0, 1e-7);
assert_coeffs_close(&d1, &d0, 1e-7);
let w = 50.0;
let rational = response_s(&g, w);
assert!((back.frequency_response(w) - rational * Complex::new(0.0, -w * 3.0 * TS).exp()).norm() < 1e-9 * rational.norm());
let fractional = ContinuousWithDelay { tf: g, delay: 2.5 * TS };
assert!(matches!(to_discrete_with_delay(&fractional, TS, ZerosAtInfinity::MinusOne), Err(MatchedZError::FractionalDelay { .. })));
}
#[test]
fn time_advance() {
let g = tf!("1000 / (s^2 + 20s + 1000)");
let gz = to_discrete(&g, TS, ZerosAtInfinity::MinusOne).unwrap();
let dc = gz.numerator.iter().sum::<f64>();
let numer = Polynomial(vec![dc / 103.0, dc * 102.0 / 103.0, 0.0]);
let mismatched = TransferFunction::<f64, Discrete>::from_polynomials(numer, gz.denominator.clone());
let back = to_continuous_with_delay(&mismatched, TS, &ToContinuousOptions::default()).unwrap();
assert!((back.delay + TS).abs() < 1e-15, "delay {}", back.delay);
let (n0, d0) = normalized(&g);
let (n1, d1) = normalized(&back.tf);
assert_coeffs_close(&d1, &d0, 1e-9);
assert_coeffs_close(&n1, &n0, 1e-9);
assert_eq!(format!("{:.3}", back), "exp(0.001 s) * (1000.000 / (s^2 + 20.000 * s + 1000.000))");
}