Files
wifi-densepose/crates/ruvector-solver/tests/test_router.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

246 lines
8.1 KiB
Rust

//! Integration tests for the algorithm router and solver orchestrator.
//!
//! Tests cover routing decisions (Neumann for diag-dominant, CG for general
//! SPD, ForwardPush for PageRank), and the fallback chain behaviour.
mod helpers;
use ruvector_solver::router::{RouterConfig, SolverOrchestrator, SolverRouter};
use ruvector_solver::types::{Algorithm, ComputeBudget, CsrMatrix, QueryType, SparsityProfile};
use helpers::{random_diag_dominant_csr, random_spd_csr, random_vector};
// ---------------------------------------------------------------------------
// Helper: default compute budget
// ---------------------------------------------------------------------------
fn default_budget() -> ComputeBudget {
ComputeBudget {
max_time: std::time::Duration::from_secs(30),
max_iterations: 10_000,
tolerance: 1e-8,
}
}
// ---------------------------------------------------------------------------
// Router selects Neumann for diag-dominant + sparse + low spectral radius
// ---------------------------------------------------------------------------
#[test]
fn test_router_selects_neumann_for_diag_dominant() {
let router = SolverRouter::new(RouterConfig::default());
// Construct a profile that satisfies all Neumann conditions:
// - diag-dominant
// - density below sparsity_sublinear_threshold (0.05)
// - spectral radius below neumann_spectral_radius_threshold (0.95)
let profile = SparsityProfile {
rows: 1000,
cols: 1000,
nnz: 3000,
density: 0.003,
is_diag_dominant: true,
estimated_spectral_radius: 0.5,
estimated_condition: 10.0,
is_symmetric_structure: true,
avg_nnz_per_row: 3.0,
max_nnz_per_row: 5,
};
let algo = router.select_algorithm(&profile, &QueryType::LinearSystem);
assert_eq!(
algo,
Algorithm::Neumann,
"diag-dominant, sparse, low spectral radius should route to Neumann"
);
// Also verify with a real matrix: build a diag-dominant matrix and check
// that the orchestrator's analyze_sparsity reports diag-dominance.
let matrix = random_diag_dominant_csr(20, 0.2, 42);
let real_profile = SolverOrchestrator::analyze_sparsity(&matrix);
assert!(
real_profile.is_diag_dominant,
"random_diag_dominant_csr should produce a diag-dominant matrix"
);
assert!(
real_profile.estimated_spectral_radius < 1.0,
"spectral radius should be < 1 for diag-dominant matrix"
);
}
// ---------------------------------------------------------------------------
// Router selects CG for well-conditioned, non-diag-dominant systems
// ---------------------------------------------------------------------------
#[test]
fn test_router_selects_cg_for_general_spd() {
let router = SolverRouter::new(RouterConfig::default());
// Profile: not diag-dominant, but well-conditioned (condition < 100).
let profile = SparsityProfile {
rows: 500,
cols: 500,
nnz: 25_000,
density: 0.10,
is_diag_dominant: false,
estimated_spectral_radius: 0.8,
estimated_condition: 50.0,
is_symmetric_structure: true,
avg_nnz_per_row: 50.0,
max_nnz_per_row: 80,
};
let algo = router.select_algorithm(&profile, &QueryType::LinearSystem);
assert_eq!(
algo,
Algorithm::CG,
"well-conditioned, non-diag-dominant should route to CG"
);
// When condition number exceeds the threshold, should route to BMSSP.
let ill_conditioned = SparsityProfile {
estimated_condition: 500.0,
..profile.clone()
};
let algo_ill = router.select_algorithm(&ill_conditioned, &QueryType::LinearSystem);
assert_eq!(
algo_ill,
Algorithm::BMSSP,
"ill-conditioned should route to BMSSP"
);
}
// ---------------------------------------------------------------------------
// Router selects ForwardPush for PageRank queries
// ---------------------------------------------------------------------------
#[test]
fn test_router_selects_push_for_pagerank() {
let router = SolverRouter::new(RouterConfig::default());
let profile = SparsityProfile {
rows: 5000,
cols: 5000,
nnz: 20_000,
density: 0.0008,
is_diag_dominant: false,
estimated_spectral_radius: 0.85,
estimated_condition: 100.0,
is_symmetric_structure: false,
avg_nnz_per_row: 4.0,
max_nnz_per_row: 50,
};
// Single-source PageRank always routes to ForwardPush.
let algo_single = router.select_algorithm(&profile, &QueryType::PageRankSingle { source: 0 });
assert_eq!(
algo_single,
Algorithm::ForwardPush,
"single-source PageRank should route to ForwardPush"
);
// Pairwise on a large graph (rows > push_graph_size_threshold = 1000)
// routes to HybridRandomWalk.
let algo_pairwise_large = router.select_algorithm(
&profile,
&QueryType::PageRankPairwise {
source: 0,
target: 100,
},
);
assert_eq!(
algo_pairwise_large,
Algorithm::HybridRandomWalk,
"pairwise PageRank on large graph should route to HybridRandomWalk"
);
// Pairwise on a small graph routes to ForwardPush.
let small_profile = SparsityProfile {
rows: 500,
cols: 500,
nnz: 2000,
density: 0.008,
..profile.clone()
};
let algo_pairwise_small = router.select_algorithm(
&small_profile,
&QueryType::PageRankPairwise {
source: 0,
target: 10,
},
);
assert_eq!(
algo_pairwise_small,
Algorithm::ForwardPush,
"pairwise PageRank on small graph should route to ForwardPush"
);
}
// ---------------------------------------------------------------------------
// Fallback chain: if first algorithm fails, falls back to CG then Dense
// ---------------------------------------------------------------------------
#[test]
fn test_router_fallback_chain() {
let orchestrator = SolverOrchestrator::new(RouterConfig::default());
// Build a well-conditioned SPD system that is solvable.
// Use a simple diag-dominant tridiagonal so all algorithms can solve it.
let matrix = CsrMatrix::<f64>::from_coo(
4,
4,
vec![
(0, 0, 4.0),
(0, 1, -1.0),
(1, 0, -1.0),
(1, 1, 4.0),
(1, 2, -1.0),
(2, 1, -1.0),
(2, 2, 4.0),
(2, 3, -1.0),
(3, 2, -1.0),
(3, 3, 4.0),
],
);
let rhs = vec![1.0, 0.0, 0.0, 1.0];
let budget = default_budget();
// solve_with_fallback should succeed regardless of which algorithm is
// tried first (the fallback chain will eventually reach CG or Dense).
let result = orchestrator
.solve_with_fallback(&matrix, &rhs, QueryType::LinearSystem, &budget)
.unwrap();
assert!(
result.residual_norm < 1e-4,
"fallback chain should produce a good solution, residual={}",
result.residual_norm
);
// Verify the fallback chain deduplication: CG primary should give [CG, Dense].
// Neumann primary should give [Neumann, CG, Dense].
let profile = SolverOrchestrator::analyze_sparsity(&matrix);
let selected = orchestrator
.router()
.select_algorithm(&profile, &QueryType::LinearSystem);
// The selected algorithm for a diag-dominant sparse low-rho matrix should
// be Neumann, and the fallback chain should include CG and Dense.
// Just verify the solve succeeded, which proves fallback works end-to-end.
assert!(result.solution.len() == 4, "solution should have 4 entries");
// Test that solve_with_fallback also works on an SPD system that routes
// to CG. The fallback chain [CG, Dense] should handle it.
let spd = random_spd_csr(10, 0.3, 42);
let rhs2 = random_vector(10, 43);
let result2 = orchestrator
.solve_with_fallback(&spd, &rhs2, QueryType::LinearSystem, &budget)
.unwrap();
assert!(
result2.residual_norm < 1e-3,
"fallback on SPD should converge, residual={}",
result2.residual_norm
);
}