use faer::{Mat, MatRef};
use petgraph::Graph;
use petgraph::graph::NodeIndex;
use rayon::prelude::*;
use crate::core::math::sparse::{coo_to_csr, csr_sparse_matmul_dense, csr_vecmat};
use crate::core::math::vector_helpers::quantile;
use crate::graph::page_rank::*;
use crate::prelude::*;
#[derive(Clone, Debug)]
pub struct LigandTargetParams<T: BixverseFloat> {
pub lr_sig_hub: T,
pub gr_hub: T,
pub ltf_cutoff: T,
pub damping_factor: T,
pub max_iter: usize,
pub tol: T,
pub secondary_targets: bool,
pub topology_correction: bool,
}
impl<T: BixverseFloat> Default for LigandTargetParams<T> {
fn default() -> Self {
Self {
lr_sig_hub: T::zero(),
gr_hub: T::zero(),
ltf_cutoff: T::from_f64(0.99).unwrap(),
damping_factor: T::from_f64(0.5).unwrap(),
max_iter: 1000,
tol: T::from_f64(1e-7).unwrap(),
secondary_targets: false,
topology_correction: false,
}
}
}
fn hub_correct<T: BixverseFloat>(to: &[u32], weight: &[T], n_nodes: usize, h: T) -> Vec<T> {
let mut indegree = vec![0u32; n_nodes];
for &t in to {
indegree[t as usize] += 1;
}
weight
.par_iter()
.zip(to.par_iter())
.map(|(&w, &t)| {
let d = indegree[t as usize];
if d == 0 {
w
} else {
w / T::from_u32(d).unwrap().powf(h)
}
})
.collect()
}
fn build_ppr_graph<T>(n_nodes: usize, from: &[u32], to: &[u32], weight: &[T]) -> PageRankGraph<T>
where
T: BixverseFloat + std::iter::Sum,
{
let mut g: Graph<&str, T> = Graph::new();
for _ in 0..n_nodes {
g.add_node("");
}
for ((&f, &t), &w) in from.iter().zip(to.iter()).zip(weight.iter()) {
g.add_edge(NodeIndex::new(f as usize), NodeIndex::new(t as usize), w);
}
PageRankGraph::from_petgraph(g)
}
fn dense_rows_to_csr_thresholded<T>(mat: &MatRef<T>, q: T) -> CompressedSparseData2<T>
where
T: BixverseFloat + Send + Sync + Default,
{
let n_rows = mat.nrows();
let n_cols = mat.ncols();
let rows: Vec<Vec<(u32, T)>> = (0..n_rows)
.into_par_iter()
.map(|i| {
let row: Vec<T> = (0..n_cols).map(|j| mat[(i, j)]).collect();
if q > T::zero() {
let thresh = quantile(&row, q);
row.iter()
.enumerate()
.filter_map(|(j, &v)| {
if v > thresh {
Some((j as u32, v))
} else {
None
}
})
.collect()
} else {
row.iter()
.enumerate()
.filter_map(|(j, &v)| {
if v != T::zero() {
Some((j as u32, v))
} else {
None
}
})
.collect()
}
})
.collect();
let mut indptr = vec![0u32];
let mut indices = Vec::new();
let mut data = Vec::new();
for sr in &rows {
for &(j, v) in sr {
indices.push(j);
data.push(v);
}
indptr.push(indices.len() as u32);
}
CompressedSparseData2::new_csr(&data, &indices, &indptr, None::<&[T]>, (n_rows, n_cols))
}
fn harmonic_combine_in_place<T: BixverseFloat>(primary: &mut Mat<T>, secondary: &Mat<T>) {
let n_rows = primary.nrows();
let n_cols = primary.ncols();
let min_pos = |m: &Mat<T>| -> T {
let mut best = T::infinity();
for i in 0..m.nrows() {
for j in 0..m.ncols() {
let v = m[(i, j)];
if v > T::zero() && v < best {
best = v;
}
}
}
if best == T::infinity() {
T::one()
} else {
best
}
};
let mp = min_pos(primary);
let ms = min_pos(secondary);
for i in 0..n_rows {
for j in 0..n_cols {
let p = if primary[(i, j)] == T::zero() {
mp
} else {
primary[(i, j)]
};
let s = if secondary[(i, j)] == T::zero() {
ms
} else {
secondary[(i, j)]
};
primary[(i, j)] = T::one() / (T::one() / p + T::one() / s);
}
}
}
fn subtract_baseline_clamp<T: BixverseFloat>(out: &mut Mat<T>, baseline: &[T]) {
let n_rows = out.nrows();
let n_cols = out.ncols();
for i in 0..n_rows {
for j in 0..n_cols {
let v = out[(i, j)] - baseline[j];
out[(i, j)] = if v < T::zero() { T::zero() } else { v };
}
}
}
fn rows_to_csr<T>(rows: &[Vec<T>], n_cols: usize) -> CompressedSparseData2<T>
where
T: BixverseFloat + Default,
{
let n_rows = rows.len();
let mut indptr = vec![0u32];
let mut indices = Vec::new();
let mut data = Vec::new();
for row in rows {
for (j, &v) in row.iter().enumerate() {
if v != T::zero() {
indices.push(j as u32);
data.push(v);
}
}
indptr.push(indices.len() as u32);
}
CompressedSparseData2::new_csr(&data, &indices, &indptr, None::<&[T]>, (n_rows, n_cols))
}
#[allow(clippy::too_many_arguments)]
pub fn construct_ligand_target_mat<T>(
n_nodes: usize,
sig_from: &[u32],
sig_to: &[u32],
sig_weight: &[T],
grn_from: &[u32],
grn_to: &[u32],
grn_weight: &[T],
ligand_seeds: &[Vec<u32>],
params: &LigandTargetParams<T>,
) -> Result<Mat<T>, BixverseErrors>
where
T: BixverseFloat + std::iter::Sum + Send + Sync + Default,
{
let n_groups = ligand_seeds.len();
if n_groups == 0 {
return Ok(Mat::zeros(0, n_nodes));
}
let sig_w = if params.lr_sig_hub > T::zero() {
hub_correct(sig_to, sig_weight, n_nodes, params.lr_sig_hub)
} else {
sig_weight.to_vec()
};
let grn_w = if params.gr_hub > T::zero() {
hub_correct(grn_to, grn_weight, n_nodes, params.gr_hub)
} else {
grn_weight.to_vec()
};
let pr_graph = build_ppr_graph(n_nodes, sig_from, sig_to, &sig_w);
let grn = coo_to_csr(grn_from, grn_to, &grn_w, (n_nodes, n_nodes));
let mut flat_personalisation: Vec<Vec<T>> = Vec::new();
let mut seed_to_group: Vec<usize> = Vec::new();
for (g, group) in ligand_seeds.iter().enumerate() {
for &seed in group {
let mut v = vec![T::zero(); n_nodes];
v[seed as usize] = T::one();
flat_personalisation.push(v);
seed_to_group.push(g);
}
}
let ppr_results: Vec<Vec<T>> = flat_personalisation
.par_iter()
.map_init(PageRankWorkingMemory::<T>::new, |wm, p| {
personalised_page_rank_optimised(
&pr_graph,
params.damping_factor,
p,
params.max_iter,
params.tol,
wm,
)
})
.collect();
let mut group_ppr: Vec<Vec<T>> = vec![vec![T::zero(); n_nodes]; n_groups];
let mut group_counts: Vec<usize> = vec![0; n_groups];
for (i, mut result) in ppr_results.into_iter().enumerate() {
if params.ltf_cutoff > T::zero() {
let thresh = quantile(&result, params.ltf_cutoff);
for v in result.iter_mut() {
if *v <= thresh {
*v = T::zero();
}
}
}
let g = seed_to_group[i];
for (k, v) in result.into_iter().enumerate() {
group_ppr[g][k] += v;
}
group_counts[g] += 1;
}
for (g, &count) in group_counts.iter().enumerate() {
if count > 1 {
let c = T::from_usize(count).unwrap();
for v in group_ppr[g].iter_mut() {
*v /= c;
}
}
}
let ltf_sparse = rows_to_csr(&group_ppr, n_nodes);
let mut out = csr_sparse_matmul_dense(<f_sparse, &grn)?;
if params.secondary_targets {
let secondary_sparse = dense_rows_to_csr_thresholded(&out.as_ref(), params.ltf_cutoff);
let secondary = csr_sparse_matmul_dense(&secondary_sparse, &grn)?;
harmonic_combine_in_place(&mut out, &secondary);
}
if params.topology_correction {
let n_t = T::from_usize(n_nodes).unwrap();
let uniform_p = vec![T::one() / n_t; n_nodes];
let mut wm = PageRankWorkingMemory::<T>::new();
let bg_pr = personalised_page_rank_optimised(
&pr_graph,
params.damping_factor,
&uniform_p,
params.max_iter,
params.tol,
&mut wm,
);
let bg_target = csr_vecmat(&bg_pr, &grn)?;
subtract_baseline_clamp(&mut out, &bg_target);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hub_correct_h_zero_is_identity() {
let to = vec![0u32, 1, 1, 2];
let w = vec![1.0_f64, 2.0, 3.0, 4.0];
assert_eq!(hub_correct(&to, &w, 3, 0.0), w);
}
#[test]
fn hub_correct_divides_by_indegree() {
let to = vec![0u32, 1, 1, 2];
let w = vec![1.0_f64, 2.0, 3.0, 4.0];
assert_eq!(hub_correct(&to, &w, 3, 1.0), vec![1.0, 1.0, 1.5, 4.0]);
}
#[test]
fn hub_correct_power_applied() {
let to = vec![1u32, 1];
let w = vec![8.0_f64, 4.0];
assert_eq!(hub_correct(&to, &w, 2, 2.0), vec![2.0, 1.0]);
}
#[test]
fn harmonic_combine_basic() {
let mut p = Mat::<f64>::zeros(1, 2);
p[(0, 0)] = 2.0;
p[(0, 1)] = 4.0;
let mut s = Mat::<f64>::zeros(1, 2);
s[(0, 0)] = 2.0;
s[(0, 1)] = 4.0;
harmonic_combine_in_place(&mut p, &s);
assert!((p[(0, 0)] - 1.0).abs() < 1e-12);
assert!((p[(0, 1)] - 2.0).abs() < 1e-12);
}
#[test]
fn harmonic_combine_replaces_zero_with_min_positive() {
let mut p = Mat::<f64>::zeros(1, 2);
p[(0, 0)] = 0.0;
p[(0, 1)] = 3.0;
let mut s = Mat::<f64>::zeros(1, 2);
s[(0, 0)] = 6.0;
s[(0, 1)] = 6.0;
harmonic_combine_in_place(&mut p, &s);
assert!((p[(0, 0)] - 2.0).abs() < 1e-12);
assert!((p[(0, 1)] - 2.0).abs() < 1e-12);
}
#[test]
fn subtract_baseline_clamps_negatives() {
let mut m = Mat::<f64>::zeros(2, 3);
m[(0, 0)] = 0.0;
m[(0, 1)] = 1.0;
m[(0, 2)] = 2.0;
m[(1, 0)] = 1.0;
m[(1, 1)] = 2.0;
m[(1, 2)] = 3.0;
subtract_baseline_clamp(&mut m, &[1.0, 1.0, 1.0]);
assert_eq!(m[(0, 0)], 0.0); assert_eq!(m[(0, 1)], 0.0);
assert_eq!(m[(0, 2)], 1.0);
assert_eq!(m[(1, 0)], 0.0);
assert_eq!(m[(1, 1)], 1.0);
assert_eq!(m[(1, 2)], 2.0);
}
}