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