use faer::{Mat, unzip, zip};
use rayon::prelude::*;
use rustc_hash::FxHashSet;
use std::collections::BTreeMap;
use crate::core::base::cors_similarity::{cor_two_matrices, set_similarity};
use crate::core::mat_struct::NamedMatrix;
use crate::prelude::*;
use crate::utils::vec_utils::{array_max, flatten_vector};
fn kth_largest<T: BixverseFloat>(values: &[T], k: usize) -> T {
let mut sorted: Vec<T> = values.to_vec();
sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
let idx = k.saturating_sub(1).min(sorted.len().saturating_sub(1));
sorted[idx]
}
fn names_by_index(names: &BTreeMap<String, usize>) -> Vec<&str> {
let mut pairs: Vec<(&str, usize)> = names.iter().map(|(k, &v)| (k.as_str(), v)).collect();
pairs.sort_by_key(|(_, idx)| *idx);
pairs.into_iter().map(|(k, _)| k).collect()
}
#[derive(Clone, Debug)]
pub struct RbhTripletStruc<'a, T> {
pub t1: &'a str,
pub t2: &'a str,
pub sim: T,
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub struct RbhResult<T> {
pub origin: String,
pub target: String,
pub origin_modules: Vec<String>,
pub target_modules: Vec<String>,
pub similarities: Vec<T>,
}
pub fn calculate_rbh_set<'a, T>(
origin_modules: &'a BTreeMap<String, FxHashSet<String>>,
target_modules: &'a BTreeMap<String, FxHashSet<String>>,
k: usize,
overlap_coefficient: bool,
min_similarity: T,
) -> Vec<RbhTripletStruc<'a, T>>
where
T: BixverseFloat,
{
let names_targets: Vec<&String> = target_modules.keys().collect();
let names_origin: Vec<&String> = origin_modules.keys().collect();
let origin_sets: Vec<&FxHashSet<String>> = origin_modules.values().collect();
let target_sets: Vec<&FxHashSet<String>> = target_modules.values().collect();
let similarities_flat: Vec<Vec<T>> = origin_sets
.into_par_iter()
.map(|v1| {
target_sets
.iter()
.map(|v2| set_similarity(v1, v2, overlap_coefficient))
.collect()
})
.collect();
let mat_data: Vec<T> = flatten_vector(similarities_flat);
let max_sim = array_max(&mat_data);
if max_sim < min_similarity {
vec![RbhTripletStruc {
t1: "NA",
t2: "NA",
sim: T::zero(),
}]
} else {
let nrow = names_origin.len();
let ncol = names_targets.len();
let sim_mat = Mat::from_fn(nrow, ncol, |i, j| mat_data[j + i * ncol]);
let row_thresholds: Vec<T> = sim_mat
.row_iter()
.map(|x| {
let row: Vec<T> = x.iter().copied().collect();
kth_largest(&row, k)
})
.collect();
let col_thresholds: Vec<T> = sim_mat
.col_iter()
.map(|x| {
let row: Vec<T> = x.iter().copied().collect();
kth_largest(&row, k)
})
.collect();
let mut matching_pairs: Vec<RbhTripletStruc<T>> = Vec::new();
for r in 0..nrow {
for c in 0..ncol {
let value = sim_mat[(r, c)];
if value >= row_thresholds[r] && value >= col_thresholds[c] {
let triplet = RbhTripletStruc {
t1: names_origin[r],
t2: names_targets[c],
sim: value,
};
matching_pairs.push(triplet)
}
}
}
if !matching_pairs.is_empty() {
matching_pairs
} else {
vec![RbhTripletStruc {
t1: "NA",
t2: "NA",
sim: T::zero(),
}]
}
}
}
pub fn calculate_rbh_cor<'a, T>(
x1: &'a NamedMatrix<'a, T>,
x2: &'a NamedMatrix<'a, T>,
k: usize,
spearman: bool,
) -> Vec<RbhTripletStruc<'a, T>>
where
T: BixverseFloat,
{
let row_names_1: FxHashSet<String> = x1.row_names.keys().cloned().collect();
let row_names_2: FxHashSet<String> = x2.row_names.keys().cloned().collect();
let names_targets: Vec<&str> = names_by_index(&x1.col_names);
let names_origin: Vec<&str> = names_by_index(&x2.col_names);
let intersecting_rows: Vec<String> = row_names_1.intersection(&row_names_2).cloned().collect();
if intersecting_rows.is_empty() {
return vec![RbhTripletStruc {
t1: "NA",
t2: "NA",
sim: T::zero(),
}];
}
let row_refs: Vec<&str> = intersecting_rows.iter().map(|s| s.as_str()).collect();
let sub_x1 = x1.get_rows(&row_refs).unwrap();
let sub_x2 = x2.get_rows(&row_refs).unwrap();
let mut correlations = cor_two_matrices(&sub_x1.as_ref(), &sub_x2.as_ref(), spearman);
zip!(correlations.as_mut()).for_each(|unzip!(x)| *x = x.abs());
let row_thresholds: Vec<T> = correlations
.row_iter()
.map(|x| {
let row: Vec<T> = x.iter().copied().collect();
kth_largest(&row, k)
})
.collect();
let col_thresholds: Vec<T> = correlations
.col_iter()
.map(|x| {
let col: Vec<T> = x.iter().copied().collect();
kth_largest(&col, k)
})
.collect();
let mut matching_pairs: Vec<RbhTripletStruc<'a, T>> = Vec::new();
let nrow = names_targets.len();
let ncol = names_origin.len();
for r in 0..nrow {
for c in 0..ncol {
let value = correlations[(r, c)];
if value >= row_thresholds[r] && value >= col_thresholds[c] {
let triplet = RbhTripletStruc {
t1: names_targets[r],
t2: names_origin[c],
sim: value,
};
matching_pairs.push(triplet);
}
}
}
if !matching_pairs.is_empty() {
matching_pairs
} else {
vec![RbhTripletStruc {
t1: "NA",
t2: "NA",
sim: T::zero(),
}]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn module(items: &[&str]) -> FxHashSet<String> {
items.iter().map(|s| s.to_string()).collect()
}
fn modules(pairs: &[(&str, &[&str])]) -> BTreeMap<String, FxHashSet<String>> {
pairs
.iter()
.map(|(n, it)| (n.to_string(), module(it)))
.collect()
}
fn three_by_three_modules() -> (
BTreeMap<String, FxHashSet<String>>,
BTreeMap<String, FxHashSet<String>>,
) {
let origin = modules(&[
("A", &["1", "2", "3", "4", "5", "6"]),
("B", &["4", "5", "6", "7", "8", "9"]),
("C", &["7", "8", "9", "10", "11", "12"]),
]);
let target = modules(&[
("X", &["1", "2", "3", "4", "5", "6"]),
("Y", &["6", "7", "10", "11", "13", "14"]),
("Z", &["7", "8", "9", "10", "11", "12"]),
]);
(origin, target)
}
fn pair_set<T>(result: &[RbhTripletStruc<T>]) -> FxHashSet<(String, String)> {
result
.iter()
.map(|r| (r.t1.to_string(), r.t2.to_string()))
.collect()
}
#[test]
fn k1_identifies_clear_reciprocal_hits() {
let origin = modules(&[("A", &["g1", "g2", "g3"]), ("B", &["g4", "g5", "g6"])]);
let target = modules(&[("X", &["g1", "g2", "g3"]), ("Y", &["g4", "g5", "g6"])]);
let result = calculate_rbh_set(&origin, &target, 1, false, 0.0_f64);
assert_eq!(result.len(), 2);
let pairs = pair_set(&result);
assert!(pairs.contains(&("A".into(), "X".into())));
assert!(pairs.contains(&("B".into(), "Y".into())));
for r in &result {
assert!((r.sim - 1.0).abs() < 1e-12);
}
}
#[test]
fn k1_excludes_non_reciprocal_hits() {
let (origin, target) = three_by_three_modules();
let result = calculate_rbh_set(&origin, &target, 1, false, 0.0_f64);
let pairs = pair_set(&result);
let expected: FxHashSet<(String, String)> =
[("A".into(), "X".into()), ("C".into(), "Z".into())]
.into_iter()
.collect();
assert_eq!(pairs, expected);
}
#[test]
fn k2_includes_pairs_missed_by_k1() {
let (origin, target) = three_by_three_modules();
let result = calculate_rbh_set(&origin, &target, 2, false, 0.0_f64);
let pairs = pair_set(&result);
let expected: FxHashSet<(String, String)> = [
("A".into(), "X".into()),
("B".into(), "X".into()),
("B".into(), "Z".into()),
("C".into(), "Y".into()),
("C".into(), "Z".into()),
]
.into_iter()
.collect();
assert_eq!(pairs, expected);
}
#[test]
fn k_at_least_as_large_as_dim_returns_all_pairs() {
let (origin, target) = three_by_three_modules();
let result = calculate_rbh_set(&origin, &target, 5, false, 0.0_f64);
assert_eq!(result.len(), 9);
}
#[test]
fn returns_na_when_max_below_threshold() {
let origin = modules(&[("A", &["g1", "g2"])]);
let target = modules(&[("X", &["g3", "g4"])]);
let result = calculate_rbh_set(&origin, &target, 1, false, 0.5_f64);
assert_eq!(result.len(), 1);
assert_eq!(result[0].t1, "NA");
assert_eq!(result[0].t2, "NA");
assert_eq!(result[0].sim, 0.0);
}
#[test]
fn overlap_coefficient_differs_from_jaccard() {
let origin = modules(&[("A", &["g1"])]);
let target = modules(&[("X", &["g1", "g2", "g3", "g4"])]);
let jaccard = calculate_rbh_set(&origin, &target, 1, false, 0.0_f64);
let overlap = calculate_rbh_set(&origin, &target, 1, true, 0.0_f64);
assert!((jaccard[0].sim - 0.25).abs() < 1e-12);
assert!((overlap[0].sim - 1.0).abs() < 1e-12);
}
fn mat_from_rows(rows: &[&[f64]]) -> Mat<f64> {
let nrow = rows.len();
let ncol = rows[0].len();
Mat::from_fn(nrow, ncol, |i, j| rows[i][j])
}
fn build_named<'a>(
values: &'a Mat<f64>,
row_names: &[&str],
col_names: &[&str],
) -> NamedMatrix<'a, f64> {
NamedMatrix {
col_names: col_names
.iter()
.enumerate()
.map(|(i, s)| (s.to_string(), i))
.collect(),
row_names: row_names
.iter()
.enumerate()
.map(|(i, s)| (s.to_string(), i))
.collect(),
values: values.as_ref(),
}
}
fn two_by_two_cor_setup() -> (Mat<f64>, Mat<f64>) {
let m1 = mat_from_rows(&[&[1.0, 1.0], &[2.0, 1.0], &[3.0, 2.0], &[4.0, 3.0]]);
let m2 = m1.clone();
(m1, m2)
}
#[test]
fn cor_k1_identifies_perfect_pairs() {
let (m1, m2) = two_by_two_cor_setup();
let x1 = build_named(&m1, &["g1", "g2", "g3", "g4"], &["A", "B"]);
let x2 = build_named(&m2, &["g1", "g2", "g3", "g4"], &["X", "Y"]);
let result = calculate_rbh_cor(&x1, &x2, 1, false);
let pairs: FxHashSet<(String, String)> = result
.iter()
.map(|r| (r.t1.to_string(), r.t2.to_string()))
.collect();
let expected: FxHashSet<(String, String)> =
[("A".into(), "X".into()), ("B".into(), "Y".into())]
.into_iter()
.collect();
assert_eq!(pairs, expected);
for r in &result {
assert!((r.sim - 1.0).abs() < 1e-10);
}
}
#[test]
fn cor_k2_returns_all_pairs_in_2x2() {
let (m1, m2) = two_by_two_cor_setup();
let x1 = build_named(&m1, &["g1", "g2", "g3", "g4"], &["A", "B"]);
let x2 = build_named(&m2, &["g1", "g2", "g3", "g4"], &["X", "Y"]);
let result = calculate_rbh_cor(&x1, &x2, 2, false);
assert_eq!(result.len(), 4);
}
#[test]
fn cor_uses_absolute_correlation() {
let m1 = mat_from_rows(&[&[1.0, 1.0], &[2.0, 1.0], &[3.0, 2.0], &[4.0, 3.0]]);
let m2 = mat_from_rows(&[&[-1.0, -1.0], &[-2.0, -1.0], &[-3.0, -2.0], &[-4.0, -3.0]]);
let x1 = build_named(&m1, &["g1", "g2", "g3", "g4"], &["A", "B"]);
let x2 = build_named(&m2, &["g1", "g2", "g3", "g4"], &["X", "Y"]);
let result = calculate_rbh_cor(&x1, &x2, 1, false);
let pairs: FxHashSet<(String, String)> = result
.iter()
.map(|r| (r.t1.to_string(), r.t2.to_string()))
.collect();
assert!(pairs.contains(&("A".into(), "X".into())));
assert!(pairs.contains(&("B".into(), "Y".into())));
for r in &result {
let p = (r.t1, r.t2);
if p == ("A", "X") || p == ("B", "Y") {
assert!((r.sim - 1.0).abs() < 1e-10);
}
}
}
#[test]
fn cor_no_row_intersection_returns_na() {
let m1 = mat_from_rows(&[&[1.0], &[2.0]]);
let m2 = mat_from_rows(&[&[1.0], &[2.0]]);
let x1 = build_named(&m1, &["g1", "g2"], &["A"]);
let x2 = build_named(&m2, &["g3", "g4"], &["X"]);
let result = calculate_rbh_cor(&x1, &x2, 1, false);
assert_eq!(result.len(), 1);
assert_eq!(result[0].t1, "NA");
assert_eq!(result[0].t2, "NA");
assert_eq!(result[0].sim, 0.0);
}
#[test]
fn cor_uses_only_intersecting_rows() {
let m1 = mat_from_rows(&[&[1.0, 1.0], &[2.0, 4.0], &[3.0, 2.0], &[4.0, 3.0]]);
let m2 = mat_from_rows(&[&[2.0, 4.0], &[3.0, 2.0], &[4.0, 3.0], &[99.0, -99.0]]);
let x1 = build_named(&m1, &["g1", "g2", "g3", "g4"], &["A", "B"]);
let x2 = build_named(&m2, &["g2", "g3", "g4", "g5"], &["X", "Y"]);
let result = calculate_rbh_cor(&x1, &x2, 1, false);
let pairs: FxHashSet<(String, String)> = result
.iter()
.map(|r| (r.t1.to_string(), r.t2.to_string()))
.collect();
let expected: FxHashSet<(String, String)> =
[("A".into(), "X".into()), ("B".into(), "Y".into())]
.into_iter()
.collect();
assert_eq!(pairs, expected);
}
#[test]
fn cor_respects_non_alphabetical_column_order() {
let m1 = mat_from_rows(&[&[1.0, 1.0], &[2.0, 1.0], &[3.0, 2.0], &[4.0, 3.0]]);
let m2 = mat_from_rows(&[&[1.0, 1.0], &[1.0, 2.0], &[2.0, 3.0], &[3.0, 4.0]]);
let x1 = NamedMatrix {
col_names: [("Z".to_string(), 0), ("A".to_string(), 1)]
.into_iter()
.collect(),
row_names: ["g1", "g2", "g3", "g4"]
.iter()
.enumerate()
.map(|(i, s)| (s.to_string(), i))
.collect(),
values: m1.as_ref(),
};
let x2 = NamedMatrix {
col_names: [("A".to_string(), 0), ("Z".to_string(), 1)]
.into_iter()
.collect(),
row_names: ["g1", "g2", "g3", "g4"]
.iter()
.enumerate()
.map(|(i, s)| (s.to_string(), i))
.collect(),
values: m2.as_ref(),
};
let result = calculate_rbh_cor(&x1, &x2, 1, false);
let pairs: FxHashSet<(String, String)> = result
.iter()
.map(|r| (r.t1.to_string(), r.t2.to_string()))
.collect();
assert!(pairs.contains(&("Z".into(), "Z".into())));
assert!(pairs.contains(&("A".into(), "A".into())));
for r in &result {
assert!((r.sim - 1.0).abs() < 1e-10);
}
}
}