git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
246 lines
8.1 KiB
Rust
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
|
|
);
|
|
}
|