modewise_hilbert/
analytic.rs1use std::f64::consts::PI;
2
3use modewise_decomposition::{Decomposition, InstantaneousModes};
4use modewise_method::MethodError;
5use ndarray::{Array3, ArrayView1, Axis};
6use rustfft::FftPlanner;
7use rustfft::num_complex::Complex64;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum Padding {
12 #[default]
14 Mirror,
15 None,
17}
18
19pub fn analytic_signal(x: ArrayView1<f64>, padding: Padding) -> Vec<Complex64> {
22 let n = x.len();
23 if n == 0 {
24 return Vec::new();
25 }
26 let (extended, offset): (Vec<f64>, usize) = match padding {
27 Padding::None => (x.to_vec(), 0),
28 Padding::Mirror => (x.iter().chain(x.iter().rev()).copied().collect(), 0),
31 };
32 let m = extended.len();
33 let mut planner = FftPlanner::new();
34 let mut buffer: Vec<Complex64> = extended.iter().map(|&v| Complex64::new(v, 0.0)).collect();
35 planner.plan_fft_forward(m).process(&mut buffer);
36 for (k, value) in buffer.iter_mut().enumerate() {
37 let gain = if k == 0 || (m % 2 == 0 && k == m / 2) {
38 1.0
39 } else if k < m.div_ceil(2) {
40 2.0
41 } else {
42 0.0
43 };
44 *value *= gain;
45 }
46 planner.plan_fft_inverse(m).process(&mut buffer);
47 let scale = 1.0 / m as f64;
48 buffer[offset..offset + n]
49 .iter()
50 .map(|z| z * scale)
51 .collect()
52}
53
54pub fn instantaneous_frequency(z: &[Complex64], fs: f64) -> Vec<f64> {
59 let n = z.len();
60 let to_hz = |radians: f64| radians * fs / (2.0 * PI);
61 let step: Vec<f64> = z.windows(2).map(|w| (w[1] * w[0].conj()).arg()).collect();
62 match n {
63 1 => vec![0.0],
64 2 => vec![to_hz(step[0]); 2],
65 _ => (0..n)
66 .map(|i| {
67 let first_step = i.clamp(1, n - 2) - 1;
68 to_hz(step[first_step] + step[first_step + 1]) / 2.0
69 })
70 .collect(),
71 }
72}
73
74pub fn instantaneous(
76 modes: &Decomposition,
77 padding: Padding,
78) -> Result<InstantaneousModes, MethodError> {
79 let fs = modes.sampling_rate().hz();
80 let dim = modes.modes().dim();
81 let mut amplitude = Array3::zeros(dim);
82 let mut frequency = Array3::zeros(dim);
83 for (k, mode) in modes.modes().axis_iter(Axis(0)).enumerate() {
84 for (c, signal) in mode.axis_iter(Axis(0)).enumerate() {
85 let z = analytic_signal(signal, padding);
86 for (t, (value, f)) in z.iter().zip(instantaneous_frequency(&z, fs)).enumerate() {
87 amplitude[[k, c, t]] = value.norm();
88 frequency[[k, c, t]] = f;
89 }
90 }
91 }
92 InstantaneousModes::new(
93 amplitude,
94 frequency,
95 modes.centre_frequencies_hz().to_vec(),
96 modes.sampling_rate(),
97 modes.labels().to_vec(),
98 )
99}
100
101#[cfg(test)]
102mod tests {
103 use ndarray::Array1;
104
105 use super::*;
106
107 #[test]
108 fn analytic_signal_of_cosine_is_complex_exponential() {
109 for n in [64usize, 65] {
110 let k = 5.0;
111 let x = Array1::from_shape_fn(n, |t| (2.0 * PI * k * t as f64 / n as f64).cos());
112 let z = analytic_signal(x.view(), Padding::None);
113 for (t, value) in z.iter().enumerate() {
114 let expected = Complex64::from_polar(1.0, 2.0 * PI * k * t as f64 / n as f64);
115 assert!((value - expected).norm() < 1e-12, "n={n} t={t}");
116 }
117 }
118 }
119
120 #[test]
121 fn edge_cases() {
122 assert!(analytic_signal(Array1::<f64>::zeros(0).view(), Padding::Mirror).is_empty());
123 assert_eq!(instantaneous_frequency(&[], 1.0), Vec::<f64>::new());
124 assert_eq!(
125 instantaneous_frequency(&[Complex64::new(1.0, 0.0)], 1.0),
126 [0.0]
127 );
128 let quarter = [Complex64::new(1.0, 0.0), Complex64::new(0.0, 1.0)];
129 assert_eq!(
130 instantaneous_frequency(&quarter, 4.0),
131 [1.0, 1.0],
132 "a quarter turn per sample at 4 Hz is 1 Hz"
133 );
134 }
135}