stats_claw/algorithms/decomposition/
ica.rs1use crate::algorithms::decomposition::{
17 at, count_to_f64, descending_order, jacobi_eigen, mean_center,
18};
19use crate::rng::SplitMix64;
20
21#[derive(Debug, Clone)]
23pub struct IcaResult {
24 pub sources: Vec<Vec<f64>>,
27}
28
29#[must_use]
62pub fn fast_ica(
63 mixed: &[Vec<f64>],
64 n_components: usize,
65 max_iter: usize,
66 tol: f64,
67 seed: u64,
68) -> IcaResult {
69 let dim = mixed.first().map_or(0, Vec::len);
70 let k = n_components.min(dim);
71 let n = mixed.len();
72 if dim == 0 || k == 0 || n == 0 {
73 return IcaResult {
74 sources: Vec::new(),
75 };
76 }
77 let whitened = whiten(mixed, dim, k);
78 let unmixing = symmetric_fastica(&whitened, k, max_iter, tol, seed);
79 let sources = project(&whitened, &unmixing, k);
80 IcaResult { sources }
81}
82
83fn whiten(mixed: &[Vec<f64>], dim: usize, k: usize) -> Vec<Vec<f64>> {
89 let (centered, _means) = mean_center(mixed, dim);
90 let mut cov = vec![0.0_f64; dim * dim];
91 for row in ¢ered {
92 for i in 0..dim {
93 let ri = row.get(i).copied().unwrap_or(0.0);
94 for j in 0..dim {
95 let rj = row.get(j).copied().unwrap_or(0.0);
96 if let Some(slot) = cov.get_mut(i * dim + j) {
97 *slot = ri.mul_add(rj, *slot);
98 }
99 }
100 }
101 }
102 let denom = count_to_f64(centered.len());
103 if denom > 0.0 {
104 for c in &mut cov {
105 *c /= denom;
106 }
107 }
108 let (values, vectors) = jacobi_eigen(&cov, dim);
109 let order = descending_order(&values);
110 let top: Vec<usize> = order.into_iter().take(k).collect();
111 centered
112 .iter()
113 .map(|row| {
114 top.iter()
115 .map(|&col| {
116 let eig = values.get(col).copied().unwrap_or(0.0).max(1e-12);
117 let projection: f64 = (0..dim)
118 .map(|f| row.get(f).copied().unwrap_or(0.0) * at(&vectors, dim, f, col))
119 .sum();
120 projection / eig.sqrt()
121 })
122 .collect()
123 })
124 .collect()
125}
126
127#[allow(clippy::many_single_char_names)]
135fn symmetric_fastica(
136 whitened: &[Vec<f64>],
137 k: usize,
138 max_iter: usize,
139 tol: f64,
140 seed: u64,
141) -> Vec<Vec<f64>> {
142 let mut rng = SplitMix64::new(seed);
143 let mut w: Vec<Vec<f64>> = (0..k)
144 .map(|_| (0..k).map(|_| rng.standard_normal()).collect())
145 .collect();
146 symmetric_decorrelate(&mut w, k);
147
148 let n = count_to_f64(whitened.len()).max(1.0);
149 for _ in 0..max_iter {
150 let mut next = vec![vec![0.0_f64; k]; k];
151 for (c, wc) in w.iter().enumerate() {
152 let mut expectation = vec![0.0_f64; k];
154 let mut mean_gprime = 0.0_f64;
155 for row in whitened {
156 let u: f64 = row.iter().zip(wc).map(|(&x, &wi)| x * wi).sum();
157 let g = u.tanh();
158 let gprime = g.mul_add(-g, 1.0);
159 for (e, &x) in expectation.iter_mut().zip(row) {
160 *e = x.mul_add(g, *e);
161 }
162 mean_gprime += gprime;
163 }
164 mean_gprime /= n;
165 if let Some(target) = next.get_mut(c) {
166 for (slot, (&e, &wi)) in target.iter_mut().zip(expectation.iter().zip(wc)) {
167 *slot = (-mean_gprime).mul_add(wi, e / n);
168 }
169 }
170 }
171 symmetric_decorrelate(&mut next, k);
172 let delta = max_abs_change(&w, &next, k);
173 w = next;
174 if delta < tol {
175 break;
176 }
177 }
178 w
179}
180
181fn symmetric_decorrelate(w: &mut [Vec<f64>], k: usize) {
184 let mut gram = vec![0.0_f64; k * k];
185 for a in 0..k {
186 for b in 0..k {
187 let dot: f64 = w.get(a).zip(w.get(b)).map_or(0.0, |(wa, wb)| {
188 wa.iter().zip(wb).map(|(&x, &y)| x * y).sum()
189 });
190 if let Some(slot) = gram.get_mut(a * k + b) {
191 *slot = dot;
192 }
193 }
194 }
195 let (values, vectors) = jacobi_eigen(&gram, k);
196 let mut inv_sqrt = vec![0.0_f64; k * k];
198 for i in 0..k {
199 for j in 0..k {
200 let mut acc = 0.0_f64;
201 for (e, &lambda) in values.iter().enumerate().take(k) {
202 let safe = lambda.max(1e-12).sqrt();
203 acc += at(&vectors, k, i, e) * at(&vectors, k, j, e) / safe;
204 }
205 put(&mut inv_sqrt, k, i, j, acc);
206 }
207 }
208 let original: Vec<Vec<f64>> = w.iter().map(Clone::clone).collect();
209 for (a, row) in w.iter_mut().enumerate().take(k) {
210 for (b, slot) in row.iter_mut().enumerate().take(k) {
211 *slot = (0..k)
212 .map(|m| {
213 at(&inv_sqrt, k, a, m)
214 * original
215 .get(m)
216 .and_then(|r| r.get(b))
217 .copied()
218 .unwrap_or(0.0)
219 })
220 .sum();
221 }
222 }
223}
224
225fn put(matrix: &mut [f64], n: usize, i: usize, j: usize, value: f64) {
227 if let Some(slot) = matrix.get_mut(i * n + j) {
228 *slot = value;
229 }
230}
231
232fn max_abs_change(a: &[Vec<f64>], b: &[Vec<f64>], k: usize) -> f64 {
234 let mut worst = 0.0_f64;
235 for row in 0..k {
236 for col in 0..k {
237 let av = a.get(row).and_then(|r| r.get(col)).copied().unwrap_or(0.0);
238 let bv = b.get(row).and_then(|r| r.get(col)).copied().unwrap_or(0.0);
239 worst = worst.max((av.abs() - bv.abs()).abs());
241 }
242 }
243 worst
244}
245
246fn project(whitened: &[Vec<f64>], unmixing: &[Vec<f64>], k: usize) -> Vec<Vec<f64>> {
249 let mut sources: Vec<Vec<f64>> = whitened
250 .iter()
251 .map(|row| {
252 (0..k)
253 .map(|c| {
254 unmixing
255 .get(c)
256 .map_or(0.0, |w| row.iter().zip(w).map(|(&x, &wi)| x * wi).sum())
257 })
258 .collect()
259 })
260 .collect();
261 let n = count_to_f64(sources.len()).max(1.0);
262 for c in 0..k {
263 let mean: f64 = sources
264 .iter()
265 .map(|r| r.get(c).copied().unwrap_or(0.0))
266 .sum::<f64>()
267 / n;
268 let var: f64 = sources
269 .iter()
270 .map(|r| {
271 let d = r.get(c).copied().unwrap_or(0.0) - mean;
272 d * d
273 })
274 .sum::<f64>()
275 / n;
276 let std = var.sqrt().max(1e-12);
277 for row in &mut sources {
278 if let Some(slot) = row.get_mut(c) {
279 *slot = (*slot - mean) / std;
280 }
281 }
282 }
283 sources
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 fn mixture() -> Vec<Vec<f64>> {
292 (0..200)
293 .map(|i| {
294 let t = f64::from(i) * 0.2;
295 let s1 = (2.0 * t).sin();
296 let s2 = if (3.0 * t).sin() >= 0.0 { 1.0 } else { -1.0 };
297 vec![0.6_f64.mul_add(s2, s1), 0.4_f64.mul_add(s1, 1.2 * s2)]
298 })
299 .collect()
300 }
301
302 #[test]
303 fn recovers_two_unit_variance_sources() {
304 let r = fast_ica(&mixture(), 2, 300, 1e-5, 3);
305 assert_eq!(r.sources.len(), 200, "source row count");
306 let n = count_to_f64(r.sources.len());
308 for c in 0..2 {
309 let var: f64 = r
310 .sources
311 .iter()
312 .map(|row| {
313 let v = row.get(c).copied().unwrap_or(0.0);
314 v * v
315 })
316 .sum::<f64>()
317 / n;
318 assert!((var - 1.0).abs() < 1e-6, "source {c} variance was {var}");
319 }
320 }
321
322 #[test]
323 fn deterministic_for_fixed_seed() {
324 let a = fast_ica(&mixture(), 2, 300, 1e-5, 9).sources;
325 let b = fast_ica(&mixture(), 2, 300, 1e-5, 9).sources;
326 let first_a = a.first().and_then(|r| r.first()).copied().unwrap_or(0.0);
327 let first_b = b.first().and_then(|r| r.first()).copied().unwrap_or(0.0);
328 assert_eq!(first_a.to_bits(), first_b.to_bits(), "non-deterministic");
329 }
330
331 #[test]
332 fn empty_input_is_empty() {
333 assert!(fast_ica(&[], 2, 10, 1e-4, 0).sources.is_empty());
334 }
335}