Skip to main content

lau_harmonic_analysis/
ztransform.rs

1//! Z-transform for discrete systems.
2
3use num_complex::Complex64;
4use serde::{Deserialize, Serialize};
5
6/// Z-transform: X(z) = sum_{n=0}^{N-1} x[n] * z^{-n}
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ZTransform;
9
10impl ZTransform {
11    /// Compute the Z-transform of a discrete signal at a given complex point z.
12    pub fn transform(signal: &[f64], z: Complex64) -> Complex64 {
13        signal.iter().enumerate().map(|(n, &x)| {
14            x * z.powi(-(n as i32))
15        }).sum()
16    }
17
18    /// Evaluate the Z-transform on the unit circle (z = e^{jω}), equivalent to the DTFT.
19    pub fn dtft(signal: &[f64], omega: f64) -> Complex64 {
20        let z = Complex64::from_polar(1.0, omega);
21        Self::transform(signal, z)
22    }
23
24    /// Compute the frequency response at multiple frequencies.
25    pub fn frequency_response(signal: &[f64], frequencies: &[f64]) -> Vec<Complex64> {
26        frequencies.iter().map(|&omega| Self::dtft(signal, omega)).collect()
27    }
28
29    /// Compute the magnitude response in dB.
30    pub fn magnitude_response_db(signal: &[f64], frequencies: &[f64]) -> Vec<f64> {
31        Self::frequency_response(signal, frequencies)
32            .iter()
33            .map(|h| 20.0 * h.norm().log10())
34            .collect()
35    }
36
37    /// Compute the phase response in radians.
38    pub fn phase_response(signal: &[f64], frequencies: &[f64]) -> Vec<f64> {
39        Self::frequency_response(signal, frequencies)
40            .iter()
41            .map(|h| h.arg())
42            .collect()
43    }
44
45    /// Check stability of an FIR filter (all zeros inside unit circle).
46    /// For IIR, you'd need pole analysis.
47    pub fn zeros(signal: &[f64]) -> Vec<Complex64> {
48        let n = signal.len();
49        if n <= 1 {
50            return vec![];
51        }
52        // Find roots of the polynomial sum a_k * z^{-k} = 0
53        // Equivalently, sum a_k * z^{N-1-k} = 0
54        let mut coeffs: Vec<f64> = signal.iter().rev().cloned().collect();
55        Self::find_roots(&coeffs)
56    }
57
58    /// Simple root finding via companion matrix eigenvalues (using nalgebra).
59    fn find_roots(coeffs: &[f64]) -> Vec<Complex64> {
60        let n = coeffs.len();
61        if n <= 1 {
62            return vec![];
63        }
64
65        // Normalize by leading coefficient
66        let a0 = coeffs[0];
67        if a0.abs() < 1e-30 {
68            return vec![];
69        }
70
71        // Build companion matrix
72        let size = n - 1;
73        use nalgebra::DMatrix;
74        let mut companion = DMatrix::<f64>::zeros(size, size);
75
76        // Last column: -a_n/a_0, -a_{n-1}/a_0, ...
77        for i in 0..size {
78            companion[(i, size - 1)] = -coeffs[n - 1 - i] / a0;
79        }
80        // Sub-diagonal: 1s
81        for i in 1..size {
82            companion[(i, i - 1)] = 1.0;
83        }
84
85        let eigenvalues = companion.complex_eigenvalues();
86        eigenvalues.iter().map(|c| Complex64::new(c.re, c.im)).collect()
87    }
88
89    /// Transfer function of a first-order system: H(z) = b0 + b1*z^{-1} / (1 + a1*z^{-1})
90    pub fn first_order_transfer(z: Complex64, b0: f64, b1: f64, a1: f64) -> Complex64 {
91        let num = b0 + b1 * z.powi(-1);
92        let den = 1.0 + a1 * z.powi(-1);
93        num / den
94    }
95
96    /// Compute group delay at a given frequency.
97    pub fn group_delay(signal: &[f64], omega: f64, d_omega: f64) -> f64 {
98        let h1 = Self::dtft(signal, omega - d_omega / 2.0);
99        let h2 = Self::dtft(signal, omega + d_omega / 2.0);
100        let phase_diff = h2.arg() - h1.arg();
101        -phase_diff / d_omega
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_ztransform_delta() {
111        // δ[n]: X(z) = 1
112        let signal = vec![1.0];
113        let z = Complex64::new(2.0, 0.0);
114        let result = ZTransform::transform(&signal, z);
115        assert!((result.re - 1.0).abs() < 1e-10);
116    }
117
118    #[test]
119    fn test_ztransform_unit_step() {
120        // u[n] for N=4: X(z) = 1 + z^{-1} + z^{-2} + z^{-3}
121        let signal = vec![1.0, 1.0, 1.0, 1.0];
122        let z = Complex64::new(2.0, 0.0);
123        let result = ZTransform::transform(&signal, z);
124        let expected = 1.0 + 0.5 + 0.25 + 0.125;
125        assert!((result.re - expected).abs() < 1e-10);
126    }
127
128    #[test]
129    fn test_ztransform_geometric() {
130        // a^n for N=3: X(z) = a + a²/z + a³/z²
131        let a = 0.5;
132        let signal = vec![a, a * a, a * a * a];
133        let z = Complex64::new(1.0, 0.0);
134        let result = ZTransform::transform(&signal, z);
135        let expected = a + a * a + a * a * a;
136        assert!((result.re - expected).abs() < 1e-10);
137    }
138
139    #[test]
140    fn test_dtft_dc() {
141        let signal = vec![1.0, 1.0, 1.0, 1.0];
142        let result = ZTransform::dtft(&signal, 0.0);
143        assert!((result.re - 4.0).abs() < 1e-10);
144    }
145
146    #[test]
147    fn test_dtft_nyquist() {
148        let signal = vec![1.0, -1.0, 1.0, -1.0];
149        let result = ZTransform::dtft(&signal, std::f64::consts::PI);
150        assert!((result.re - 4.0).abs() < 1e-10);
151    }
152
153    #[test]
154    fn test_magnitude_response_db() {
155        let signal = vec![1.0, 0.0, 0.0, 0.0];
156        let freqs = vec![0.0, std::f64::consts::PI / 4.0];
157        let db = ZTransform::magnitude_response_db(&signal, &freqs);
158        // Delta has flat magnitude response (0 dB)
159        assert!((db[0] - 0.0).abs() < 1e-10);
160    }
161
162    #[test]
163    fn test_phase_response() {
164        let signal = vec![1.0, 0.0, 0.0, 0.0];
165        let freqs = vec![0.0];
166        let phase = ZTransform::phase_response(&signal, &freqs);
167        assert!(phase[0].abs() < 1e-10);
168    }
169
170    #[test]
171    fn test_first_order_transfer() {
172        let z = Complex64::new(1.0, 0.0);
173        let h = ZTransform::first_order_transfer(z, 1.0, 0.5, -0.3);
174        let expected = (1.0 + 0.5) / (1.0 - 0.3);
175        assert!((h.re - expected).abs() < 1e-10);
176    }
177
178    #[test]
179    fn test_group_delay() {
180        let signal = vec![1.0, 0.0, 0.0, 0.0];
181        let gd = ZTransform::group_delay(&signal, 0.5, 0.01);
182        // Delta has 0 group delay everywhere
183        assert!(gd.abs() < 0.5, "Group delay of delta should be ~0: {gd}");
184    }
185}