//! Shared test helpers for the ruvector-solver integration test suite. //! //! Provides deterministic random matrix generators, dense reference solvers, //! and floating-point comparison utilities used across all test modules. use ruvector_solver::types::CsrMatrix; // --------------------------------------------------------------------------- // Random number generator (simple LCG for deterministic reproducibility) // --------------------------------------------------------------------------- /// A minimal linear congruential generator for deterministic test data. /// /// Uses the Numerical Recipes LCG parameters. Not cryptographically secure, /// but perfectly adequate for generating reproducible test matrices. pub struct Lcg { state: u64, } impl Lcg { /// Create a new LCG with the given seed. pub fn new(seed: u64) -> Self { Self { state: seed } } /// Generate the next u64 value. pub fn next_u64(&mut self) -> u64 { self.state = self .state .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); self.state } /// Generate a uniform f64 in [0, 1). pub fn next_f64(&mut self) -> f64 { (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64 } /// Generate a uniform f64 in [lo, hi). pub fn next_f64_range(&mut self, lo: f64, hi: f64) -> f64 { lo + (hi - lo) * self.next_f64() } } // --------------------------------------------------------------------------- // Matrix generators // --------------------------------------------------------------------------- /// Generate a random diagonally dominant CSR matrix of dimension `n`. /// /// Each row has approximately `density * n` non-zero off-diagonal entries /// (at least 1). The diagonal entry is set to `1 + sum_of_abs_off_diag` /// to guarantee strict diagonal dominance. /// /// The resulting matrix is suitable for Neumann and Jacobi solvers. pub fn random_diag_dominant_csr(n: usize, density: f64, seed: u64) -> CsrMatrix { let mut rng = Lcg::new(seed); let mut entries: Vec<(usize, usize, f64)> = Vec::new(); for i in 0..n { let mut off_diag_sum = 0.0f64; for j in 0..n { if i == j { continue; } if rng.next_f64() < density { let val = rng.next_f64_range(-1.0, 1.0); entries.push((i, j, val)); off_diag_sum += val.abs(); } } // Ensure at least one off-diagonal entry per row for non-trivial testing. if off_diag_sum == 0.0 && n > 1 { let j = (i + 1) % n; let val = rng.next_f64_range(0.1, 0.5); entries.push((i, j, val)); off_diag_sum = val; } // Diagonal: strictly dominant. let diag_val = off_diag_sum + 1.0 + rng.next_f64(); entries.push((i, i, diag_val)); } CsrMatrix::::from_coo(n, n, entries) } /// Generate a random graph Laplacian CSR matrix of dimension `n`. /// /// A graph Laplacian `L = D - A` where: /// - `A` is the adjacency matrix of a random undirected graph. /// - `D` is the degree matrix. /// - Each row sums to zero (L * ones = 0). /// /// The resulting matrix is symmetric positive semi-definite. pub fn random_laplacian_csr(n: usize, density: f64, seed: u64) -> CsrMatrix { let mut rng = Lcg::new(seed); // Build symmetric adjacency: for i < j, randomly include edge (i,j). let mut adj = vec![vec![0.0f64; n]; n]; for i in 0..n { for j in (i + 1)..n { if rng.next_f64() < density { let weight = rng.next_f64_range(0.1, 2.0); adj[i][j] = weight; adj[j][i] = weight; } } } // Ensure the graph is connected: add a path 0-1-2-...-n-1. for i in 0..n.saturating_sub(1) { if adj[i][i + 1] == 0.0 { let weight = rng.next_f64_range(0.1, 1.0); adj[i][i + 1] = weight; adj[i + 1][i] = weight; } } // Build Laplacian: L[i][j] = -A[i][j] for i != j, L[i][i] = sum_j A[i][j]. let mut entries: Vec<(usize, usize, f64)> = Vec::new(); for i in 0..n { let mut degree = 0.0f64; for j in 0..n { if i != j && adj[i][j] != 0.0 { entries.push((i, j, -adj[i][j])); degree += adj[i][j]; } } entries.push((i, i, degree)); } CsrMatrix::::from_coo(n, n, entries) } /// Generate a random SPD (symmetric positive definite) matrix. /// /// Constructs `A = B^T B + epsilon * I` where `B` has random entries, /// guaranteeing positive definiteness. pub fn random_spd_csr(n: usize, density: f64, seed: u64) -> CsrMatrix { let mut rng = Lcg::new(seed); // Build a random dense matrix B, then compute A = B^T B + eps * I. // For efficiency with CSR, we do this differently: build a sparse // symmetric matrix and add a diagonal shift. let mut dense = vec![vec![0.0f64; n]; n]; for i in 0..n { for j in i..n { if i == j || rng.next_f64() < density { let val = rng.next_f64_range(-1.0, 1.0); dense[i][j] += val; if i != j { dense[j][i] += val; } } } } // Compute A = M^T M where M = dense (makes it PSD). let mut a = vec![vec![0.0f64; n]; n]; for i in 0..n { for j in 0..n { let mut sum = 0.0; for k in 0..n { sum += dense[k][i] * dense[k][j]; } a[i][j] = sum; } } // Add diagonal shift to ensure positive definiteness. for i in 0..n { a[i][i] += 1.0; } // Convert to COO. let mut entries: Vec<(usize, usize, f64)> = Vec::new(); for i in 0..n { for j in 0..n { if a[i][j].abs() > 1e-15 { entries.push((i, j, a[i][j])); } } } CsrMatrix::::from_coo(n, n, entries) } /// Generate a deterministic random vector of length `n`. pub fn random_vector(n: usize, seed: u64) -> Vec { let mut rng = Lcg::new(seed); (0..n).map(|_| rng.next_f64_range(-1.0, 1.0)).collect() } /// Build a simple undirected graph as a CSR adjacency matrix. /// /// Each entry `(u, v)` in `edges` creates entries `A[u][v] = 1` and /// `A[v][u] = 1`. pub fn adjacency_from_edges(n: usize, edges: &[(usize, usize)]) -> CsrMatrix { let mut entries: Vec<(usize, usize, f64)> = Vec::new(); for &(u, v) in edges { entries.push((u, v, 1.0)); if u != v { entries.push((v, u, 1.0)); } } CsrMatrix::::from_coo(n, n, entries) } // --------------------------------------------------------------------------- // Dense reference solver // --------------------------------------------------------------------------- /// Solve `Ax = b` using dense Gaussian elimination with partial pivoting. /// /// This is an O(n^3) reference solver used only for small test problems /// to verify iterative solver accuracy. /// /// # Panics /// /// Panics if the matrix is singular or dimensions are inconsistent. pub fn dense_solve(matrix: &CsrMatrix, rhs: &[f64]) -> Vec { let n = matrix.rows; assert_eq!(n, matrix.cols, "dense_solve requires a square matrix"); assert_eq!(rhs.len(), n, "rhs length must match matrix dimension"); // Convert CSR to dense augmented matrix [A | b]. let mut aug = vec![vec![0.0f64; n + 1]; n]; for i in 0..n { aug[i][n] = rhs[i]; let start = matrix.row_ptr[i]; let end = matrix.row_ptr[i + 1]; for idx in start..end { let j = matrix.col_indices[idx]; aug[i][j] = matrix.values[idx]; } } // Forward elimination with partial pivoting. for col in 0..n { // Find pivot. let mut max_row = col; let mut max_val = aug[col][col].abs(); for row in (col + 1)..n { if aug[row][col].abs() > max_val { max_val = aug[row][col].abs(); max_row = row; } } assert!(max_val > 1e-15, "matrix is singular or near-singular"); aug.swap(col, max_row); // Eliminate. let pivot = aug[col][col]; for row in (col + 1)..n { let factor = aug[row][col] / pivot; for j in col..=n { aug[row][j] -= factor * aug[col][j]; } } } // Back substitution. let mut x = vec![0.0f64; n]; for i in (0..n).rev() { let mut sum = aug[i][n]; for j in (i + 1)..n { sum -= aug[i][j] * x[j]; } x[i] = sum / aug[i][i]; } x } // --------------------------------------------------------------------------- // Floating-point comparison utilities // --------------------------------------------------------------------------- /// Compute the L2 norm of a vector. pub fn l2_norm(v: &[f64]) -> f64 { v.iter().map(|&x| x * x).sum::().sqrt() } /// Compute the L2 distance between two vectors. pub fn l2_distance(a: &[f64], b: &[f64]) -> f64 { assert_eq!(a.len(), b.len(), "vectors must have same length"); a.iter() .zip(b.iter()) .map(|(&ai, &bi)| (ai - bi) * (ai - bi)) .sum::() .sqrt() } /// Compute the relative error ||approx - exact|| / ||exact||. /// /// Returns absolute error if the exact solution has zero norm. pub fn relative_error(approx: &[f64], exact: &[f64]) -> f64 { let exact_norm = l2_norm(exact); let error = l2_distance(approx, exact); if exact_norm > 1e-15 { error / exact_norm } else { error } } /// Compute the residual `b - A*x` for a sparse system. pub fn compute_residual(matrix: &CsrMatrix, x: &[f64], rhs: &[f64]) -> Vec { let n = matrix.rows; let mut ax = vec![0.0f64; n]; matrix.spmv(x, &mut ax); (0..n).map(|i| rhs[i] - ax[i]).collect() } /// Convert an f32 solution vector to f64 for comparison. pub fn f32_to_f64(v: &[f32]) -> Vec { v.iter().map(|&x| x as f64).collect() }