Squashed 'vendor/ruvector/' content from commit b64c2172

git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
commit d803bfe2b1
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
//! Integration tests for Forward Push, Backward Push, and mass conservation.
//!
//! Tests cover PPR computation on small graphs, star/complete topologies,
//! mass conservation invariants, and agreement between forward and backward
//! push algorithms.
mod helpers;
use approx::assert_relative_eq;
#[cfg(feature = "backward-push")]
use ruvector_solver::backward_push::BackwardPushSolver;
use ruvector_solver::forward_push::{forward_push_with_residuals, ForwardPushSolver};
#[allow(unused_imports)]
use ruvector_solver::traits::SublinearPageRank;
use ruvector_solver::types::CsrMatrix;
use helpers::adjacency_from_edges;
// ---------------------------------------------------------------------------
// Helper: build common graph topologies
// ---------------------------------------------------------------------------
/// 4-node graph: 0--1--2--3, 0--2 (bidirectional).
fn simple_graph_4() -> CsrMatrix<f64> {
adjacency_from_edges(4, &[(0, 1), (1, 2), (2, 3), (0, 2)])
}
/// Star graph centred at 0 with k leaves (bidirectional edges).
fn star_graph(k: usize) -> CsrMatrix<f64> {
let n = k + 1;
let edges: Vec<(usize, usize)> = (1..n).map(|i| (0, i)).collect();
adjacency_from_edges(n, &edges)
}
/// Complete graph on n vertices (bidirectional edges, no self-loops).
fn complete_graph(n: usize) -> CsrMatrix<f64> {
let mut edges = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
edges.push((i, j));
}
}
adjacency_from_edges(n, &edges)
}
/// Directed cycle: 0->1->2->...->n-1->0.
fn directed_cycle(n: usize) -> CsrMatrix<f64> {
let entries: Vec<(usize, usize, f64)> = (0..n).map(|i| (i, (i + 1) % n, 1.0f64)).collect();
CsrMatrix::<f64>::from_coo(n, n, entries)
}
// ---------------------------------------------------------------------------
// Forward Push: 4-node graph
// ---------------------------------------------------------------------------
#[test]
fn test_forward_push_simple_graph() {
let graph = simple_graph_4();
let solver = ForwardPushSolver::new(0.85, 1e-8);
let result = solver.ppr_from_source(&graph, 0).unwrap();
// Source should have highest PPR.
assert!(!result.is_empty());
assert_eq!(result[0].0, 0, "source vertex should be ranked first");
assert!(result[0].1 > 0.0);
// All returned scores should be positive and sorted descending.
for w in result.windows(2) {
assert!(
w[0].1 >= w[1].1,
"results should be sorted descending: {} < {}",
w[0].1,
w[1].1
);
}
// Verify all 4 nodes get some probability.
let nodes: Vec<usize> = result.iter().map(|(v, _)| *v).collect();
for v in 0..4 {
assert!(
nodes.contains(&v),
"node {} should appear in PPR results",
v
);
}
// Neighbours of source should have more PPR than non-neighbours.
let ppr: Vec<f64> = {
let mut dense = vec![0.0f64; 4];
for &(v, s) in &result {
dense[v] = s;
}
dense
};
// 0 is connected to 1 and 2. 3 is only reachable through 2.
assert!(
ppr[1] > ppr[3] || ppr[2] > ppr[3],
"direct neighbours should have higher PPR than distant nodes"
);
}
// ---------------------------------------------------------------------------
// Forward Push: star graph — center should dominate
// ---------------------------------------------------------------------------
#[test]
fn test_forward_push_star_graph() {
let graph = star_graph(5); // 6 nodes: center=0, leaves=1..5
let solver = ForwardPushSolver::new(0.85, 1e-8);
// PPR from center: center should have highest score.
let result = solver.ppr_from_source(&graph, 0).unwrap();
assert_eq!(result[0].0, 0);
// All leaf scores should be approximately equal (by symmetry).
let leaf_scores: Vec<f64> = result
.iter()
.filter(|(v, _)| *v != 0)
.map(|(_, s)| *s)
.collect();
assert_eq!(leaf_scores.len(), 5);
let mean = leaf_scores.iter().sum::<f64>() / leaf_scores.len() as f64;
for &s in &leaf_scores {
assert_relative_eq!(s, mean, epsilon = 1e-6);
}
// Center PPR should be strictly higher than any leaf.
for &s in &leaf_scores {
assert!(result[0].1 > s, "center PPR should exceed leaf PPR");
}
}
// ---------------------------------------------------------------------------
// Forward Push: complete graph — approximately uniform
// ---------------------------------------------------------------------------
#[test]
fn test_forward_push_complete_graph() {
let n = 5;
let graph = complete_graph(n);
let solver = ForwardPushSolver::new(0.85, 1e-8);
let result = solver.ppr_from_source(&graph, 0).unwrap();
// All n nodes should appear.
assert_eq!(result.len(), n);
// Non-source nodes should have approximately equal PPR.
let non_source: Vec<f64> = result
.iter()
.filter(|(v, _)| *v != 0)
.map(|(_, s)| *s)
.collect();
let mean = non_source.iter().sum::<f64>() / non_source.len() as f64;
for &s in &non_source {
assert_relative_eq!(s, mean, epsilon = 1e-6);
}
}
// ---------------------------------------------------------------------------
// Forward Push: mass conservation
// ---------------------------------------------------------------------------
#[test]
fn test_forward_push_mass_conservation() {
let graph = simple_graph_4();
let (p, r) = forward_push_with_residuals(&graph, 0, 0.85, 1e-8).unwrap();
let total_p: f64 = p.iter().sum();
let total_r: f64 = r.iter().sum();
let total = total_p + total_r;
assert_relative_eq!(total, 1.0, epsilon = 1e-6);
// Also verify on star graph.
let star = star_graph(4);
let (p2, r2) = forward_push_with_residuals(&star, 0, 0.85, 1e-6).unwrap();
let total2 = p2.iter().sum::<f64>() + r2.iter().sum::<f64>();
assert_relative_eq!(total2, 1.0, epsilon = 1e-5);
// And on directed cycle.
let cycle = directed_cycle(6);
let (p3, r3) = forward_push_with_residuals(&cycle, 0, 0.85, 1e-6).unwrap();
let total3 = p3.iter().sum::<f64>() + r3.iter().sum::<f64>();
assert_relative_eq!(total3, 1.0, epsilon = 1e-5);
}
// ---------------------------------------------------------------------------
// Backward Push: simple verification
// ---------------------------------------------------------------------------
#[cfg(feature = "backward-push")]
#[test]
fn test_backward_push_simple() {
let graph = directed_cycle(4); // 0->1->2->3->0
let solver = BackwardPushSolver::new(0.15, 1e-6);
// Backward push to target 0: nodes that can reach 0 should have PPR.
let result = solver.ppr_to_target(&graph, 0).unwrap();
assert!(!result.is_empty());
// The target node itself should have the highest PPR.
let target_ppr = result
.iter()
.find(|&&(v, _)| v == 0)
.map(|&(_, p)| p)
.unwrap_or(0.0);
assert!(target_ppr > 0.0, "target should have positive PPR");
// Total PPR should be <= 1.
let total: f64 = result.iter().map(|(_, v)| v).sum();
assert!(
total <= 1.0 + 1e-6,
"total PPR should be <= 1, got {}",
total
);
}
// ---------------------------------------------------------------------------
// Random walk pairwise: forward and backward push should agree
// ---------------------------------------------------------------------------
#[cfg(feature = "backward-push")]
#[test]
fn test_random_walk_pairwise() {
// On a symmetric graph, forward push from s and backward push to s
// should produce similar PPR distributions (up to algorithm variance).
let graph = complete_graph(5);
let forward = ForwardPushSolver::new(0.15, 1e-8);
let backward = BackwardPushSolver::new(0.15, 1e-8);
let source = 0;
// Forward push from source 0.
let fwd_result = forward.ppr_from_source(&graph, source).unwrap();
let mut fwd_ppr = vec![0.0f64; 5];
for &(v, s) in &fwd_result {
fwd_ppr[v] = s;
}
// Backward push to target 0.
let bwd_result = backward.ppr_to_target(&graph, source).unwrap();
let mut bwd_ppr = vec![0.0f64; 5];
for &(v, s) in &bwd_result {
bwd_ppr[v] = s;
}
// On a symmetric complete graph, forward PPR(0 -> v) should equal
// backward PPR(v -> 0), which is what backward push computes.
// The self-PPR (source=target=0) should match closely.
let fwd_self = fwd_ppr[0];
let bwd_self = bwd_ppr[0];
// They should agree to reasonable precision on a symmetric graph.
let self_ppr_diff = (fwd_self - bwd_self).abs();
assert!(
self_ppr_diff < 0.1,
"self-PPR should agree: forward={}, backward={}, diff={}",
fwd_self,
bwd_self,
self_ppr_diff
);
// Non-source nodes should have similar PPR in both directions
// (by symmetry of the complete graph).
for v in 1..5 {
let diff = (fwd_ppr[v] - bwd_ppr[v]).abs();
assert!(
diff < 0.1,
"PPR for node {} should agree: forward={}, backward={}, diff={}",
v,
fwd_ppr[v],
bwd_ppr[v],
diff
);
}
}