Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
281
crates/ruvector-solver/tests/test_push.rs
Normal file
281
crates/ruvector-solver/tests/test_push.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user