1use faer::{Mat, unzip, zip};
6use rayon::prelude::*;
7use rustc_hash::FxHashSet;
8use std::collections::BTreeMap;
9
10use crate::prelude::*;
11use bixverse_core::core::base::cors_similarity::{cor_two_matrices, set_similarity};
12use bixverse_core::core::mat_struct::NamedMatrix;
13use bixverse_core::utils::vec_utils::{array_max, flatten_vector};
14
15fn kth_largest<T: BixverseFloat>(values: &[T], k: usize) -> T {
30 let mut sorted: Vec<T> = values.to_vec();
31 sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
32 let idx = k.saturating_sub(1).min(sorted.len().saturating_sub(1));
33 sorted[idx]
34}
35
36fn names_by_index(names: &BTreeMap<String, usize>) -> Vec<&str> {
46 let mut pairs: Vec<(&str, usize)> = names.iter().map(|(k, &v)| (k.as_str(), v)).collect();
47 pairs.sort_by_key(|(_, idx)| *idx);
48 pairs.into_iter().map(|(k, _)| k).collect()
49}
50
51#[derive(Clone, Debug)]
57pub struct RbhTripletStruc<'a, T> {
58 pub t1: &'a str,
60 pub t2: &'a str,
62 pub sim: T,
64}
65
66#[derive(Clone, Debug)]
68#[allow(dead_code)]
69pub struct RbhResult<T> {
70 pub origin: String,
72 pub target: String,
74 pub origin_modules: Vec<String>,
76 pub target_modules: Vec<String>,
78 pub similarities: Vec<T>,
80}
81
82pub fn calculate_rbh_set<'a, T>(
109 origin_modules: &'a BTreeMap<String, FxHashSet<String>>,
110 target_modules: &'a BTreeMap<String, FxHashSet<String>>,
111 k: usize,
112 overlap_coefficient: bool,
113 min_similarity: T,
114) -> Vec<RbhTripletStruc<'a, T>>
115where
116 T: BixverseFloat,
117{
118 let names_targets: Vec<&String> = target_modules.keys().collect();
119 let names_origin: Vec<&String> = origin_modules.keys().collect();
120
121 let origin_sets: Vec<&FxHashSet<String>> = origin_modules.values().collect();
122 let target_sets: Vec<&FxHashSet<String>> = target_modules.values().collect();
123
124 let similarities_flat: Vec<Vec<T>> = origin_sets
125 .into_par_iter()
126 .map(|v1| {
127 target_sets
128 .iter()
129 .map(|v2| set_similarity(v1, v2, overlap_coefficient))
130 .collect()
131 })
132 .collect();
133
134 let mat_data: Vec<T> = flatten_vector(similarities_flat);
135
136 let max_sim = array_max(&mat_data);
137
138 if max_sim < min_similarity {
139 vec![RbhTripletStruc {
140 t1: "NA",
141 t2: "NA",
142 sim: T::zero(),
143 }]
144 } else {
145 let nrow = names_origin.len();
146 let ncol = names_targets.len();
147
148 let sim_mat = Mat::from_fn(nrow, ncol, |i, j| mat_data[j + i * ncol]);
149
150 let row_thresholds: Vec<T> = sim_mat
151 .row_iter()
152 .map(|x| {
153 let row: Vec<T> = x.iter().copied().collect();
154 kth_largest(&row, k)
155 })
156 .collect();
157
158 let col_thresholds: Vec<T> = sim_mat
159 .col_iter()
160 .map(|x| {
161 let row: Vec<T> = x.iter().copied().collect();
162 kth_largest(&row, k)
163 })
164 .collect();
165
166 let mut matching_pairs: Vec<RbhTripletStruc<T>> = Vec::new();
167
168 for r in 0..nrow {
169 for c in 0..ncol {
170 let value = sim_mat[(r, c)];
171 if value >= row_thresholds[r] && value >= col_thresholds[c] {
172 let triplet = RbhTripletStruc {
173 t1: names_origin[r],
174 t2: names_targets[c],
175 sim: value,
176 };
177
178 matching_pairs.push(triplet)
179 }
180 }
181 }
182
183 if !matching_pairs.is_empty() {
184 matching_pairs
185 } else {
186 vec![RbhTripletStruc {
187 t1: "NA",
188 t2: "NA",
189 sim: T::zero(),
190 }]
191 }
192 }
193}
194
195pub fn calculate_rbh_cor<'a, T>(
217 x1: &'a NamedMatrix<'a, T>,
218 x2: &'a NamedMatrix<'a, T>,
219 k: usize,
220 spearman: bool,
221) -> Vec<RbhTripletStruc<'a, T>>
222where
223 T: BixverseFloat,
224{
225 let row_names_1: FxHashSet<String> = x1.row_names.keys().cloned().collect();
226 let row_names_2: FxHashSet<String> = x2.row_names.keys().cloned().collect();
227
228 let names_targets: Vec<&str> = names_by_index(&x1.col_names);
229 let names_origin: Vec<&str> = names_by_index(&x2.col_names);
230
231 let intersecting_rows: Vec<String> = row_names_1.intersection(&row_names_2).cloned().collect();
232
233 if intersecting_rows.is_empty() {
235 return vec![RbhTripletStruc {
236 t1: "NA",
237 t2: "NA",
238 sim: T::zero(),
239 }];
240 }
241
242 let row_refs: Vec<&str> = intersecting_rows.iter().map(|s| s.as_str()).collect();
243 let sub_x1 = x1.get_rows(&row_refs).unwrap();
244 let sub_x2 = x2.get_rows(&row_refs).unwrap();
245
246 let mut correlations = cor_two_matrices(&sub_x1.as_ref(), &sub_x2.as_ref(), spearman);
247
248 zip!(correlations.as_mut()).for_each(|unzip!(x)| *x = x.abs());
249
250 let row_thresholds: Vec<T> = correlations
251 .row_iter()
252 .map(|x| {
253 let row: Vec<T> = x.iter().copied().collect();
254 kth_largest(&row, k)
255 })
256 .collect();
257
258 let col_thresholds: Vec<T> = correlations
259 .col_iter()
260 .map(|x| {
261 let col: Vec<T> = x.iter().copied().collect();
262 kth_largest(&col, k)
263 })
264 .collect();
265
266 let mut matching_pairs: Vec<RbhTripletStruc<'a, T>> = Vec::new();
267 let nrow = names_targets.len();
268 let ncol = names_origin.len();
269
270 for r in 0..nrow {
271 for c in 0..ncol {
272 let value = correlations[(r, c)];
273 if value >= row_thresholds[r] && value >= col_thresholds[c] {
274 let triplet = RbhTripletStruc {
275 t1: names_targets[r],
276 t2: names_origin[c],
277 sim: value,
278 };
279 matching_pairs.push(triplet);
280 }
281 }
282 }
283
284 if !matching_pairs.is_empty() {
285 matching_pairs
286 } else {
287 vec![RbhTripletStruc {
288 t1: "NA",
289 t2: "NA",
290 sim: T::zero(),
291 }]
292 }
293}
294
295#[cfg(test)]
300mod tests {
301 use super::*;
302
303 fn module(items: &[&str]) -> FxHashSet<String> {
308 items.iter().map(|s| s.to_string()).collect()
309 }
310
311 fn modules(pairs: &[(&str, &[&str])]) -> BTreeMap<String, FxHashSet<String>> {
312 pairs
313 .iter()
314 .map(|(n, it)| (n.to_string(), module(it)))
315 .collect()
316 }
317
318 fn three_by_three_modules() -> (
330 BTreeMap<String, FxHashSet<String>>,
331 BTreeMap<String, FxHashSet<String>>,
332 ) {
333 let origin = modules(&[
334 ("A", &["1", "2", "3", "4", "5", "6"]),
335 ("B", &["4", "5", "6", "7", "8", "9"]),
336 ("C", &["7", "8", "9", "10", "11", "12"]),
337 ]);
338 let target = modules(&[
339 ("X", &["1", "2", "3", "4", "5", "6"]),
340 ("Y", &["6", "7", "10", "11", "13", "14"]),
341 ("Z", &["7", "8", "9", "10", "11", "12"]),
342 ]);
343 (origin, target)
344 }
345
346 fn pair_set<T>(result: &[RbhTripletStruc<T>]) -> FxHashSet<(String, String)> {
347 result
348 .iter()
349 .map(|r| (r.t1.to_string(), r.t2.to_string()))
350 .collect()
351 }
352
353 #[test]
355 fn k1_identifies_clear_reciprocal_hits() {
356 let origin = modules(&[("A", &["g1", "g2", "g3"]), ("B", &["g4", "g5", "g6"])]);
357 let target = modules(&[("X", &["g1", "g2", "g3"]), ("Y", &["g4", "g5", "g6"])]);
358
359 let result = calculate_rbh_set(&origin, &target, 1, false, 0.0_f64);
360
361 assert_eq!(result.len(), 2);
362 let pairs = pair_set(&result);
363 assert!(pairs.contains(&("A".into(), "X".into())));
364 assert!(pairs.contains(&("B".into(), "Y".into())));
365 for r in &result {
366 assert!((r.sim - 1.0).abs() < 1e-12);
367 }
368 }
369
370 #[test]
372 fn k1_excludes_non_reciprocal_hits() {
373 let (origin, target) = three_by_three_modules();
374
375 let result = calculate_rbh_set(&origin, &target, 1, false, 0.0_f64);
376
377 let pairs = pair_set(&result);
378 let expected: FxHashSet<(String, String)> =
379 [("A".into(), "X".into()), ("C".into(), "Z".into())]
380 .into_iter()
381 .collect();
382 assert_eq!(pairs, expected);
383 }
384
385 #[test]
387 fn k2_includes_pairs_missed_by_k1() {
388 let (origin, target) = three_by_three_modules();
389
390 let result = calculate_rbh_set(&origin, &target, 2, false, 0.0_f64);
391
392 let pairs = pair_set(&result);
393 let expected: FxHashSet<(String, String)> = [
394 ("A".into(), "X".into()),
395 ("B".into(), "X".into()),
396 ("B".into(), "Z".into()),
397 ("C".into(), "Y".into()),
398 ("C".into(), "Z".into()),
399 ]
400 .into_iter()
401 .collect();
402 assert_eq!(pairs, expected);
403 }
404
405 #[test]
407 fn k_at_least_as_large_as_dim_returns_all_pairs() {
408 let (origin, target) = three_by_three_modules();
409
410 let result = calculate_rbh_set(&origin, &target, 5, false, 0.0_f64);
411
412 assert_eq!(result.len(), 9);
413 }
414
415 #[test]
417 fn returns_na_when_max_below_threshold() {
418 let origin = modules(&[("A", &["g1", "g2"])]);
419 let target = modules(&[("X", &["g3", "g4"])]);
420
421 let result = calculate_rbh_set(&origin, &target, 1, false, 0.5_f64);
422
423 assert_eq!(result.len(), 1);
424 assert_eq!(result[0].t1, "NA");
425 assert_eq!(result[0].t2, "NA");
426 assert_eq!(result[0].sim, 0.0);
427 }
428
429 #[test]
431 fn overlap_coefficient_differs_from_jaccard() {
432 let origin = modules(&[("A", &["g1"])]);
433 let target = modules(&[("X", &["g1", "g2", "g3", "g4"])]);
434
435 let jaccard = calculate_rbh_set(&origin, &target, 1, false, 0.0_f64);
436 let overlap = calculate_rbh_set(&origin, &target, 1, true, 0.0_f64);
437
438 assert!((jaccard[0].sim - 0.25).abs() < 1e-12);
439 assert!((overlap[0].sim - 1.0).abs() < 1e-12);
440 }
441
442 fn mat_from_rows(rows: &[&[f64]]) -> Mat<f64> {
447 let nrow = rows.len();
448 let ncol = rows[0].len();
449 Mat::from_fn(nrow, ncol, |i, j| rows[i][j])
450 }
451
452 fn build_named<'a>(
453 values: &'a Mat<f64>,
454 row_names: &[&str],
455 col_names: &[&str],
456 ) -> NamedMatrix<'a, f64> {
457 NamedMatrix {
458 col_names: col_names
459 .iter()
460 .enumerate()
461 .map(|(i, s)| (s.to_string(), i))
462 .collect(),
463 row_names: row_names
464 .iter()
465 .enumerate()
466 .map(|(i, s)| (s.to_string(), i))
467 .collect(),
468 values: values.as_ref(),
469 }
470 }
471
472 fn two_by_two_cor_setup() -> (Mat<f64>, Mat<f64>) {
481 let m1 = mat_from_rows(&[&[1.0, 1.0], &[2.0, 1.0], &[3.0, 2.0], &[4.0, 3.0]]);
482 let m2 = m1.clone();
483 (m1, m2)
484 }
485
486 #[test]
488 fn cor_k1_identifies_perfect_pairs() {
489 let (m1, m2) = two_by_two_cor_setup();
490 let x1 = build_named(&m1, &["g1", "g2", "g3", "g4"], &["A", "B"]);
491 let x2 = build_named(&m2, &["g1", "g2", "g3", "g4"], &["X", "Y"]);
492
493 let result = calculate_rbh_cor(&x1, &x2, 1, false);
494
495 let pairs: FxHashSet<(String, String)> = result
496 .iter()
497 .map(|r| (r.t1.to_string(), r.t2.to_string()))
498 .collect();
499 let expected: FxHashSet<(String, String)> =
500 [("A".into(), "X".into()), ("B".into(), "Y".into())]
501 .into_iter()
502 .collect();
503 assert_eq!(pairs, expected);
504
505 for r in &result {
506 assert!((r.sim - 1.0).abs() < 1e-10);
507 }
508 }
509
510 #[test]
512 fn cor_k2_returns_all_pairs_in_2x2() {
513 let (m1, m2) = two_by_two_cor_setup();
514 let x1 = build_named(&m1, &["g1", "g2", "g3", "g4"], &["A", "B"]);
515 let x2 = build_named(&m2, &["g1", "g2", "g3", "g4"], &["X", "Y"]);
516
517 let result = calculate_rbh_cor(&x1, &x2, 2, false);
518
519 assert_eq!(result.len(), 4);
520 }
521
522 #[test]
524 fn cor_uses_absolute_correlation() {
525 let m1 = mat_from_rows(&[&[1.0, 1.0], &[2.0, 1.0], &[3.0, 2.0], &[4.0, 3.0]]);
527 let m2 = mat_from_rows(&[&[-1.0, -1.0], &[-2.0, -1.0], &[-3.0, -2.0], &[-4.0, -3.0]]);
528 let x1 = build_named(&m1, &["g1", "g2", "g3", "g4"], &["A", "B"]);
529 let x2 = build_named(&m2, &["g1", "g2", "g3", "g4"], &["X", "Y"]);
530
531 let result = calculate_rbh_cor(&x1, &x2, 1, false);
532
533 let pairs: FxHashSet<(String, String)> = result
534 .iter()
535 .map(|r| (r.t1.to_string(), r.t2.to_string()))
536 .collect();
537 assert!(pairs.contains(&("A".into(), "X".into())));
538 assert!(pairs.contains(&("B".into(), "Y".into())));
539
540 for r in &result {
541 let p = (r.t1, r.t2);
542 if p == ("A", "X") || p == ("B", "Y") {
543 assert!((r.sim - 1.0).abs() < 1e-10);
544 }
545 }
546 }
547
548 #[test]
550 fn cor_no_row_intersection_returns_na() {
551 let m1 = mat_from_rows(&[&[1.0], &[2.0]]);
552 let m2 = mat_from_rows(&[&[1.0], &[2.0]]);
553 let x1 = build_named(&m1, &["g1", "g2"], &["A"]);
554 let x2 = build_named(&m2, &["g3", "g4"], &["X"]);
555
556 let result = calculate_rbh_cor(&x1, &x2, 1, false);
557
558 assert_eq!(result.len(), 1);
559 assert_eq!(result[0].t1, "NA");
560 assert_eq!(result[0].t2, "NA");
561 assert_eq!(result[0].sim, 0.0);
562 }
563
564 #[test]
566 fn cor_uses_only_intersecting_rows() {
567 let m1 = mat_from_rows(&[&[1.0, 1.0], &[2.0, 4.0], &[3.0, 2.0], &[4.0, 3.0]]);
572 let m2 = mat_from_rows(&[&[2.0, 4.0], &[3.0, 2.0], &[4.0, 3.0], &[99.0, -99.0]]);
573 let x1 = build_named(&m1, &["g1", "g2", "g3", "g4"], &["A", "B"]);
574 let x2 = build_named(&m2, &["g2", "g3", "g4", "g5"], &["X", "Y"]);
575
576 let result = calculate_rbh_cor(&x1, &x2, 1, false);
577
578 let pairs: FxHashSet<(String, String)> = result
579 .iter()
580 .map(|r| (r.t1.to_string(), r.t2.to_string()))
581 .collect();
582 let expected: FxHashSet<(String, String)> =
583 [("A".into(), "X".into()), ("B".into(), "Y".into())]
584 .into_iter()
585 .collect();
586 assert_eq!(pairs, expected);
587 }
588
589 #[test]
591 fn cor_respects_non_alphabetical_column_order() {
592 let m1 = mat_from_rows(&[&[1.0, 1.0], &[2.0, 1.0], &[3.0, 2.0], &[4.0, 3.0]]);
593 let m2 = mat_from_rows(&[&[1.0, 1.0], &[1.0, 2.0], &[2.0, 3.0], &[3.0, 4.0]]);
594
595 let x1 = NamedMatrix {
596 col_names: [("Z".to_string(), 0), ("A".to_string(), 1)]
597 .into_iter()
598 .collect(),
599 row_names: ["g1", "g2", "g3", "g4"]
600 .iter()
601 .enumerate()
602 .map(|(i, s)| (s.to_string(), i))
603 .collect(),
604 values: m1.as_ref(),
605 };
606 let x2 = NamedMatrix {
607 col_names: [("A".to_string(), 0), ("Z".to_string(), 1)]
608 .into_iter()
609 .collect(),
610 row_names: ["g1", "g2", "g3", "g4"]
611 .iter()
612 .enumerate()
613 .map(|(i, s)| (s.to_string(), i))
614 .collect(),
615 values: m2.as_ref(),
616 };
617
618 let result = calculate_rbh_cor(&x1, &x2, 1, false);
619
620 let pairs: FxHashSet<(String, String)> = result
622 .iter()
623 .map(|r| (r.t1.to_string(), r.t2.to_string()))
624 .collect();
625 assert!(pairs.contains(&("Z".into(), "Z".into())));
626 assert!(pairs.contains(&("A".into(), "A".into())));
627 for r in &result {
628 assert!((r.sim - 1.0).abs() < 1e-10);
629 }
630 }
631}