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::*;
const ROW_BLOCK: usize = 16;
#[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-12).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 n_blocks = n_rows.div_ceil(ROW_BLOCK);
let blocks: Vec<Vec<Vec<(u32, T)>>> = (0..n_blocks)
.into_par_iter()
.map(|block| {
let row_start = block * ROW_BLOCK;
let height = ROW_BLOCK.min(n_rows - row_start);
let mut buf = vec![T::zero(); height * n_cols];
for j in 0..n_cols {
for r in 0..height {
buf[r * n_cols + j] = mat[(row_start + r, j)];
}
}
buf.chunks(n_cols.max(1))
.take(height)
.map(|row| {
if q > T::zero() {
let thresh = quantile(row, q);
row.iter()
.enumerate()
.filter(|&(_, &v)| v > thresh)
.map(|(j, &v)| (j as u32, v))
.collect()
} else {
row.iter()
.enumerate()
.filter(|&(_, &v)| v != T::zero())
.map(|(j, &v)| (j as u32, v))
.collect()
}
})
.collect()
})
.collect();
let rows: Vec<Vec<(u32, T)>> = blocks.into_iter().flatten().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_cols = primary.ncols();
let min_pos = |m: &Mat<T>| -> T {
let mut best = T::infinity();
for j in 0..m.ncols() {
for &v in m.col_as_slice(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 j in 0..n_cols {
for (p_slot, &s_val) in primary
.col_as_slice_mut(j)
.iter_mut()
.zip(secondary.col_as_slice(j))
{
let p = if *p_slot == T::zero() { mp } else { *p_slot };
let s = if s_val == T::zero() { ms } else { s_val };
*p_slot = T::one() / (T::one() / p + T::one() / s);
}
}
}
fn subtract_baseline_clamp<T: BixverseFloat>(out: &mut Mat<T>, baseline: &[T]) {
let n_cols = out.ncols();
for j in 0..n_cols {
let base = baseline[j];
for slot in out.col_as_slice_mut(j) {
let v = *slot - base;
*slot = 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_seeds: Vec<u32> = Vec::new();
let mut seed_to_group: Vec<usize> = Vec::new();
for (g, group) in ligand_seeds.iter().enumerate() {
for &seed in group {
flat_seeds.push(seed);
seed_to_group.push(g);
}
}
let ppr_results: Vec<Vec<T>> = flat_seeds
.par_iter()
.map_init(
|| (PageRankWorkingMemory::<T>::new(), vec![T::zero(); n_nodes]),
|(wm, personalisation), &seed| {
personalisation[seed as usize] = T::one();
let mut result = personalised_page_rank_optimised(
&pr_graph,
params.damping_factor,
personalisation,
params.max_iter,
params.tol,
wm,
);
personalisation[seed as usize] = T::zero();
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();
}
}
}
result
},
)
.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, result) in ppr_results.into_iter().enumerate() {
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);
}
}