use faer::Mat;
use rayon::prelude::*;
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use std::collections::BTreeMap;
use std::sync::RwLock;
use crate::graph::graph_structures::edges_to_csr;
use crate::prelude::*;
const WANG_MIRROR_TILE: usize = 64;
#[derive(Clone, Debug, Default)]
enum OntoSemSimType {
#[default]
Resnik,
Lin,
Combined,
}
fn parse_onto_similarity_type(sim_type: &str) -> Option<OntoSemSimType> {
match sim_type.to_lowercase().as_str() {
"resnik" => Some(OntoSemSimType::Resnik),
"lin" => Some(OntoSemSimType::Lin),
"combined" => Some(OntoSemSimType::Combined),
_ => None,
}
}
#[derive(Clone, Debug)]
pub struct OntoSimRes<'a, T> {
pub t1: &'a str,
pub t2: &'a str,
pub sim: T,
}
struct InternedOntology<'a, T> {
ids: FxHashMap<&'a str, u32>,
ancestors: Vec<Vec<(u32, T)>>,
term_ic: Vec<T>,
}
impl<'a, T: BixverseFloat> InternedOntology<'a, T> {
fn new(
terms_split: &'a [(String, &[String])],
ancestor_map: &'a FxHashMap<String, FxHashSet<String>>,
info_content_map: &'a BTreeMap<String, T>,
) -> Self {
let mut ids: FxHashMap<&'a str, u32> = FxHashMap::default();
let mut terms: Vec<&'a str> = Vec::new();
for (t1, others) in terms_split {
for term in std::iter::once(t1).chain(others.iter()) {
ids.entry(term.as_str()).or_insert_with(|| {
terms.push(term.as_str());
(terms.len() - 1) as u32
});
}
}
let n_terms = terms.len();
let mut ancestors: Vec<Vec<(u32, T)>> = Vec::with_capacity(n_terms);
let mut term_ic: Vec<T> = Vec::with_capacity(n_terms);
let mut next_id = n_terms as u32;
for &term in &terms {
term_ic.push(info_content_map.get(term).copied().unwrap_or(T::one()));
let mut list: Vec<(u32, T)> = Vec::new();
if let Some(set) = ancestor_map.get(term) {
for ancestor in set {
if let Some(&ic) = info_content_map.get(ancestor) {
let id = *ids.entry(ancestor.as_str()).or_insert_with(|| {
next_id += 1;
next_id - 1
});
list.push((id, ic));
}
}
}
list.sort_unstable_by_key(|&(id, _)| id);
ancestors.push(list);
}
Self {
ids,
ancestors,
term_ic,
}
}
#[inline]
fn mica(&self, a: usize, b: usize) -> T {
let (xs, ys) = (&self.ancestors[a], &self.ancestors[b]);
let (mut i, mut j) = (0, 0);
let mut best = T::zero();
while i < xs.len() && j < ys.len() {
match xs[i].0.cmp(&ys[j].0) {
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
std::cmp::Ordering::Equal => {
best = best.max(xs[i].1);
i += 1;
j += 1;
}
}
}
best
}
}
pub fn calculate_onto_sim<'a, T>(
terms_split: &'a Vec<(String, &[String])>,
sim_type: &str,
ancestors_map: FxHashMap<String, FxHashSet<String>>,
ic_map: BTreeMap<String, T>,
) -> Vec<OntoSimRes<'a, T>>
where
T: BixverseFloat,
{
let max_ic = *ic_map
.values()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap();
let sim_type = parse_onto_similarity_type(sim_type).unwrap_or_default();
let interned = InternedOntology::new(terms_split, &ancestors_map, &ic_map);
let half = T::from_f32(0.5).unwrap();
let two = T::from_f32(2.0).unwrap();
let onto_sim: Vec<Vec<OntoSimRes<'_, T>>> = terms_split
.par_iter()
.map(|(t1, others)| {
let id1 = interned.ids[t1.as_str()] as usize;
let ic1 = interned.term_ic[id1];
others
.iter()
.map(|t2| {
let id2 = interned.ids[t2.as_str()] as usize;
let mica = interned.mica(id1, id2);
let ic2 = interned.term_ic[id2];
let sim = match sim_type {
OntoSemSimType::Resnik => mica / max_ic,
OntoSemSimType::Lin => two * mica / (ic1 + ic2),
OntoSemSimType::Combined => {
let lin_sim = two * mica / (ic1 + ic2);
let resnik_sim = mica / max_ic;
(lin_sim + resnik_sim) * half
}
};
OntoSimRes { t1, t2, sim }
})
.collect()
})
.collect();
flatten_vector(onto_sim)
}
pub type SValueCache<T> = RwLock<FxHashMap<usize, FxHashMap<usize, T>>>;
pub struct WangSimOntology<T> {
term_to_idx: FxHashMap<String, usize>,
idx_to_term: Vec<String>,
child_indptr: Vec<usize>,
child_indices: Vec<u32>,
child_weights: Vec<T>,
ancestors: Vec<FxHashSet<usize>>,
topo_order: Vec<usize>,
s_values_cache: SValueCache<T>,
}
impl<T> WangSimOntology<T>
where
T: BixverseFloat + std::iter::Sum,
{
pub fn new(parents: &[String], children: &[String], w: &[T]) -> Self {
assert_same_len!(parents, children, w);
let mut term_to_idx = FxHashMap::default();
let mut idx_to_term = Vec::new();
let mut all_terms = FxHashSet::with_capacity_and_hasher(
(parents.len() + children.len()) * 2,
FxBuildHasher,
);
all_terms.extend(parents.iter().cloned());
all_terms.extend(children.iter().cloned());
idx_to_term.reserve(all_terms.len());
term_to_idx.reserve(all_terms.len());
for term in all_terms {
term_to_idx.insert(term.clone(), idx_to_term.len());
idx_to_term.push(term);
}
let n = idx_to_term.len();
let parent_idx: Vec<u32> = parents.iter().map(|t| term_to_idx[t] as u32).collect();
let child_idx: Vec<u32> = children.iter().map(|t| term_to_idx[t] as u32).collect();
let (child_indptr, child_indices, child_weights) =
edges_to_csr(n, &parent_idx, &child_idx, w);
let (parent_indptr, parent_indices, _) = edges_to_csr(n, &child_idx, &parent_idx, w);
let topo_order = Self::compute_topological_order(&child_indptr, &child_indices);
let ancestors = Self::get_ancestors(&parent_indptr, &parent_indices, &topo_order);
WangSimOntology {
term_to_idx,
idx_to_term,
child_indptr,
child_indices,
child_weights,
ancestors,
topo_order,
s_values_cache: RwLock::new(FxHashMap::default()),
}
}
pub fn calc_sim_matrix(&self) -> (Mat<T>, Vec<String>) {
let n = self.idx_to_term.len();
let mut matrix: Mat<T> = Mat::zeros(n, n);
let per_term: Vec<(Vec<(u32, T)>, T)> = (0..n)
.into_par_iter()
.map(|i| {
let s_values = self.get_or_compute_s_values(i);
let total: T = s_values.values().copied().sum();
let mut dense: Vec<(u32, T)> = self.ancestors[i]
.iter()
.map(|node| {
(
*node as u32,
s_values.get(node).copied().unwrap_or_else(T::zero),
)
})
.collect();
dense.sort_unstable_by_key(|&(id, _)| id);
(dense, total)
})
.collect();
matrix
.par_col_iter_mut()
.enumerate()
.for_each(|(j, mut col)| {
for i in 0..j {
col[i] = Self::pair_similarity(
&per_term[i].0,
&per_term[j].0,
per_term[i].1,
per_term[j].1,
);
}
col[j] = T::one();
});
for jj in (0..n).step_by(WANG_MIRROR_TILE) {
for ii in (jj..n).step_by(WANG_MIRROR_TILE) {
for j in jj..(jj + WANG_MIRROR_TILE).min(n) {
for i in ii.max(j + 1)..(ii + WANG_MIRROR_TILE).min(n) {
matrix[(i, j)] = matrix[(j, i)];
}
}
}
}
(matrix, self.idx_to_term.clone())
}
pub fn clear_cache(&self) {
let mut cache = self.s_values_cache.write().unwrap();
cache.clear();
}
fn get_or_compute_s_values(&self, term_idx: usize) -> FxHashMap<usize, T> {
{
let cache = self.s_values_cache.read().unwrap();
if let Some(cached) = cache.get(&term_idx) {
return cached.clone();
}
}
let s_values = self.calculate_s_values(term_idx);
{
let mut cache = self.s_values_cache.write().unwrap();
cache.insert(term_idx, s_values.clone());
}
s_values
}
#[inline]
fn pair_similarity(s_val_1: &[(u32, T)], s_val_2: &[(u32, T)], sv1: T, sv2: T) -> T {
let zero = T::zero();
let (mut i, mut j) = (0, 0);
let mut numerator = zero;
let mut any_common = false;
while i < s_val_1.len() && j < s_val_2.len() {
match s_val_1[i].0.cmp(&s_val_2[j].0) {
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
std::cmp::Ordering::Equal => {
numerator += s_val_1[i].1 + s_val_2[j].1;
any_common = true;
i += 1;
j += 1;
}
}
}
if !any_common {
return zero;
}
let denominator = sv1 + sv2;
if denominator > zero {
numerator / denominator
} else {
zero
}
}
fn get_ancestors(
parent_indptr: &[usize],
parent_indices: &[u32],
topo_order: &[usize],
) -> Vec<FxHashSet<usize>> {
let mut ancestors = vec![FxHashSet::default(); topo_order.len()];
for &node_idx in topo_order.iter().rev() {
let mut node_ancestors = FxHashSet::default();
node_ancestors.insert(node_idx);
for &parent_idx in &parent_indices[parent_indptr[node_idx]..parent_indptr[node_idx + 1]]
{
node_ancestors.extend(&ancestors[parent_idx as usize]);
}
ancestors[node_idx] = node_ancestors;
}
ancestors
}
fn compute_topological_order(child_indptr: &[usize], child_indices: &[u32]) -> Vec<usize> {
let n = child_indptr.len() - 1;
let mut in_degree = vec![0usize; n];
for &c in child_indices {
in_degree[c as usize] += 1;
}
let mut order: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
let mut head = 0;
while head < order.len() {
let node = order[head];
head += 1;
for &c in &child_indices[child_indptr[node]..child_indptr[node + 1]] {
in_degree[c as usize] -= 1;
if in_degree[c as usize] == 0 {
order.push(c as usize);
}
}
}
if order.len() < n {
panic!("Ontology contains cycles");
}
order.reverse();
order
}
fn calculate_s_values(&self, term_idx: usize) -> FxHashMap<usize, T> {
let dag_nodes = &self.ancestors[term_idx];
let mut s_values = FxHashMap::with_capacity_and_hasher(dag_nodes.len(), FxBuildHasher);
s_values.insert(term_idx, T::one());
for &node_idx in &self.topo_order {
if !dag_nodes.contains(&node_idx) || node_idx == term_idx {
continue;
}
let mut max_contribution: T = T::zero();
let range = self.child_indptr[node_idx]..self.child_indptr[node_idx + 1];
for (&child_idx, &edge_weight) in self.child_indices[range.clone()]
.iter()
.zip(&self.child_weights[range])
{
let child_idx = child_idx as usize;
if dag_nodes.contains(&child_idx)
&& let Some(&child_s_value) = s_values.get(&child_idx)
{
max_contribution = max_contribution.max(edge_weight * child_s_value);
}
}
if max_contribution > T::zero() {
s_values.insert(node_idx, max_contribution);
}
}
s_values
}
pub fn wang_sim(&self, term1: &str, term2: &str) -> Option<T> {
let term_idx_1 = *self.term_to_idx.get(term1)?;
let term_idx_2 = *self.term_to_idx.get(term2)?;
if term_idx_1 == term_idx_2 {
return Some(T::one());
}
self.wang_sim_by_idx(term_idx_1, term_idx_2)
}
fn wang_sim_by_idx(&self, term_idx_1: usize, term_idx_2: usize) -> Option<T> {
let s_val_1 = self.get_or_compute_s_values(term_idx_1);
let s_val_2 = self.get_or_compute_s_values(term_idx_2);
let dag1_nodes = &self.ancestors[term_idx_1];
let dag2_nodes = &self.ancestors[term_idx_2];
let (smaller, larger) = if dag1_nodes.len() < dag2_nodes.len() {
(dag1_nodes, dag2_nodes)
} else {
(dag2_nodes, dag1_nodes)
};
let common_nodes: Vec<usize> = smaller
.iter()
.filter(|node| larger.contains(node))
.cloned()
.collect();
if common_nodes.is_empty() {
return Some(T::zero());
}
let sv1: T = s_val_1.values().copied().sum();
let sv2: T = s_val_2.values().copied().sum();
let zero = T::zero();
let numerator: T = common_nodes
.iter()
.map(|&node_idx| {
*s_val_1.get(&node_idx).unwrap_or(&zero) + *s_val_2.get(&node_idx).unwrap_or(&zero)
})
.sum();
let denominator = sv1 + sv2;
if denominator > zero {
Some(numerator / denominator)
} else {
Some(T::zero())
}
}
}
pub fn filter_sims_critval<'a, T>(
sim_vals: &[T],
names: &'a [String],
threshold: T,
) -> Vec<OntoSimRes<'a, T>>
where
T: BixverseFloat,
{
let n = names.len();
let mut results = Vec::new();
let mut idx = 0;
for i in 0..n {
for j in i..n {
if i != j {
if idx < sim_vals.len() {
let sim = sim_vals[idx];
if sim >= threshold {
results.push(OntoSimRes {
t1: &names[i],
t2: &names[j],
sim,
})
}
}
idx += 1;
}
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn chain() -> WangSimOntology<f64> {
WangSimOntology::new(
&["A".to_string(), "B".to_string()],
&["B".to_string(), "C".to_string()],
&[0.8, 0.8],
)
}
#[test]
fn wang_sim_on_a_chain_matches_the_hand_derivation() {
let onto = chain();
assert_relative_eq!(
onto.wang_sim("B", "C").unwrap(),
3.24 / 4.24,
epsilon = 1e-12
);
assert_relative_eq!(
onto.wang_sim("A", "C").unwrap(),
1.64 / 3.44,
epsilon = 1e-12
);
assert_relative_eq!(onto.wang_sim("A", "B").unwrap(), 1.8 / 2.8, epsilon = 1e-12);
}
#[test]
fn wang_sim_handles_self_and_unknown_terms() {
let onto = chain();
assert_relative_eq!(onto.wang_sim("A", "A").unwrap(), 1.0, epsilon = 1e-12);
assert!(onto.wang_sim("A", "NOPE").is_none());
}
#[test]
fn wang_sim_of_disconnected_terms_is_zero_and_matches_the_matrix() {
let onto = WangSimOntology::<f64>::new(
&["X".to_string(), "Y".to_string()],
&["X1".to_string(), "Y1".to_string()],
&[0.8, 0.8],
);
let pair = onto.wang_sim("X1", "Y1").unwrap();
assert_relative_eq!(pair, 0.0, epsilon = 1e-12);
let (matrix, names) = onto.calc_sim_matrix();
let i = names.iter().position(|n| n == "X1").unwrap();
let j = names.iter().position(|n| n == "Y1").unwrap();
assert_relative_eq!(pair, matrix[(i, j)], epsilon = 1e-12);
}
#[test]
fn calc_sim_matrix_is_symmetric_and_agrees_with_wang_sim() {
let onto = chain();
let (matrix, names) = onto.calc_sim_matrix();
assert_eq!(names.len(), 3);
let mut sorted = names.clone();
sorted.sort();
assert_eq!(sorted, vec!["A", "B", "C"]);
for (i, ni) in names.iter().enumerate() {
assert_relative_eq!(matrix[(i, i)], 1.0, epsilon = 1e-12);
for (j, nj) in names.iter().enumerate() {
assert_relative_eq!(matrix[(i, j)], matrix[(j, i)], epsilon = 1e-12);
if i != j {
assert_relative_eq!(
matrix[(i, j)],
onto.wang_sim(ni, nj).unwrap(),
epsilon = 1e-12
);
}
}
}
}
#[test]
#[should_panic(expected = "cycle")]
fn wang_sim_ontology_rejects_a_cyclic_graph() {
WangSimOntology::<f64>::new(
&["A".to_string(), "B".to_string()],
&["B".to_string(), "A".to_string()],
&[0.8, 0.8],
);
}
}