Skip to main content

bixverse_methods/methods/
coremo.rs

1//! Implementation of the CoReMo workflow from François, et al., Nat Commun,
2//! 2024
3
4use faer::MatRef;
5use rayon::prelude::*;
6use rustc_hash::FxHashMap;
7use std::collections::VecDeque;
8
9use crate::prelude::*;
10
11///////////////////////
12// Cluster stability //
13///////////////////////
14
15/// Helper functions to calculate the intersection of sorted usize vectors
16///
17/// ### Params
18///
19/// * `a` - (Sorted) slice of usize
20/// * `b` - (Sorted) slice of usize
21///
22/// ### Returns
23///
24/// Intersection between the two sorted slices.
25pub fn intersection_size_sorted(a: &[usize], b: &[usize]) -> usize {
26    let mut count = 0;
27    let mut i = 0;
28    let mut j = 0;
29
30    while i < a.len() && j < b.len() {
31        match a[i].cmp(&b[j]) {
32            std::cmp::Ordering::Equal => {
33                count += 1;
34                i += 1;
35                j += 1;
36            }
37            std::cmp::Ordering::Less => i += 1,
38            std::cmp::Ordering::Greater => j += 1,
39        }
40    }
41
42    count
43}
44
45/// Function that assesses the cluster stability.
46///
47/// ### Params
48///
49/// * `cluster_matrix` - A matrix with the columns representing the bootstraps,
50///   the rows the features and the values which cluster the feature belongs to.
51///
52/// ### Returns
53///
54/// Returns tuple of (average Jaccard similarities, standard deviations of the
55/// Jaccard similarities).
56pub fn cluster_stability<T>(cluster_matrix: &MatRef<i32>) -> Vec<(T, T)>
57where
58    T: BixverseFloat,
59{
60    let n_features = cluster_matrix.nrows();
61    let n_iter = cluster_matrix.ncols();
62
63    // Pre-compute cluster membership maps for all bootstraps
64    let bootstrap_cluster_maps: Vec<FxHashMap<i32, Vec<usize>>> = (0..n_iter)
65        .into_par_iter()
66        .map(|boot_idx| {
67            let mut clusters_map: FxHashMap<i32, Vec<usize>> = FxHashMap::default();
68            for feature_idx in 0..n_features {
69                let cluster_id = cluster_matrix[(feature_idx, boot_idx)];
70                clusters_map
71                    .entry(cluster_id)
72                    .or_default()
73                    .push(feature_idx);
74            }
75            clusters_map
76        })
77        .collect();
78
79    // Process features in parallel with optimized memory usage
80    (0..n_features)
81        .into_par_iter()
82        .map(|feature_idx| {
83            let n_pairs = (n_iter * (n_iter - 1)) / 2;
84            let mut jaccard_scores = Vec::with_capacity(n_pairs);
85
86            for i in 0..(n_iter - 1) {
87                for j in (i + 1)..n_iter {
88                    let cluster_i = cluster_matrix[(feature_idx, i)];
89                    let cluster_j = cluster_matrix[(feature_idx, j)];
90
91                    let members_i = bootstrap_cluster_maps[i]
92                        .get(&cluster_i)
93                        .map(|v| v.as_slice())
94                        .unwrap_or(&[]);
95
96                    let members_j = bootstrap_cluster_maps[j]
97                        .get(&cluster_j)
98                        .map(|v| v.as_slice())
99                        .unwrap_or(&[]);
100
101                    let intersection_size = intersection_size_sorted(members_i, members_j);
102                    let union_size = members_i.len() + members_j.len() - intersection_size;
103
104                    let jaccard = if union_size == 0 {
105                        0.0
106                    } else {
107                        intersection_size as f64 / union_size as f64
108                    };
109                    jaccard_scores.push(jaccard);
110                }
111            }
112
113            let mean_jaccard = jaccard_scores.iter().sum::<f64>() / jaccard_scores.len() as f64;
114            let variance = jaccard_scores
115                .iter()
116                .map(|x| (x - mean_jaccard).powi(2))
117                .sum::<f64>()
118                / jaccard_scores.len() as f64;
119            let std_jaccard = variance.sqrt();
120
121            (
122                T::from_f64(mean_jaccard).unwrap(),
123                T::from_f64(std_jaccard).unwrap(),
124            )
125        })
126        .collect()
127}
128
129/// Helper to split correlation matrices
130///
131/// Function will check the signs of the correlation matrix and return a vector
132/// of only `1` if everything has a positive sign, `-1` if everything has a
133/// negative sign and a mix if the correlation signs differ. Under the hood it
134/// uses a graph-based approach which is fast.
135///
136/// ### Params
137///
138/// * `cor_mat` - The correlation matrix to split
139///
140/// ### Returns
141///
142/// A vector containing -1 and/or 1, pending input correlation matrix.
143pub fn split_cor_mat_by_sign<T>(cor_mat: &MatRef<T>) -> Vec<i32>
144where
145    T: BixverseFloat,
146{
147    // assert_symmetric_mat!(cor_mat);
148
149    let n = cor_mat.ncols();
150    let total_elem = n * (n - 1) / 2;
151    let mut off_diag = Vec::with_capacity(total_elem);
152
153    for i in 0..n {
154        for j in (i + 1)..n {
155            off_diag.push(*cor_mat.get(i, j));
156        }
157    }
158
159    let all_pos = off_diag.iter().all(|x| *x >= T::zero());
160    let all_neg = off_diag.iter().all(|x| *x <= T::zero());
161
162    if all_pos {
163        return vec![1; n];
164    }
165    if all_neg {
166        return vec![-1; n];
167    }
168
169    let mut visited = vec![false; n];
170    let mut groups = Vec::new();
171
172    for i in 0..n {
173        if !visited[i] {
174            let mut component = Vec::new();
175            let mut queue = VecDeque::new();
176            queue.push_back(i);
177            visited[i] = true;
178
179            while let Some(node) = queue.pop_front() {
180                component.push(node);
181
182                for j in 0..n {
183                    if !visited[j] && *cor_mat.get(node, j) > T::zero() {
184                        visited[j] = true;
185                        queue.push_back(j);
186                    }
187                }
188            }
189            groups.push(component);
190        }
191    }
192
193    let mut result = vec![-1; n];
194    for group in groups {
195        // check if this group correlates positively with feature 0
196        let has_positive_with_first =
197            group.contains(&0) || group.iter().any(|&idx| *cor_mat.get(0, idx) > T::zero());
198
199        let label = if has_positive_with_first { 1 } else { -1 };
200        for &idx in &group {
201            result[idx] = label;
202        }
203    }
204
205    result
206}