1use modewise_decomposition::{Decomposition, Loadings, ModeDecomposer};
2use modewise_fourier::{from_half_spectrum, to_half_spectrum};
3use modewise_method::{MethodDescriptor, MethodError};
4use modewise_signals::MultichannelSeries;
5use ndarray::{Array1, Array2, Array3, Axis, Zip, s};
6use rustfft::num_complex::Complex64;
7use tracing::{debug, warn};
8
9use crate::lasso::lasso;
10use crate::params::VlmdParams;
11
12const EMPTY_MODE: f64 = 1e-10;
14
15#[derive(Debug, Clone)]
16pub struct Vlmd {
17 params: VlmdParams,
18}
19
20#[derive(Debug, Clone, PartialEq)]
23pub struct VlmdOutput {
24 pub modes: Decomposition,
25 pub latent_modes: Decomposition,
26 pub loadings: Loadings,
27 pub iterations: usize,
28}
29
30struct State {
31 loadings: Array2<f64>,
32 latents: Array2<f64>,
33 latents_hat: Array2<Complex64>,
34 modes_hat: Array3<Complex64>,
35 modes_sum: Array2<Complex64>,
36 dual: Array2<Complex64>,
37}
38
39impl Vlmd {
40 pub fn new(params: VlmdParams) -> Result<Self, MethodError> {
41 params.validate()?;
42 Ok(Self { params })
43 }
44
45 pub fn params(&self) -> &VlmdParams {
46 &self.params
47 }
48
49 pub fn decompose_latent(&self, signal: &MultichannelSeries) -> Result<VlmdOutput, MethodError> {
51 let p = &self.params;
52 let n_channels = signal.n_channels();
53 let n_latents = p.latents_for(n_channels)?;
54 let x = signal.data();
55 let x_hat = to_half_spectrum(x);
56 let n_bins = x_hat.ncols();
57 let freqs = Array1::linspace(0.0, 0.5, n_bins);
58 let penalty = p.reg_lambda * 1e-3;
59 let fit = 2.0 / p.reg_rho;
60 let narrowness = 4.0 * p.alpha / p.reg_rho;
61
62 let latents = x.slice(s![..n_latents, ..]).to_owned();
63 let mut state = State {
64 loadings: Array2::eye(n_latents.max(n_channels))
65 .slice_move(s![..n_latents, ..n_channels]),
66 latents_hat: to_half_spectrum(latents.view()),
67 latents,
68 modes_hat: Array3::zeros((p.num_modes, n_latents, n_bins)),
69 modes_sum: Array2::zeros((n_latents, n_bins)),
70 dual: Array2::zeros((n_latents, n_bins)),
71 };
72 let mut omegas = vec![0.0; p.num_modes];
73 let mut used = omegas.clone();
74 let mut change = p.tolerance + f64::EPSILON;
75 let mut iterations = 0;
76 while iterations < p.max_iterations && change > p.tolerance {
77 lasso(state.latents.view(), x, penalty, &mut state.loadings);
78 bound_loadings(&mut state.loadings);
79 update_latents(&mut state, &x_hat, fit);
80 let next = update_modes(&mut state, &freqs, &omegas, narrowness);
81 Zip::from(&mut state.dual)
82 .and(&state.latents_hat)
83 .and(&state.modes_sum)
84 .for_each(|g, z, m| *g += (z - m) * p.tau);
85 state.latents = from_half_spectrum(state.latents_hat.view());
86 change = omegas
87 .iter()
88 .zip(&next)
89 .map(|(old, new)| ((new - old) / (old + 1e-10)).powi(2))
90 .sum();
91 used = std::mem::replace(&mut omegas, next);
92 iterations += 1;
93 }
94 debug!(
95 iterations,
96 change,
97 converged = change <= p.tolerance,
98 "vlmd finished"
99 );
100 self.output(signal, state, &used, iterations)
101 }
102
103 fn output(
104 &self,
105 signal: &MultichannelSeries,
106 state: State,
107 omegas: &[f64],
108 iterations: usize,
109 ) -> Result<VlmdOutput, MethodError> {
110 let fs = signal.sampling_rate();
111 let (n_latents, n_channels) = state.loadings.dim();
112 let n_samples = signal.n_samples();
113 let mut order: Vec<usize> = (0..omegas.len()).collect();
114 order.sort_by(|&a, &b| omegas[a].total_cmp(&omegas[b]));
115
116 let mut latent_modes = Array3::zeros((order.len(), n_latents, n_samples));
117 let mut modes = Array3::zeros((order.len(), n_channels, n_samples));
118 for (i, &k) in order.iter().enumerate() {
119 let theta = from_half_spectrum(state.modes_hat.index_axis(Axis(0), k));
120 modes
121 .index_axis_mut(Axis(0), i)
122 .assign(&state.loadings.t().dot(&theta));
123 latent_modes.index_axis_mut(Axis(0), i).assign(&theta);
124 }
125 let centres: Vec<f64> = order.iter().map(|&k| omegas[k] * fs.hz()).collect();
126 let latent_labels: Vec<String> = (0..n_latents).map(|l| format!("latent_{l}")).collect();
127 Ok(VlmdOutput {
128 modes: Decomposition::new(modes, centres.clone(), fs, signal.labels().to_vec(), true)?,
129 latent_modes: Decomposition::new(
130 latent_modes,
131 centres,
132 fs,
133 latent_labels.clone(),
134 true,
135 )?,
136 loadings: Loadings::new(state.loadings, latent_labels, signal.labels().to_vec())?,
137 iterations,
138 })
139 }
140}
141
142fn bound_loadings(loadings: &mut Array2<f64>) {
144 for mut row in loadings.rows_mut() {
145 let largest = row.iter().fold(0.0f64, |m, v| m.max(v.abs()));
146 if largest > 1.0 {
147 row /= largest;
148 }
149 }
150}
151
152fn update_latents(state: &mut State, x_hat: &Array2<Complex64>, fit: f64) {
154 let mut residual = x_hat
155 - &state
156 .loadings
157 .t()
158 .mapv(|v| Complex64::new(v, 0.0))
159 .dot(&state.latents_hat);
160 for l in 0..state.loadings.nrows() {
161 let a = state.loadings.row(l);
162 let mut z = state.latents_hat.row_mut(l);
163 for (c, &acl) in a.iter().enumerate() {
164 if acl != 0.0 {
165 Zip::from(residual.row_mut(c))
166 .and(&z)
167 .for_each(|r, &zl| *r += zl * acl);
168 }
169 }
170 let mut projection = Array1::<Complex64>::zeros(z.len());
171 for (c, &acl) in a.iter().enumerate() {
172 if acl != 0.0 {
173 projection.scaled_add(Complex64::new(acl, 0.0), &residual.row(c));
174 }
175 }
176 let denominator = 1.0 + fit * a.dot(&a);
177 Zip::from(&mut z)
178 .and(&projection)
179 .and(state.modes_sum.row(l))
180 .and(state.dual.row(l))
181 .for_each(|z, &proj, &modes, &dual| *z = (proj * fit + modes - dual) / denominator);
182 for (c, &acl) in a.iter().enumerate() {
183 if acl != 0.0 {
184 Zip::from(residual.row_mut(c))
185 .and(&z)
186 .for_each(|r, &zl| *r -= zl * acl);
187 }
188 }
189 }
190}
191
192fn update_modes(
195 state: &mut State,
196 freqs: &Array1<f64>,
197 omegas: &[f64],
198 narrowness: f64,
199) -> Vec<f64> {
200 let mut next = omegas.to_vec();
201 for (k, mut mode) in state.modes_hat.axis_iter_mut(Axis(0)).enumerate() {
202 let (mut power, mut weighted) = (0.0, 0.0);
203 Zip::indexed(&mut mode)
204 .and(&mut state.modes_sum)
205 .and(&state.latents_hat)
206 .and(&state.dual)
207 .for_each(|(_, j), m, sum, &z, &g| {
208 let others = *sum - *m;
209 let updated =
210 (z - others + g) / (1.0 + narrowness * (freqs[j] - omegas[k]).powi(2));
211 *m = updated;
212 *sum = others + updated;
213 power += updated.norm_sqr();
214 weighted += updated.norm_sqr() * freqs[j];
215 });
216 next[k] = if power < EMPTY_MODE {
217 warn!(
218 mode = k,
219 "vlmd mode has no energy; its frequency is set to 0"
220 );
221 0.0
222 } else {
223 weighted / power
224 };
225 }
226 next
227}
228
229impl ModeDecomposer for Vlmd {
230 fn descriptor(&self) -> MethodDescriptor {
231 MethodDescriptor {
232 name: "vlmd",
233 version: env!("CARGO_PKG_VERSION"),
234 multivariate: true,
235 }
236 }
237
238 fn decompose(&self, signal: &MultichannelSeries) -> Result<Decomposition, MethodError> {
239 Ok(self.decompose_latent(signal)?.modes)
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn an_empty_mode_gets_frequency_zero_not_nan() {
249 let mut state = State {
250 loadings: Array2::eye(1),
251 latents: Array2::zeros((1, 4)),
252 latents_hat: Array2::zeros((1, 5)),
253 modes_hat: Array3::zeros((2, 1, 5)),
254 modes_sum: Array2::zeros((1, 5)),
255 dual: Array2::zeros((1, 5)),
256 };
257 let freqs = Array1::linspace(0.0, 0.5, 5);
258 assert_eq!(
259 update_modes(&mut state, &freqs, &[0.1, 0.3], 10.0),
260 [0.0, 0.0]
261 );
262 state.latents_hat[[0, 2]] = Complex64::new(1e-6, 0.0);
263 assert_eq!(
264 update_modes(&mut state, &freqs, &[0.1, 0.3], 10.0),
265 [0.0, 0.0],
266 "energy below 1e-10"
267 );
268 }
269
270 #[test]
271 fn rows_above_one_are_rescaled_and_others_left_alone() {
272 let mut a = ndarray::array![[2.0, -4.0], [0.5, -1.0]];
273 bound_loadings(&mut a);
274 assert_eq!(a, ndarray::array![[0.5, -1.0], [0.5, -1.0]]);
275 }
276}