bixverse_methods/methods/
snf.rs1use ann_search_rs::prelude::SimdDistance;
5use faer::{Mat, MatRef};
6use rayon::prelude::*;
7use std::collections::BinaryHeap;
8
9use crate::prelude::*;
10use bixverse_core::core::base::cors_similarity::*;
11use bixverse_core::core::math::matrix_helpers::*;
12
13fn affinity_from_distance<T>(dist: &MatRef<T>, k: usize, mu: T) -> Mat<T>
33where
34 T: BixverseFloat + std::iter::Sum,
35{
36 let n = dist.nrows();
37
38 let knn_avg_dist: Vec<T> = (0..n)
40 .into_par_iter()
41 .map_init(Vec::new, |distances: &mut Vec<T>, i| {
42 distances.clear();
43 distances.extend(
44 dist.col(i)
45 .iter()
46 .enumerate()
47 .filter(|&(j, _)| j != i)
48 .map(|(_, &d)| d),
49 );
50
51 let k_actual = k.min(distances.len());
52 if k_actual < distances.len() {
53 distances.select_nth_unstable_by(k_actual, |a, b| a.partial_cmp(b).unwrap());
54 }
55 distances[..k_actual].sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
56
57 let dist_sum: T = distances[..k_actual].iter().cloned().sum();
58 dist_sum / T::from_usize(k_actual).unwrap()
59 })
60 .collect();
61
62 let mut affinity = Mat::zeros(n, n);
64
65 let three = T::from_f32(3.0).unwrap();
66 let two = T::from_f32(2.0).unwrap();
67
68 affinity
69 .par_col_iter_mut()
70 .enumerate()
71 .for_each(|(j, mut col)| {
72 for i in 0..n {
73 if i == j {
74 col[i] = T::one();
75 continue;
76 }
77 let (lo, hi) = if i < j { (i, j) } else { (j, i) };
78 let dij = dist[(i, j)];
79 let sigma = mu * (knn_avg_dist[lo] + knn_avg_dist[hi] + dij) / three;
80
81 col[i] = if sigma < T::epsilon() {
82 T::zero()
83 } else {
84 (-dij.powi(2) / (two * sigma.powi(2))).exp()
85 };
86 }
87 });
88
89 affinity
90}
91
92fn knn_threshold<T>(mat: &MatRef<T>, k: usize) -> Vec<Vec<(usize, T)>>
106where
107 T: BixverseFloat + std::iter::Sum,
108{
109 let n = mat.nrows();
110
111 let mat_t = mat.transpose().to_owned();
114
115 (0..n)
116 .into_par_iter()
117 .map(|i| {
118 let mut heap = BinaryHeap::with_capacity(k + 1);
119
120 for (j, &sim) in mat_t.col(i).iter().enumerate() {
121 if i == j {
122 continue;
123 }
124 let entry = (RevOrderedFloat(sim), j);
125 if heap.len() < k {
126 heap.push(entry);
127 } else if heap.peek().is_some_and(|top| entry < *top) {
128 heap.pop();
129 heap.push(entry);
130 }
131 }
132
133 let mut neighbors: Vec<(usize, T)> = heap
134 .into_iter()
135 .map(|elem| (elem.1, elem.0.get_value()))
136 .collect();
137 neighbors.sort_unstable_by_key(|&(j, _)| j);
138
139 let knn_sum: T = neighbors.iter().map(|(_, v)| *v).sum();
140
141 neighbors
142 .into_iter()
143 .map(|(idx, val)| (idx, val / knn_sum))
144 .collect()
145 })
146 .collect()
147}
148
149fn b0_normalise<T>(mat: &MatRef<T>, alpha: T) -> Mat<T>
164where
165 T: BixverseFloat + std::iter::Sum,
166{
167 let n = mat.nrows();
168 let normalised = normalise_rows_l1(mat);
169
170 let half = T::from_f32(0.5).unwrap();
171 let two = T::from_f32(2.0).unwrap();
172
173 Mat::from_fn(n, n, |i, j| {
174 if i == j {
175 half
176 } else {
177 *normalised.get(i, j) / (two * alpha)
178 }
179 })
180}
181
182pub fn snf_gower_dist<T>(mat: &MatRef<T>, is_cat: &[bool], ranges: Option<&[T]>) -> Mat<T>
196where
197 T: BixverseFloat,
198{
199 let (nrows, ncols) = mat.shape();
200
201 assert!(
202 is_cat.len() == nrows,
203 "The categorical vector length {} doesn't match features {}",
204 is_cat.len(),
205 nrows
206 );
207
208 let computed_ranges: Vec<T> = if let Some(r) = ranges {
210 assert!(
211 r.len() == nrows,
212 "The range vector length {} doesn't match features {}",
213 r.len(),
214 nrows
215 );
216
217 r.to_vec()
218 } else {
219 (0..nrows)
220 .into_par_iter()
221 .map(|i| {
222 if is_cat[i] {
223 T::one()
224 } else {
225 let mut min_val = T::infinity();
226 let mut max_val = T::neg_infinity();
227 for j in 0..ncols {
228 let val = *mat.get(i, j);
229 min_val = min_val.min(val);
230 max_val = max_val.max(val);
231 }
232 let range = max_val - min_val;
233 if range < T::epsilon() {
234 T::one()
235 } else {
236 range
237 }
238 }
239 })
240 .collect()
241 };
242
243 let order: Vec<usize> = (0..nrows)
246 .filter(|&k| !is_cat[k])
247 .chain((0..nrows).filter(|&k| is_cat[k]))
248 .collect();
249 let n_cont = is_cat.iter().filter(|&&c| !c).count();
250 let ranges_ordered: Vec<T> = order.iter().map(|&k| computed_ranges[k]).collect();
251 let ordered = Mat::from_fn(nrows, ncols, |r, c| mat[(order[r], c)]);
252 let n_features = T::from_usize(nrows).unwrap();
253
254 let mut res = Mat::zeros(ncols, ncols);
255
256 res.par_col_iter_mut().enumerate().for_each(|(j, mut col)| {
257 let col_j = ordered.col_as_slice(j);
258 for i in 0..j {
259 let col_i = ordered.col_as_slice(i);
260 let mut total_dist = T::zero();
261
262 for ((&a, &b), &range) in col_i[..n_cont]
263 .iter()
264 .zip(&col_j[..n_cont])
265 .zip(&ranges_ordered[..n_cont])
266 {
267 total_dist += (a - b).abs() / range;
268 }
269 for (&a, &b) in col_i[n_cont..].iter().zip(&col_j[n_cont..]) {
270 if (a - b).abs() >= T::epsilon() {
271 total_dist += T::one();
272 }
273 }
274
275 col[i] = total_dist / n_features;
276 }
277 });
278
279 for j in 0..ncols {
280 for i in 0..j {
281 res[(j, i)] = res[(i, j)];
282 }
283 }
284
285 res
286}
287
288pub fn make_affinity_continuous<T>(
306 data: &MatRef<T>,
307 distance_type: &str,
308 k: usize,
309 mu: T,
310 normalise: bool,
311) -> Mat<T>
312where
313 T: BixverseFloat + std::iter::Sum + SimdDistance,
314{
315 let dist_type = parse_distance_type(distance_type).unwrap_or_default();
316
317 let normalised_data = if normalise {
318 scale_matrix_col(data, true)
319 } else {
320 data.to_owned()
321 };
322
323 let dist_mat = match dist_type {
324 DistanceType::L1Norm => column_pairwise_l1_norm(&normalised_data.as_ref()),
325 DistanceType::L2Norm => column_pairwise_l2_norm(&normalised_data.as_ref()),
326 DistanceType::Cosine => column_pairwise_cosine_dist(&normalised_data.as_ref()),
327 DistanceType::Canberra => column_pairwise_canberra_dist(&normalised_data.as_ref()),
328 DistanceType::Correlation => {
329 let cor = column_pairwise_cor(&normalised_data.as_ref(), false);
330 Mat::from_fn(cor.nrows(), cor.ncols(), |i, j| T::one() - cor[(i, j)])
331 }
332 };
333
334 affinity_from_distance(&dist_mat.as_ref(), k, mu)
335}
336
337pub fn make_affinity_mixed<T>(data: &MatRef<T>, is_cat: &[bool], k: usize, mu: T) -> Mat<T>
350where
351 T: BixverseFloat + std::iter::Sum,
352{
353 let dist_mat = snf_gower_dist(data, is_cat, None);
354 affinity_from_distance(&dist_mat.as_ref(), k, mu)
355}
356
357pub fn make_affinity_categorical<T>(data: &MatRef<i32>, k: usize, mu: T) -> Mat<T>
369where
370 T: BixverseFloat + std::iter::Sum,
371{
372 let dist_mat = column_pairwise_hamming_cat(data);
373 affinity_from_distance(&dist_mat.as_ref(), k, mu)
374}
375
376pub fn snf<T>(aff_mats: &[MatRef<T>], k: usize, t: usize, alpha: T) -> Mat<T>
397where
398 T: BixverseFloat + std::iter::Sum,
399{
400 assert!(
401 !aff_mats.is_empty(),
402 "At least one affinity matrix required"
403 );
404
405 let n = aff_mats[0].nrows();
406
407 for (i, mat) in aff_mats.iter().enumerate() {
408 assert_symmetric_mat!(mat);
409 assert!(
410 mat.nrows() == n,
411 "Matrix {} has different size: {} x {} (expected {} x {})",
412 i,
413 mat.nrows(),
414 mat.ncols(),
415 n,
416 n
417 );
418 }
419
420 let m = aff_mats.len();
421
422 let mut aff = aff_mats
423 .par_iter()
424 .map(|mat| normalise_rows_l1(&mat.as_ref()))
425 .collect::<Vec<Mat<T>>>();
426
427 let wk = aff
428 .par_iter()
429 .map(|mat_i| knn_threshold(&mat_i.as_ref(), k))
430 .collect::<Vec<Vec<Vec<(usize, T)>>>>();
431
432 let mut p_sum: Mat<T> = Mat::zeros(n, n);
434 let mut wk_p: Mat<T> = Mat::zeros(n, n);
435 let mut fused: Mat<T> = Mat::zeros(n, n);
436 let n_others = T::from_f64((m - 1) as f64).unwrap();
437
438 for _ in 0..t {
439 for v in 0..m {
440 p_sum.par_col_iter_mut().enumerate().for_each(|(c, col)| {
442 let col = col.try_as_col_major_mut().unwrap().as_slice_mut();
443 col.fill(T::zero());
444 for (idx, mat) in aff.iter().enumerate() {
445 if idx != v {
446 for (o, &x) in col.iter_mut().zip(mat.col_as_slice(c)) {
447 *o += x;
448 }
449 }
450 }
451 for o in col.iter_mut() {
452 *o /= n_others;
453 }
454 });
455
456 let rows = &wk[v];
458 wk_p.par_col_iter_mut().enumerate().for_each(|(c, col)| {
459 let col = col.try_as_col_major_mut().unwrap().as_slice_mut();
460 let p_col = p_sum.col_as_slice(c);
461 for (o, row) in col.iter_mut().zip(rows) {
462 let mut acc = T::zero();
463 for &(j, val) in row {
464 acc += val * p_col[j];
465 }
466 *o = acc;
467 }
468 });
469 fused.par_col_iter_mut().enumerate().for_each(|(l, col)| {
470 let col = col.try_as_col_major_mut().unwrap().as_slice_mut();
471 col.fill(T::zero());
472 for &(j, val) in &rows[l] {
473 for (o, &x) in col.iter_mut().zip(wk_p.col_as_slice(j)) {
474 *o += val * x;
475 }
476 }
477 });
478
479 aff[v] = b0_normalise(&fused.as_ref(), alpha)
480 }
481 }
482
483 let mut w: Mat<T> = aff
485 .par_iter()
486 .cloned()
487 .reduce(|| Mat::zeros(n, n), |acc, mat| acc + mat);
488
489 w = &w / m as f64;
490
491 w = normalise_rows_l1(&w.as_ref());
492
493 w = (&w + w.transpose()) / 2.0;
494
495 for i in 0..n {
496 w[(i, i)] = T::one();
497 }
498
499 w
500}