1use rayon::prelude::*;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::graph::graph_structures::edges_to_csr;
8use crate::prelude::*;
9
10const DEFAULT_TOLERANCE: f64 = 1e-12;
16
17#[derive(Debug)]
26pub struct PageRankWorkingMemory<T> {
27 ranks: Vec<T>,
28 new_ranks: Vec<T>,
29}
30
31impl<T> PageRankWorkingMemory<T>
32where
33 T: BixverseFloat,
34{
35 pub fn new() -> Self {
41 Self {
42 ranks: Vec::new(),
43 new_ranks: Vec::new(),
44 }
45 }
46
47 fn ensure_capacity(&mut self, node_count: usize) {
53 if self.ranks.len() < node_count {
54 self.ranks.resize(node_count, T::zero());
55 self.new_ranks.resize(node_count, T::zero());
56 }
57 }
58}
59
60impl<T> Default for PageRankWorkingMemory<T>
61where
62 T: BixverseFloat,
63{
64 fn default() -> Self {
65 Self {
66 ranks: Vec::new(),
67 new_ranks: Vec::new(),
68 }
69 }
70}
71
72#[derive(Clone)]
74pub struct PageRankGraph<T> {
75 node_count: usize,
77 in_edges_flat: Vec<usize>,
79 in_edge_weights_flat: Vec<T>,
81 in_edges_offsets: Vec<usize>,
83 inv_out_weight: Vec<T>,
85}
86
87impl<T> PageRankGraph<T>
88where
89 T: BixverseFloat + std::iter::Sum,
90{
91 pub fn from_indices(
105 node_count: usize,
106 from: &[usize],
107 to: &[usize],
108 weights: Option<&[T]>,
109 undirected: bool,
110 ) -> Self {
111 assert_same_len!(from, to);
112
113 if let Some(weights) = weights {
114 assert_same_len!(from, weights);
115 }
116
117 let mut out_edges: Vec<Vec<(usize, T)>> = vec![Vec::new(); node_count];
118 let mut in_edges: Vec<Vec<(usize, T)>> = vec![Vec::new(); node_count];
119
120 for (i, (&from_idx, &to_idx)) in from.iter().zip(to.iter()).enumerate() {
121 let weight = weights.map(|w| w[i]).unwrap_or(T::one());
122
123 out_edges[from_idx].push((to_idx, weight));
124 in_edges[to_idx].push((from_idx, weight));
125
126 if undirected {
127 out_edges[to_idx].push((from_idx, weight));
128 in_edges[from_idx].push((to_idx, weight));
129 }
130 }
131
132 let mut in_edges_flat = Vec::new();
134 let mut in_edge_weights_flat = Vec::new();
135 let mut in_edges_offsets = Vec::with_capacity(node_count + 1);
136 in_edges_offsets.push(0);
137
138 for node_in_edges in &in_edges {
139 for &(node_idx, weight) in node_in_edges {
140 in_edges_flat.push(node_idx);
141 in_edge_weights_flat.push(weight);
142 }
143 in_edges_offsets.push(in_edges_flat.len());
144 }
145
146 let inv_out_weight = inv_out_weights(&out_edges);
147
148 Self {
149 node_count,
150 in_edges_flat,
151 in_edge_weights_flat,
152 in_edges_offsets,
153 inv_out_weight,
154 }
155 }
156
157 pub fn from_strings(
175 nodes: &[String],
176 from: &[String],
177 to: &[String],
178 weights: Option<&[T]>,
179 undirected: bool,
180 ) -> Self {
181 let name_to_idx: FxHashMap<&String, usize> = nodes
182 .iter()
183 .enumerate()
184 .map(|(idx, name)| (name, idx))
185 .collect();
186 let lookup = |name: &String| {
187 *name_to_idx
188 .get(name)
189 .unwrap_or_else(|| panic!("Node '{name}' not found in nodes list"))
190 };
191 let from_idx: Vec<usize> = from.iter().map(lookup).collect();
192 let to_idx: Vec<usize> = to.iter().map(lookup).collect();
193
194 Self::from_indices(nodes.len(), &from_idx, &to_idx, weights, undirected)
195 }
196
197 #[inline]
210 fn in_edges(&self, node: usize) -> (&[usize], &[T]) {
211 let start = self.in_edges_offsets[node];
212 let end = self.in_edges_offsets[node + 1];
213 (
214 &self.in_edges_flat[start..end],
215 &self.in_edge_weights_flat[start..end],
216 )
217 }
218}
219
220fn inv_out_weights<T>(out_edges: &[Vec<(usize, T)>]) -> Vec<T>
235where
236 T: BixverseFloat + std::iter::Sum,
237{
238 out_edges
239 .iter()
240 .map(|edges| {
241 let total: T = edges.iter().map(|(_, weight)| *weight).sum();
242 if total > T::zero() {
243 T::one() / total
244 } else {
245 T::zero()
246 }
247 })
248 .collect()
249}
250
251pub fn personalised_page_rank<T>(
275 graph: &PageRankGraph<T>,
276 damping_factor: T,
277 personalisation_vector: &[T],
278 nb_iter: usize,
279 tol: Option<T>,
280) -> Vec<T>
281where
282 T: BixverseFloat + std::iter::Sum,
283{
284 if graph.node_count == 0 {
285 return vec![];
286 }
287
288 assert!(
289 T::zero() <= damping_factor && damping_factor <= T::one(),
290 "Damping factor should be between 0 and 1."
291 );
292 assert_eq!(
293 personalisation_vector.len(),
294 graph.node_count,
295 "Personalisation vector length must match node count."
296 );
297
298 let tolerance = tol.unwrap_or(T::from_f64(DEFAULT_TOLERANCE).unwrap());
299
300 personalised_page_rank_optimised(
301 graph,
302 damping_factor,
303 personalisation_vector,
304 nb_iter,
305 tolerance,
306 &mut PageRankWorkingMemory::new(),
307 )
308}
309
310pub fn personalised_page_rank_optimised<T>(
332 graph: &PageRankGraph<T>,
333 damping_factor: T,
334 personalisation_vector: &[T],
335 nb_iter: usize,
336 tolerance: T,
337 working_memory: &mut PageRankWorkingMemory<T>,
338) -> Vec<T>
339where
340 T: BixverseFloat + std::iter::Sum,
341{
342 let node_count = graph.node_count;
343
344 working_memory.ensure_capacity(node_count);
346 let ranks = &mut working_memory.ranks;
347 let new_ranks = &mut working_memory.new_ranks;
348
349 ranks[..node_count].copy_from_slice(personalisation_vector);
351
352 let teleport_factor = T::one() - damping_factor;
353
354 for _ in 0..nb_iter {
355 new_ranks[..node_count]
357 .par_iter_mut()
358 .enumerate()
359 .for_each(|(v, new_rank)| {
360 let teleport_prob = teleport_factor * personalisation_vector[v];
361
362 let (in_nodes, in_weights) = graph.in_edges(v);
363 let link_prob: T = in_nodes
364 .iter()
365 .zip(in_weights.iter())
366 .map(|(&w, &edge_weight)| ranks[w] * edge_weight * graph.inv_out_weight[w])
367 .sum();
368
369 *new_rank = teleport_prob + damping_factor * link_prob;
370 });
371
372 let l1: T = new_ranks[..node_count]
373 .par_iter()
374 .zip(&ranks[..node_count])
375 .map(|(new, old)| (*new - *old).abs())
376 .sum();
377
378 std::mem::swap(ranks, new_ranks);
380
381 if l1 <= tolerance {
382 break;
383 }
384 }
385
386 let sum: T = ranks[..node_count].iter().cloned().sum();
388 if sum > T::zero() {
389 ranks[..node_count].iter_mut().for_each(|x| *x /= sum);
390 }
391
392 ranks[..node_count].to_vec()
393}
394
395pub fn constrained_personalised_page_rank<T>(
421 graph: &AttributedGraph<T>,
422 damping_factor: T,
423 personalisation_vector: &[T],
424 nb_iter: usize,
425 tol: Option<T>,
426 sink_node_types: Option<&FxHashSet<String>>,
427 constrained_edge_types: Option<&FxHashSet<String>>,
428) -> Vec<T>
429where
430 T: BixverseFloat + std::iter::Sum,
431{
432 let node_count = graph.node_types.len();
433 if node_count == 0 {
434 return vec![];
435 }
436
437 assert!(
439 T::zero() <= damping_factor && damping_factor <= T::one(),
440 "Damping factor should be between 0 and 1."
441 );
442 assert_eq!(
443 personalisation_vector.len(),
444 node_count,
445 "Personalization vector length must match node count."
446 );
447
448 let tolerance = tol.unwrap_or(T::from_f64(DEFAULT_TOLERANCE).unwrap());
449 let binding = FxHashSet::default();
450 let sink_types = sink_node_types.unwrap_or(&binding);
451 let constrained_types = constrained_edge_types.unwrap_or(&binding);
452
453 let mut out_edges: Vec<Vec<(usize, T, bool)>> = vec![Vec::new(); node_count];
455 let mut in_edges: Vec<Vec<(usize, T, bool)>> = vec![Vec::new(); node_count];
456
457 for (((&source, &target), &edge_type), &weight) in graph
458 .from
459 .iter()
460 .zip(&graph.to)
461 .zip(&graph.edge_types)
462 .zip(&graph.weights)
463 {
464 if sink_types.contains(graph.node_types[source]) {
466 continue;
467 }
468
469 let is_sink_edge = constrained_types.contains(edge_type);
470 out_edges[source].push((target, weight, is_sink_edge));
471 in_edges[target].push((source, weight, is_sink_edge));
472 }
473
474 let out_degrees: Vec<T> = out_edges
476 .iter()
477 .map(|edges| {
478 let total_weight: T = edges.iter().map(|(_, weight, _)| *weight).sum();
479 total_weight
480 })
481 .collect();
482
483 let mut flowable_ranks: Vec<T> = personalisation_vector.to_vec();
485 let mut absorbed_ranks: Vec<T> = vec![T::zero(); node_count];
486 let teleport_factor = T::one() - damping_factor;
487
488 for _ in 0..nb_iter {
489 let (new_flowable, new_absorbed): (Vec<T>, Vec<T>) = (0..node_count)
491 .into_par_iter()
492 .map(|v| {
493 let teleport_mass = teleport_factor * personalisation_vector[v];
494 let mut flowable = teleport_mass;
495 let mut absorbed = T::zero();
496
497 for &(w, edge_weight, is_sink_edge) in &in_edges[v] {
499 if out_degrees[w] > T::zero() {
500 let flow =
501 damping_factor * flowable_ranks[w] * edge_weight / out_degrees[w];
502
503 if is_sink_edge {
504 absorbed += flow;
505 } else {
506 flowable += flow;
507 }
508 }
509 }
510
511 (flowable, absorbed)
512 })
513 .unzip();
514
515 let total_absorbed_mass: T = new_absorbed.iter().copied().sum();
517
518 let final_flowable: Vec<T> = new_flowable
519 .into_iter()
520 .enumerate()
521 .map(|(v, flowable)| flowable + total_absorbed_mass * personalisation_vector[v])
522 .collect();
523
524 let total_ranks: Vec<T> = final_flowable
526 .iter()
527 .zip(&new_absorbed)
528 .map(|(f, a)| *f + *a)
529 .collect();
530
531 let l1 = total_ranks
533 .par_iter()
534 .zip(flowable_ranks.par_iter().zip(&absorbed_ranks))
535 .map(|(new_total, (old_flow, old_abs))| (*new_total - (*old_flow + *old_abs)).abs())
536 .sum::<T>();
537
538 flowable_ranks = final_flowable;
539 absorbed_ranks = new_absorbed;
540
541 if l1 <= tolerance {
542 break;
543 }
544 }
545
546 let mut ranks: Vec<T> = flowable_ranks
548 .iter()
549 .zip(&absorbed_ranks)
550 .map(|(f, a)| *f + *a)
551 .collect();
552
553 let sum: T = ranks.iter().copied().sum();
555 if sum > T::zero() {
556 ranks.iter_mut().for_each(|x| *x /= sum);
557 }
558
559 ranks
560}
561
562#[derive(Clone, Debug)]
569pub struct ConstrainedPageRankGraph<T> {
570 node_count: usize,
572 n_types: usize,
574 node_type: Vec<u16>,
576 is_sink_type: Vec<bool>,
578 type_weights: Option<Vec<T>>,
580 in_indptr: Vec<usize>,
582 in_indices: Vec<u32>,
584 in_weights: Vec<T>,
586 out_indptr: Vec<usize>,
588 out_indices: Vec<u32>,
590 out_weights: Vec<T>,
592 base_factors: Vec<T>,
594}
595
596impl<T> ConstrainedPageRankGraph<T>
597where
598 T: BixverseFloat + std::iter::Sum,
599{
600 pub fn new(
620 node_types: &[String],
621 from: &[usize],
622 to: &[usize],
623 weights: Option<&[T]>,
624 type_weights: Option<&FxHashMap<String, T>>,
625 sink_types: &FxHashSet<String>,
626 undirected: bool,
627 ) -> Result<Self, MethodsErrors> {
628 let node_count = node_types.len();
629 if node_count == 0 {
630 return Err(CoreErrors::InvalidArgument("The graph has no nodes.".to_string()).into());
631 }
632 if node_count > u32::MAX as usize {
633 return Err(CoreErrors::InvalidArgument(format!(
634 "{node_count} nodes exceed the u32 index range."
635 ))
636 .into());
637 }
638 if to.len() != from.len() {
639 return Err(MethodsErrors::GraphLengthMismatch {
640 name: "to",
641 expected: from.len(),
642 got: to.len(),
643 });
644 }
645 if let Some(w) = weights
646 && w.len() != from.len()
647 {
648 return Err(MethodsErrors::GraphLengthMismatch {
649 name: "weights",
650 expected: from.len(),
651 got: w.len(),
652 });
653 }
654
655 let mut type_ids: FxHashMap<&str, u16> = FxHashMap::default();
657 let mut type_names: Vec<&str> = Vec::new();
658 let mut node_type = Vec::with_capacity(node_count);
659 for t in node_types {
660 let id = match type_ids.get(t.as_str()) {
661 Some(&id) => id,
662 None => {
663 if type_names.len() >= u16::MAX as usize {
664 return Err(CoreErrors::InvalidArgument(
665 "Too many distinct node types.".to_string(),
666 )
667 .into());
668 }
669 let id = type_names.len() as u16;
670 type_ids.insert(t.as_str(), id);
671 type_names.push(t.as_str());
672 id
673 }
674 };
675 node_type.push(id);
676 }
677 let n_types = type_names.len();
678
679 let type_weights = match type_weights {
680 Some(map) => {
681 let mut w = Vec::with_capacity(n_types);
682 for name in &type_names {
683 let w_t = *map
684 .get(*name)
685 .ok_or_else(|| MethodsErrors::MissingNodeTypeWeight(name.to_string()))?;
686 if !(w_t.is_finite() && w_t > T::zero()) {
687 return Err(CoreErrors::InvalidArgument(format!(
688 "The weight of node type '{name}' must be positive and finite."
689 ))
690 .into());
691 }
692 w.push(w_t);
693 }
694 Some(w)
695 }
696 None => None,
697 };
698
699 let is_sink_type: Vec<bool> = node_types.iter().map(|t| sink_types.contains(t)).collect();
700
701 let n_edges = if undirected {
702 2 * from.len()
703 } else {
704 from.len()
705 };
706 let mut src: Vec<u32> = Vec::with_capacity(n_edges);
707 let mut dst: Vec<u32> = Vec::with_capacity(n_edges);
708 let mut edge_w: Vec<T> = Vec::with_capacity(n_edges);
709 for (k, (&a, &b)) in from.iter().zip(to).enumerate() {
710 for (name, idx) in [("from", a), ("to", b)] {
711 if idx >= node_count {
712 return Err(MethodsErrors::GraphIndexOutOfRange {
713 name,
714 index: idx,
715 n_nodes: node_count,
716 });
717 }
718 }
719 let w_k = weights.map_or(T::one(), |w| w[k]);
720 if !(w_k.is_finite() && w_k >= T::zero()) {
721 return Err(CoreErrors::InvalidArgument(format!(
722 "Edge weight {k} must be non-negative and finite."
723 ))
724 .into());
725 }
726 src.push(a as u32);
727 dst.push(b as u32);
728 edge_w.push(w_k);
729 if undirected {
730 src.push(b as u32);
731 dst.push(a as u32);
732 edge_w.push(w_k);
733 }
734 }
735
736 let (out_indptr, out_indices, out_weights) = edges_to_csr(node_count, &src, &dst, &edge_w);
737 let (in_indptr, in_indices, in_weights) = edges_to_csr(node_count, &dst, &src, &edge_w);
738
739 let mut graph = Self {
740 node_count,
741 n_types,
742 node_type,
743 is_sink_type,
744 type_weights,
745 in_indptr,
746 in_indices,
747 in_weights,
748 out_indptr,
749 out_indices,
750 out_weights,
751 base_factors: Vec::new(),
752 };
753
754 let no_seeds = vec![false; node_count];
755 let mut base_factors = vec![T::zero(); node_count * n_types];
756 base_factors
757 .par_chunks_mut(n_types)
758 .enumerate()
759 .for_each(|(i, row)| graph.fill_factor_row(i, &no_seeds, row));
760 graph.base_factors = base_factors;
761
762 Ok(graph)
763 }
764
765 pub fn node_count(&self) -> usize {
771 self.node_count
772 }
773
774 fn fill_factor_row(&self, i: usize, seed_mask: &[bool], row: &mut [T]) {
785 row.fill(T::zero());
786 if self.is_sink_type[i] && !seed_mask[i] {
787 return;
788 }
789
790 for k in self.out_indptr[i]..self.out_indptr[i + 1] {
791 let j = self.out_indices[k] as usize;
792 if !seed_mask[j] {
793 row[self.node_type[j] as usize] += self.out_weights[k];
794 }
795 }
796
797 match &self.type_weights {
798 Some(w) => {
799 let denom: T = row
800 .iter()
801 .zip(w)
802 .filter(|(s, _)| **s > T::zero())
803 .map(|(_, w_t)| *w_t)
804 .sum();
805 for (s, w_t) in row.iter_mut().zip(w) {
806 *s = if *s > T::zero() {
807 *w_t / (denom * *s)
808 } else {
809 T::zero()
810 };
811 }
812 }
813 None => {
814 let total: T = row.iter().copied().sum();
815 let f = if total > T::zero() {
816 T::one() / total
817 } else {
818 T::zero()
819 };
820 row.fill(f);
821 }
822 }
823 }
824}
825
826#[derive(Debug)]
830pub struct ConstrainedPageRankWorkingMemory<T> {
831 ranks: Vec<T>,
833 new_ranks: Vec<T>,
835 factors: Vec<T>,
837 seed_mask: Vec<bool>,
839 touched: Vec<usize>,
841}
842
843impl<T> ConstrainedPageRankWorkingMemory<T>
844where
845 T: BixverseFloat,
846{
847 pub fn new() -> Self {
853 Self {
854 ranks: Vec::new(),
855 new_ranks: Vec::new(),
856 factors: Vec::new(),
857 seed_mask: Vec::new(),
858 touched: Vec::new(),
859 }
860 }
861}
862
863impl<T> Default for ConstrainedPageRankWorkingMemory<T>
864where
865 T: BixverseFloat,
866{
867 fn default() -> Self {
868 Self::new()
869 }
870}
871
872pub fn constrained_personalised_page_rank_optimised<T>(
905 graph: &ConstrainedPageRankGraph<T>,
906 damping_factor: T,
907 personalisation_vector: &[T],
908 max_iter: usize,
909 tolerance: T,
910 working_memory: &mut ConstrainedPageRankWorkingMemory<T>,
911) -> Result<Vec<T>, MethodsErrors>
912where
913 T: BixverseFloat + std::iter::Sum,
914{
915 let n = graph.node_count;
916 let nt = graph.n_types;
917
918 if personalisation_vector.len() != n {
919 return Err(MethodsErrors::GraphLengthMismatch {
920 name: "personalisation_vector",
921 expected: n,
922 got: personalisation_vector.len(),
923 });
924 }
925 if !(T::zero() <= damping_factor && damping_factor <= T::one()) {
926 return Err(CoreErrors::InvalidArgument(
927 "The damping factor must be within [0, 1].".to_string(),
928 )
929 .into());
930 }
931 if personalisation_vector
932 .iter()
933 .any(|p| !(p.is_finite() && *p >= T::zero()))
934 {
935 return Err(CoreErrors::InvalidArgument(
936 "The personalisation vector must be non-negative and finite.".to_string(),
937 )
938 .into());
939 }
940 let p_sum: T = personalisation_vector.iter().copied().sum();
941 if p_sum <= T::zero() {
942 return Err(CoreErrors::InvalidArgument(
943 "The personalisation vector sums to zero.".to_string(),
944 )
945 .into());
946 }
947
948 let ConstrainedPageRankWorkingMemory {
949 ranks,
950 new_ranks,
951 factors,
952 seed_mask,
953 touched,
954 } = working_memory;
955
956 ranks.resize(n, T::zero());
957 new_ranks.resize(n, T::zero());
958 factors.clear();
959 factors.extend_from_slice(&graph.base_factors);
960 seed_mask.clear();
961 seed_mask.extend(personalisation_vector.iter().map(|p| *p > T::zero()));
962
963 touched.clear();
965 for s in (0..n).filter(|&s| seed_mask[s]) {
966 touched.push(s);
967 let (start, end) = (graph.in_indptr[s], graph.in_indptr[s + 1]);
968 touched.extend(graph.in_indices[start..end].iter().map(|&w| w as usize));
969 }
970 for &i in touched.iter() {
971 graph.fill_factor_row(i, seed_mask, &mut factors[i * nt..(i + 1) * nt]);
972 }
973
974 ranks.copy_from_slice(personalisation_vector);
975 let teleport_factor = T::one() - damping_factor;
976
977 for _ in 0..max_iter {
978 for v in 0..n {
979 let mut rank = teleport_factor * personalisation_vector[v];
980 if !seed_mask[v] {
981 let t_v = graph.node_type[v] as usize;
982 for k in graph.in_indptr[v]..graph.in_indptr[v + 1] {
983 let w = graph.in_indices[k] as usize;
984 rank += damping_factor * ranks[w] * factors[w * nt + t_v] * graph.in_weights[k];
985 }
986 }
987 new_ranks[v] = rank;
988 }
989
990 let l1: T = new_ranks
991 .iter()
992 .zip(ranks.iter())
993 .map(|(a, b)| (*a - *b).abs())
994 .sum();
995
996 std::mem::swap(ranks, new_ranks);
997
998 if l1 <= tolerance {
999 break;
1000 }
1001 }
1002
1003 let sum: T = ranks.iter().copied().sum();
1004 let mut res = ranks.clone();
1005 if sum > T::zero() {
1006 res.iter_mut().for_each(|x| *x /= sum);
1007 }
1008
1009 Ok(res)
1010}
1011
1012#[cfg(test)]
1017mod tests {
1018 use super::*;
1019 use crate::graph::graph_structures::graph_from_strings_with_attributes;
1020
1021 #[test]
1026 fn test_page_rank_matches_igraph_prpack() {
1027 let from: Vec<usize> = vec![0, 0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6];
1028 let to: Vec<usize> = vec![1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 0];
1029 let weights = vec![0.5, 1.5, 2.0, 0.3, 1.0, 0.7, 0.9, 1.2, 2.5, 0.4, 0.8, 0.0];
1030 let expected = [
1031 2.845_972_704_682_356_3e-1,
1032 5.691_945_409_364_712_6e-2,
1033 2.103_545_042_591_307_1e-1,
1034 1.049_297_762_422_016_8e-1,
1035 1.052_691_717_448_718_7e-1,
1036 1.321_832_351_066_182_5e-1,
1037 3.524_886_269_509_82e-2,
1038 7.049_772_539_019_64e-2,
1039 ];
1040 let mut p_vec = vec![0.0; 8];
1041 p_vec[0] = 1.0;
1042
1043 let graph = PageRankGraph::from_indices(8, &from, &to, Some(&weights), false);
1044
1045 let tol = DEFAULT_TOLERANCE;
1046 let optimised = personalised_page_rank_optimised(
1047 &graph,
1048 0.8,
1049 &p_vec,
1050 1000,
1051 tol,
1052 &mut PageRankWorkingMemory::new(),
1053 );
1054 let reference = personalised_page_rank(&graph, 0.8, &p_vec, 1000, None);
1055
1056 for i in 0..8 {
1057 assert!(
1058 (optimised[i] - expected[i]).abs() < 1e-10,
1059 "node {i}: optimised {} vs igraph {}",
1060 optimised[i],
1061 expected[i]
1062 );
1063 assert!(
1064 (reference[i] - expected[i]).abs() < 1e-10,
1065 "node {i}: reference {} vs igraph {}",
1066 reference[i],
1067 expected[i]
1068 );
1069 }
1070 }
1071
1072 #[test]
1076 fn test_constrained_page_rank_sink_edge_absorbs_mass() {
1077 let nodes: Vec<String> = ["a", "b", "c"].iter().map(|s| s.to_string()).collect();
1078 let node_types: Vec<String> = vec!["n".to_string(); 3];
1079 let from = vec!["a".to_string(), "b".to_string()];
1080 let to = vec!["b".to_string(), "c".to_string()];
1081 let edge_types = vec!["sink".to_string(), "flow".to_string()];
1082 let weights = vec![1.0, 1.0];
1083 let graph = graph_from_strings_with_attributes(
1084 &nodes,
1085 &node_types,
1086 &from,
1087 &to,
1088 &edge_types,
1089 &weights,
1090 );
1091 let p_vec = [1.0, 0.0, 0.0];
1092 let sink_edges: FxHashSet<String> = ["sink".to_string()].into_iter().collect();
1093
1094 let constrained = constrained_personalised_page_rank(
1095 &graph,
1096 0.85,
1097 &p_vec,
1098 1000,
1099 None,
1100 None,
1101 Some(&sink_edges),
1102 );
1103 let free = constrained_personalised_page_rank(&graph, 0.85, &p_vec, 1000, None, None, None);
1104
1105 assert!((constrained.iter().sum::<f64>() - 1.0).abs() < 1e-12);
1106 assert!(constrained[1] > 0.0);
1107 assert_eq!(constrained[2], 0.0);
1108 assert!(free[2] > 0.0);
1109 }
1110
1111 fn toy_multiscale() -> (Vec<String>, Vec<usize>, Vec<usize>, FxHashMap<String, f64>) {
1114 let types: Vec<String> = [
1115 "drug", "drug", "protein", "protein", "protein", "function", "disease",
1116 ]
1117 .iter()
1118 .map(|s| s.to_string())
1119 .collect();
1120 let from = vec![0, 0, 1, 2, 3, 2, 4, 6];
1121 let to = vec![2, 3, 3, 3, 4, 5, 5, 4];
1122 let type_weights: FxHashMap<String, f64> = [
1123 ("drug", 3.0),
1124 ("protein", 2.0),
1125 ("function", 1.5),
1126 ("disease", 4.0),
1127 ]
1128 .iter()
1129 .map(|(k, v)| (k.to_string(), *v))
1130 .collect();
1131 (types, from, to, type_weights)
1132 }
1133
1134 fn toy_sinks() -> FxHashSet<String> {
1135 ["drug", "disease"].iter().map(|s| s.to_string()).collect()
1136 }
1137
1138 #[test]
1141 fn test_constrained_optimised_matches_dense_reference() {
1142 let (types, from, to, tw) = toy_multiscale();
1143 let sinks = toy_sinks();
1144 let n = types.len();
1145 let seed = 0;
1146 let alpha = 0.85;
1147
1148 let is_sink = |i: usize| sinks.contains(&types[i]) && i != seed;
1151 let mut adj = vec![vec![false; n]; n];
1152 for (&a, &b) in from.iter().zip(&to) {
1153 adj[a][b] = true;
1154 adj[b][a] = true;
1155 }
1156 for (i, row) in adj.iter_mut().enumerate() {
1157 row[seed] = false;
1158 if is_sink(i) {
1159 row.fill(false);
1160 }
1161 }
1162 let mut m = vec![vec![0.0; n]; n];
1163 for i in 0..n {
1164 let mut counts: FxHashMap<&str, f64> = FxHashMap::default();
1165 for j in (0..n).filter(|&j| adj[i][j]) {
1166 *counts.entry(types[j].as_str()).or_default() += 1.0;
1167 }
1168 let denom: f64 = counts.keys().map(|t| tw[*t]).sum();
1169 for j in (0..n).filter(|&j| adj[i][j]) {
1170 let t = types[j].as_str();
1171 m[i][j] = tw[t] / denom / counts[t];
1172 }
1173 }
1174 let mut s = vec![0.0; n];
1175 s[seed] = 1.0;
1176 let mut r = s.clone();
1177 for _ in 0..5000 {
1178 let sink_mass: f64 = (0..n).filter(|&j| is_sink(j)).map(|j| r[j]).sum();
1179 r = (0..n)
1180 .map(|v| {
1181 let walk: f64 = (0..n).map(|i| r[i] * m[i][v]).sum();
1182 (1.0 - alpha) * s[v] + alpha * s[v] * sink_mass + alpha * walk
1183 })
1184 .collect();
1185 }
1186 let total: f64 = r.iter().sum();
1187 r.iter_mut().for_each(|x| *x /= total);
1188
1189 let graph =
1190 ConstrainedPageRankGraph::new(&types, &from, &to, None, Some(&tw), &sinks, true)
1191 .unwrap();
1192 let mut mem = ConstrainedPageRankWorkingMemory::new();
1193 let res = constrained_personalised_page_rank_optimised(
1194 &graph, alpha, &s, 10_000, 1e-14, &mut mem,
1195 )
1196 .unwrap();
1197
1198 for (i, (a, b)) in r.iter().zip(&res).enumerate() {
1199 assert!((a - b).abs() < 1e-9, "node {i}: dense {a} vs optimised {b}");
1200 }
1201 assert!((res.iter().sum::<f64>() - 1.0).abs() < 1e-9);
1202 }
1203
1204 #[test]
1206 fn test_constrained_seed_is_source_not_sink() {
1207 let (types, from, to, tw) = toy_multiscale();
1208 let graph =
1209 ConstrainedPageRankGraph::new(&types, &from, &to, None, Some(&tw), &toy_sinks(), true)
1210 .unwrap();
1211 let mut mem = ConstrainedPageRankWorkingMemory::new();
1212 let mut s = vec![0.0; types.len()];
1213 s[0] = 1.0;
1214 let res =
1215 constrained_personalised_page_rank_optimised(&graph, 0.85, &s, 1000, 1e-12, &mut mem)
1216 .unwrap();
1217
1218 assert!(res[0] < 0.5);
1219 assert!(res[1] > 0.0);
1220 assert!(res[6] > 0.0);
1221 }
1222
1223 #[test]
1226 fn test_constrained_type_weighted_rows_sum_to_one() {
1227 let (types, from, to, tw) = toy_multiscale();
1228 let graph =
1229 ConstrainedPageRankGraph::new(&types, &from, &to, None, Some(&tw), &toy_sinks(), true)
1230 .unwrap();
1231 let nt = graph.n_types;
1232 for i in 0..graph.node_count {
1233 let row_sum: f64 = (graph.out_indptr[i]..graph.out_indptr[i + 1])
1234 .map(|k| {
1235 let j = graph.out_indices[k] as usize;
1236 graph.base_factors[i * nt + graph.node_type[j] as usize] * graph.out_weights[k]
1237 })
1238 .sum();
1239 let expected = if graph.is_sink_type[i] { 0.0 } else { 1.0 };
1240 assert!((row_sum - expected).abs() < 1e-12, "node {i}: {row_sum}");
1241 }
1242 }
1243
1244 #[test]
1247 fn test_constrained_working_memory_reuse() {
1248 let (types, from, to, tw) = toy_multiscale();
1249 let graph =
1250 ConstrainedPageRankGraph::new(&types, &from, &to, None, Some(&tw), &toy_sinks(), true)
1251 .unwrap();
1252 let n = types.len();
1253 let mut s0 = vec![0.0; n];
1254 s0[0] = 1.0;
1255 let mut s6 = vec![0.0; n];
1256 s6[6] = 1.0;
1257
1258 let mut mem = ConstrainedPageRankWorkingMemory::new();
1259 let _ =
1260 constrained_personalised_page_rank_optimised(&graph, 0.85, &s0, 1000, 1e-12, &mut mem)
1261 .unwrap();
1262 let reused =
1263 constrained_personalised_page_rank_optimised(&graph, 0.85, &s6, 1000, 1e-12, &mut mem)
1264 .unwrap();
1265 let fresh = constrained_personalised_page_rank_optimised(
1266 &graph,
1267 0.85,
1268 &s6,
1269 1000,
1270 1e-12,
1271 &mut ConstrainedPageRankWorkingMemory::new(),
1272 )
1273 .unwrap();
1274
1275 assert_eq!(reused, fresh);
1276 }
1277
1278 #[test]
1280 fn test_constrained_rejects_bad_input() {
1281 let (types, _, _, tw) = toy_multiscale();
1282 let sinks = toy_sinks();
1283 assert!(
1284 ConstrainedPageRankGraph::new(&types, &[0], &[99], None, Some(&tw), &sinks, true)
1285 .is_err()
1286 );
1287 let mut partial = tw.clone();
1288 partial.remove("function");
1289 assert!(
1290 ConstrainedPageRankGraph::new(&types, &[0], &[2], None, Some(&partial), &sinks, true)
1291 .is_err()
1292 );
1293
1294 let graph =
1295 ConstrainedPageRankGraph::new(&types, &[0], &[2], None, Some(&tw), &sinks, true)
1296 .unwrap();
1297 let mut mem = ConstrainedPageRankWorkingMemory::new();
1298 let zeros = vec![0.0; types.len()];
1299 assert!(
1300 constrained_personalised_page_rank_optimised(&graph, 0.85, &zeros, 10, 1e-6, &mut mem)
1301 .is_err()
1302 );
1303 }
1304}