//! Node.js NAPI bindings for the RuVector sublinear-time solver. //! //! Provides high-performance sparse linear system solving, PageRank //! computation, and complexity estimation for Node.js applications. //! //! All heavy computation runs on worker threads via `tokio::task::spawn_blocking` //! to avoid blocking the Node.js event loop. #![deny(clippy::all)] use napi::bindgen_prelude::*; use napi_derive::napi; use ruvector_solver::types::Algorithm; use std::time::Instant; // --------------------------------------------------------------------------- // Configuration types (NAPI objects) // --------------------------------------------------------------------------- /// Configuration for solving a sparse linear system Ax = b. #[napi(object)] pub struct SolveConfig { /// Non-zero values in CSR format. pub values: Vec, /// Column indices for each non-zero entry. pub col_indices: Vec, /// Row pointers (length = rows + 1). pub row_ptrs: Vec, /// Number of rows in the matrix. pub rows: u32, /// Number of columns in the matrix. pub cols: u32, /// Right-hand side vector b. pub rhs: Vec, /// Convergence tolerance (default: 1e-6). pub tolerance: Option, /// Maximum number of iterations (default: 1000). pub max_iterations: Option, /// Algorithm to use: "neumann", "jacobi", "gauss-seidel", "conjugate-gradient". /// Defaults to "jacobi". pub algorithm: Option, } /// Result of solving a sparse linear system. #[napi(object)] pub struct SolveResult { /// Solution vector x. pub solution: Vec, /// Number of iterations performed. pub iterations: u32, /// Final residual norm ||Ax - b||. pub residual: f64, /// Whether the solver converged within tolerance. pub converged: bool, /// Algorithm that was used. pub algorithm: String, /// Wall-clock time in microseconds. pub time_us: u32, } /// Configuration for PageRank computation. #[napi(object)] pub struct PageRankConfig { /// Non-zero values in CSR format (edge weights). pub values: Vec, /// Column indices for each non-zero entry. pub col_indices: Vec, /// Row pointers (length = rows + 1). pub row_ptrs: Vec, /// Number of nodes in the graph. pub num_nodes: u32, /// Damping factor (default: 0.85). pub damping: Option, /// Convergence tolerance (default: 1e-6). pub tolerance: Option, /// Maximum number of iterations (default: 100). pub max_iterations: Option, /// Personalization vector (uniform if omitted). pub personalization: Option>, } /// Result of PageRank computation. #[napi(object)] pub struct PageRankResult { /// PageRank scores for each node (sums to 1.0). pub scores: Vec, /// Number of iterations performed. pub iterations: u32, /// Final convergence residual. pub residual: f64, /// Whether the computation converged. pub converged: bool, /// Wall-clock time in microseconds. pub time_us: u32, } /// Configuration for complexity estimation. #[napi(object)] pub struct ComplexityConfig { /// Number of rows in the matrix. pub rows: u32, /// Number of non-zero entries. pub nnz: u32, /// Algorithm to estimate for. pub algorithm: Option, } /// Result of complexity estimation. #[napi(object)] pub struct ComplexityResult { /// Estimated time complexity class (e.g. "O(n log n)"). pub complexity_class: String, /// Estimated number of floating-point operations. pub estimated_flops: f64, /// Recommended algorithm for this problem size. pub recommended_algorithm: String, /// Estimated wall-clock time in microseconds. pub estimated_time_us: f64, /// Sparsity ratio (nnz / n^2). pub sparsity: f64, } /// Convergence history entry. #[napi(object)] pub struct ConvergenceEntry { /// Iteration index (0-based). pub iteration: u32, /// Residual norm at this iteration. pub residual: f64, } /// Result of solving with convergence history. #[napi(object)] pub struct SolveWithHistoryResult { /// Solution vector x. pub solution: Vec, /// Number of iterations performed. pub iterations: u32, /// Final residual norm. pub residual: f64, /// Whether the solver converged. pub converged: bool, /// Algorithm used. pub algorithm: String, /// Wall-clock time in microseconds. pub time_us: u32, /// Per-iteration convergence history. pub convergence_history: Vec, } // --------------------------------------------------------------------------- // Internal helpers // --------------------------------------------------------------------------- /// Parse an algorithm name string into the core Algorithm enum. fn parse_algorithm(name: &str) -> Result { match name.to_lowercase().as_str() { "neumann" | "neumann-series" => Ok(Algorithm::Neumann), "jacobi" => Ok(Algorithm::Jacobi), "gauss-seidel" | "gaussseidel" | "gs" => Ok(Algorithm::GaussSeidel), "forward-push" | "forwardpush" => Ok(Algorithm::ForwardPush), "backward-push" | "backwardpush" => Ok(Algorithm::BackwardPush), "conjugate-gradient" | "cg" => Ok(Algorithm::CG), other => Err(Error::new( Status::InvalidArg, format!( "Unknown algorithm '{}'. Expected one of: neumann, jacobi, \ gauss-seidel, conjugate-gradient, forward-push, backward-push", other ), )), } } /// Validate CSR input dimensions for consistency. fn validate_csr_input( values: &[f64], col_indices: &[u32], row_ptrs: &[u32], rows: usize, cols: usize, ) -> Result<()> { if row_ptrs.len() != rows + 1 { return Err(Error::new( Status::InvalidArg, format!( "row_ptrs length {} does not equal rows + 1 = {}", row_ptrs.len(), rows + 1 ), )); } if values.len() != col_indices.len() { return Err(Error::new( Status::InvalidArg, format!( "values length {} does not match col_indices length {}", values.len(), col_indices.len() ), )); } let expected_nnz = row_ptrs[rows] as usize; if values.len() != expected_nnz { return Err(Error::new( Status::InvalidArg, format!( "values length {} does not match row_ptrs[rows] = {}", values.len(), expected_nnz ), )); } // Validate monotonicity of row_ptrs. for i in 1..row_ptrs.len() { if row_ptrs[i] < row_ptrs[i - 1] { return Err(Error::new( Status::InvalidArg, format!( "row_ptrs is not monotonically non-decreasing at position {}", i ), )); } } // Validate column indices and value finiteness. for (idx, (&col, &val)) in col_indices.iter().zip(values.iter()).enumerate() { if col as usize >= cols { return Err(Error::new( Status::InvalidArg, format!( "column index {} at position {} is out of bounds (cols={})", col, idx, cols ), )); } if !val.is_finite() { return Err(Error::new( Status::InvalidArg, format!("non-finite value {} at position {}", val, idx), )); } } Ok(()) } /// Sparse matrix-vector multiply y = A*x using CSR arrays. fn spmv_f64( row_ptrs: &[usize], col_indices: &[usize], values: &[f64], x: &[f64], y: &mut [f64], rows: usize, ) { for i in 0..rows { let start = row_ptrs[i]; let end = row_ptrs[i + 1]; let mut sum = 0.0f64; for idx in start..end { sum += values[idx] * x[col_indices[idx]]; } y[i] = sum; } } /// Compute L2 norm of the residual r = b - A*x. fn residual_norm( row_ptrs: &[usize], col_indices: &[usize], values: &[f64], x: &[f64], b: &[f64], rows: usize, ) -> f64 { let mut norm_sq = 0.0f64; for i in 0..rows { let start = row_ptrs[i]; let end = row_ptrs[i + 1]; let mut ax_i = 0.0f64; for idx in start..end { ax_i += values[idx] * x[col_indices[idx]]; } let r = b[i] - ax_i; norm_sq += r * r; } norm_sq.sqrt() } /// Extract the diagonal entries of a CSR matrix. /// /// Returns `None` if any diagonal entry is zero (or missing). fn extract_diagonal( row_ptrs: &[usize], col_indices: &[usize], values: &[f64], rows: usize, ) -> Option> { let mut diag = vec![0.0f64; rows]; for i in 0..rows { let start = row_ptrs[i]; let end = row_ptrs[i + 1]; let mut found = false; for idx in start..end { if col_indices[idx] == i { diag[i] = values[idx]; found = true; break; } } if !found || diag[i].abs() < 1e-15 { return None; } } Some(diag) } /// Jacobi iterative solver for Ax = b. /// /// Requires the diagonal of A to be non-zero. Iterates: /// x_{k+1}[i] = (b[i] - sum_{j!=i} a_{ij} * x_k[j]) / a_{ii} fn solve_jacobi( row_ptrs: &[usize], col_indices: &[usize], values: &[f64], rhs: &[f64], rows: usize, tolerance: f64, max_iterations: usize, ) -> (Vec, usize, f64, bool, Vec<(usize, f64)>) { let mut x = vec![0.0f64; rows]; let mut x_new = vec![0.0f64; rows]; let mut history = Vec::new(); let diag = match extract_diagonal(row_ptrs, col_indices, values, rows) { Some(d) => d, None => { let res = residual_norm(row_ptrs, col_indices, values, &x, rhs, rows); history.push((0, res)); return (x, 0, res, false, history); } }; let mut converged = false; let mut final_residual = f64::MAX; let mut iters = 0; for iter in 0..max_iterations { for i in 0..rows { let start = row_ptrs[i]; let end = row_ptrs[i + 1]; let mut sigma = 0.0f64; for idx in start..end { let j = col_indices[idx]; if j != i { sigma += values[idx] * x[j]; } } x_new[i] = (rhs[i] - sigma) / diag[i]; } std::mem::swap(&mut x, &mut x_new); let res = residual_norm(row_ptrs, col_indices, values, &x, rhs, rows); history.push((iter, res)); final_residual = res; iters = iter + 1; if res < tolerance { converged = true; break; } } (x, iters, final_residual, converged, history) } /// Gauss-Seidel iterative solver for Ax = b. /// /// Updates x in-place within each iteration using the most recent values. /// Generally converges faster than Jacobi for the same problem. fn solve_gauss_seidel( row_ptrs: &[usize], col_indices: &[usize], values: &[f64], rhs: &[f64], rows: usize, tolerance: f64, max_iterations: usize, ) -> (Vec, usize, f64, bool, Vec<(usize, f64)>) { let mut x = vec![0.0f64; rows]; let mut history = Vec::new(); let diag = match extract_diagonal(row_ptrs, col_indices, values, rows) { Some(d) => d, None => { let res = residual_norm(row_ptrs, col_indices, values, &x, rhs, rows); history.push((0, res)); return (x, 0, res, false, history); } }; let mut converged = false; let mut final_residual = f64::MAX; let mut iters = 0; for iter in 0..max_iterations { for i in 0..rows { let start = row_ptrs[i]; let end = row_ptrs[i + 1]; let mut sigma = 0.0f64; for idx in start..end { let j = col_indices[idx]; if j != i { sigma += values[idx] * x[j]; } } x[i] = (rhs[i] - sigma) / diag[i]; } let res = residual_norm(row_ptrs, col_indices, values, &x, rhs, rows); history.push((iter, res)); final_residual = res; iters = iter + 1; if res < tolerance { converged = true; break; } } (x, iters, final_residual, converged, history) } /// Neumann series solver: x = sum_{k=0}^{K} (I - A)^k * b. /// /// Converges when the spectral radius of (I - A) is less than 1. fn solve_neumann( row_ptrs: &[usize], col_indices: &[usize], values: &[f64], rhs: &[f64], rows: usize, tolerance: f64, max_iterations: usize, ) -> (Vec, usize, f64, bool, Vec<(usize, f64)>) { let mut x = vec![0.0f64; rows]; let mut term = rhs.to_vec(); let mut temp = vec![0.0f64; rows]; let mut history = Vec::new(); let mut converged = false; let mut final_residual = f64::MAX; let mut iters = 0; for iter in 0..max_iterations { // Accumulate current term into x. for i in 0..rows { x[i] += term[i]; } // Compute next term: term_{k+1} = (I - A) * term_k spmv_f64(row_ptrs, col_indices, values, &term, &mut temp, rows); for i in 0..rows { temp[i] = term[i] - temp[i]; } std::mem::swap(&mut term, &mut temp); let term_norm: f64 = term.iter().map(|&t| t * t).sum::().sqrt(); let res = residual_norm(row_ptrs, col_indices, values, &x, rhs, rows); history.push((iter, res)); final_residual = res; iters = iter + 1; if res < tolerance || term_norm < tolerance * 1e-2 { converged = true; break; } // Divergence detection. if !term_norm.is_finite() { break; } } (x, iters, final_residual, converged, history) } /// Conjugate gradient solver for symmetric positive-definite Ax = b. fn solve_cg( row_ptrs: &[usize], col_indices: &[usize], values: &[f64], rhs: &[f64], rows: usize, tolerance: f64, max_iterations: usize, ) -> (Vec, usize, f64, bool, Vec<(usize, f64)>) { let mut x = vec![0.0f64; rows]; let mut history = Vec::new(); // r = b - A*x (initially r = b since x = 0). let mut r = rhs.to_vec(); let mut p = r.clone(); let mut ap = vec![0.0f64; rows]; let mut rs_old: f64 = r.iter().map(|&v| v * v).sum(); let tol_sq = tolerance * tolerance; let mut converged = false; let mut final_residual = rs_old.sqrt(); let mut iters = 0; for iter in 0..max_iterations { spmv_f64(row_ptrs, col_indices, values, &p, &mut ap, rows); let p_ap: f64 = p.iter().zip(ap.iter()).map(|(&a, &b)| a * b).sum(); if p_ap.abs() < 1e-30 { break; } let alpha = rs_old / p_ap; for i in 0..rows { x[i] += alpha * p[i]; } for i in 0..rows { r[i] -= alpha * ap[i]; } let rs_new: f64 = r.iter().map(|&v| v * v).sum(); final_residual = rs_new.sqrt(); history.push((iter, final_residual)); iters = iter + 1; if rs_new < tol_sq { converged = true; break; } let beta = rs_new / rs_old; for i in 0..rows { p[i] = r[i] + beta * p[i]; } rs_old = rs_new; } (x, iters, final_residual, converged, history) } /// Dispatch to the appropriate solver based on algorithm selection. fn dispatch_solver( algo: Algorithm, row_ptrs: &[usize], col_indices: &[usize], values: &[f64], rhs: &[f64], rows: usize, tolerance: f64, max_iterations: usize, ) -> (Vec, usize, f64, bool, Vec<(usize, f64)>) { match algo { Algorithm::Jacobi => solve_jacobi( row_ptrs, col_indices, values, rhs, rows, tolerance, max_iterations, ), Algorithm::GaussSeidel => solve_gauss_seidel( row_ptrs, col_indices, values, rhs, rows, tolerance, max_iterations, ), Algorithm::Neumann => solve_neumann( row_ptrs, col_indices, values, rhs, rows, tolerance, max_iterations, ), Algorithm::CG => solve_cg( row_ptrs, col_indices, values, rhs, rows, tolerance, max_iterations, ), // Forward/backward push are graph algorithms, not general linear solvers. // Fall back to Jacobi. _ => solve_jacobi( row_ptrs, col_indices, values, rhs, rows, tolerance, max_iterations, ), } } // --------------------------------------------------------------------------- // NapiSolver // --------------------------------------------------------------------------- /// High-performance sparse linear solver with automatic algorithm selection. /// /// Provides async methods for solving Ax = b, computing PageRank, and /// estimating computational complexity. All heavy computation runs on /// worker threads. /// /// # Example /// ```javascript /// const { NapiSolver } = require('@ruvector/solver'); /// /// const solver = new NapiSolver(); /// const result = await solver.solve({ /// values: [4, -1, -1, 4, -1, -1, 4], /// colIndices: [0, 1, 0, 1, 2, 1, 2], /// rowPtrs: [0, 2, 5, 7], /// rows: 3, cols: 3, /// rhs: [1, 0, 1], /// }); /// console.log('Solution:', result.solution); /// console.log('Converged:', result.converged); /// ``` #[napi] pub struct NapiSolver { default_tolerance: f64, default_max_iterations: usize, } #[napi] impl NapiSolver { /// Create a new solver instance with default settings. #[napi(constructor)] pub fn new() -> Self { Self { default_tolerance: 1e-6, default_max_iterations: 1000, } } /// Solve a sparse linear system Ax = b asynchronously. /// /// Runs the computation on a worker thread to avoid blocking the /// Node.js event loop. /// /// # Arguments /// * `config` - Solver configuration including the CSR matrix, RHS vector, /// tolerance, max iterations, and algorithm selection. /// /// # Returns /// A `SolveResult` containing the solution vector, convergence info, /// and timing data. /// /// # Example /// ```javascript /// const result = await solver.solve({ /// values: [2, -1, -1, 2, -1, -1, 2], /// colIndices: [0, 1, 0, 1, 2, 1, 2], /// rowPtrs: [0, 2, 5, 7], /// rows: 3, cols: 3, /// rhs: [1, 0, 1], /// tolerance: 1e-8, /// algorithm: 'jacobi', /// }); /// ``` #[napi] pub async fn solve(&self, config: SolveConfig) -> Result { let tolerance = config.tolerance.unwrap_or(self.default_tolerance); let max_iterations = config .max_iterations .map(|m| m as usize) .unwrap_or(self.default_max_iterations); let algo = parse_algorithm(config.algorithm.as_deref().unwrap_or("jacobi"))?; let algo_name = algo.to_string(); let rows = config.rows as usize; let cols = config.cols as usize; validate_csr_input( &config.values, &config.col_indices, &config.row_ptrs, rows, cols, )?; if config.rhs.len() != rows { return Err(Error::new( Status::InvalidArg, format!( "rhs length {} does not match rows = {}", config.rhs.len(), rows ), )); } let values = config.values; let col_indices: Vec = config.col_indices.iter().map(|&c| c as usize).collect(); let row_ptrs: Vec = config.row_ptrs.iter().map(|&p| p as usize).collect(); let rhs = config.rhs; let result = tokio::task::spawn_blocking(move || { let start = Instant::now(); let (solution, iterations, residual, converged, _history) = dispatch_solver( algo, &row_ptrs, &col_indices, &values, &rhs, rows, tolerance, max_iterations, ); let elapsed_us = start.elapsed().as_micros().min(u32::MAX as u128) as u32; SolveResult { solution, iterations: iterations as u32, residual, converged, algorithm: algo_name, time_us: elapsed_us, } }) .await .map_err(|e| Error::from_reason(format!("Solver task failed: {}", e)))?; Ok(result) } /// Solve a sparse linear system from JSON input. /// /// Accepts a JSON string with the same fields as `SolveConfig` and /// returns a JSON string with the `SolveResult` fields. /// /// # Example /// ```javascript /// const input = JSON.stringify({ /// values: [2, -1, -1, 2], /// col_indices: [0, 1, 0, 1], /// row_ptrs: [0, 2, 4], /// rows: 2, cols: 2, /// rhs: [1, 1], /// }); /// const output = await solver.solveJson(input); /// const result = JSON.parse(output); /// ``` #[napi] pub async fn solve_json(&self, json: String) -> Result { let input: SolveJsonInput = serde_json::from_str(&json) .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid JSON input: {}", e)))?; let config = SolveConfig { values: input.values, col_indices: input.col_indices, row_ptrs: input.row_ptrs, rows: input.rows, cols: input.cols, rhs: input.rhs, tolerance: input.tolerance, max_iterations: input.max_iterations, algorithm: input.algorithm, }; let result = self.solve(config).await?; let output = SolveJsonOutput { solution: result.solution, iterations: result.iterations, residual: result.residual, converged: result.converged, algorithm: result.algorithm, time_us: result.time_us, }; serde_json::to_string(&output).map_err(|e| { Error::new( Status::GenericFailure, format!("Serialization error: {}", e), ) }) } /// Compute PageRank scores for a directed graph asynchronously. /// /// Implements the power iteration method: /// r_{k+1} = d * A^T * D^{-1} * r_k + (1 - d) * p /// where d is the damping factor, D is the out-degree diagonal, and p /// is the personalization vector. /// /// # Example /// ```javascript /// // Simple 3-node graph: 0->1, 1->2, 2->0 /// const result = await solver.pagerank({ /// values: [1, 1, 1], /// colIndices: [1, 2, 0], /// rowPtrs: [0, 1, 2, 3], /// numNodes: 3, /// damping: 0.85, /// }); /// console.log('PageRank:', result.scores); /// ``` #[napi] pub async fn pagerank(&self, config: PageRankConfig) -> Result { let damping = config.damping.unwrap_or(0.85); let tolerance = config.tolerance.unwrap_or(1e-6); let max_iterations = config.max_iterations.map(|m| m as usize).unwrap_or(100); let num_nodes = config.num_nodes as usize; if damping < 0.0 || damping > 1.0 { return Err(Error::new( Status::InvalidArg, format!("Damping factor must be in [0, 1], got {}", damping), )); } validate_csr_input( &config.values, &config.col_indices, &config.row_ptrs, num_nodes, num_nodes, )?; let values: Vec = config.values; let col_indices: Vec = config.col_indices.iter().map(|&c| c as usize).collect(); let row_ptrs: Vec = config.row_ptrs.iter().map(|&p| p as usize).collect(); let personalization = config.personalization; if let Some(ref pv) = personalization { if pv.len() != num_nodes { return Err(Error::new( Status::InvalidArg, format!( "personalization length {} does not match num_nodes = {}", pv.len(), num_nodes ), )); } } let result = tokio::task::spawn_blocking(move || { let start = Instant::now(); let p = personalization.unwrap_or_else(|| vec![1.0 / num_nodes as f64; num_nodes]); // Compute out-degrees for row-stochastic normalization. let mut out_degree = vec![0.0f64; num_nodes]; for i in 0..num_nodes { for idx in row_ptrs[i]..row_ptrs[i + 1] { out_degree[i] += values[idx]; } } let mut rank = vec![1.0 / num_nodes as f64; num_nodes]; let mut new_rank = vec![0.0f64; num_nodes]; let mut converged = false; let mut final_residual = f64::MAX; let mut iters = 0; for iter in 0..max_iterations { for i in 0..num_nodes { new_rank[i] = (1.0 - damping) * p[i]; } let mut dangling_sum = 0.0f64; for i in 0..num_nodes { let s = row_ptrs[i]; let e = row_ptrs[i + 1]; if s == e || out_degree[i].abs() < 1e-15 { dangling_sum += rank[i]; } else { let contribution = rank[i] / out_degree[i]; for idx in s..e { new_rank[col_indices[idx]] += damping * values[idx] * contribution; } } } if dangling_sum > 0.0 { let dangling_contrib = damping * dangling_sum / num_nodes as f64; for i in 0..num_nodes { new_rank[i] += dangling_contrib; } } let mut diff = 0.0f64; for i in 0..num_nodes { diff += (new_rank[i] - rank[i]).abs(); } std::mem::swap(&mut rank, &mut new_rank); final_residual = diff; iters = iter + 1; if diff < tolerance { converged = true; break; } } let elapsed_us = start.elapsed().as_micros().min(u32::MAX as u128) as u32; PageRankResult { scores: rank, iterations: iters as u32, residual: final_residual, converged, time_us: elapsed_us, } }) .await .map_err(|e| Error::from_reason(format!("PageRank task failed: {}", e)))?; Ok(result) } /// Estimate computational complexity for a given problem size. /// /// This is a synchronous method since the estimation is O(1). /// /// # Example /// ```javascript /// const estimate = solver.estimateComplexity({ /// rows: 10000, /// nnz: 50000, /// algorithm: 'jacobi', /// }); /// console.log('Complexity:', estimate.complexityClass); /// console.log('Recommended:', estimate.recommendedAlgorithm); /// ``` #[napi] pub fn estimate_complexity(&self, config: ComplexityConfig) -> Result { let n = config.rows as f64; let nnz = config.nnz as f64; let sparsity = if n * n > 0.0 { nnz / (n * n) } else { 0.0 }; let algo_name = config.algorithm.as_deref().unwrap_or("auto"); let recommended = if n < 100.0 { "gauss-seidel" } else if sparsity < 0.01 && n > 10000.0 { "conjugate-gradient" } else if sparsity < 0.05 { "neumann" } else { "jacobi" }; let (complexity_class, estimated_flops) = match algo_name { "neumann" | "neumann-series" => { let k = 50.0; ("O(k * nnz)".to_string(), k * nnz) } "jacobi" => { let k = n.sqrt().max(10.0); ("O(sqrt(n) * nnz)".to_string(), k * nnz) } "gauss-seidel" | "gs" => { let k = (n.sqrt() / 2.0).max(5.0); ("O(sqrt(n) * nnz)".to_string(), k * nnz) } "conjugate-gradient" | "cg" => { let cond_est = n.sqrt(); ("O(sqrt(kappa) * nnz)".to_string(), cond_est * nnz) } _ => { let k = n.sqrt().max(10.0); ("O(sqrt(n) * nnz)".to_string(), k * nnz) } }; let estimated_time_us = estimated_flops / 1000.0; Ok(ComplexityResult { complexity_class, estimated_flops, recommended_algorithm: recommended.to_string(), estimated_time_us, sparsity, }) } /// Solve with full convergence history returned. /// /// Identical to `solve` but also returns per-iteration residual data /// for convergence analysis and visualization. /// /// # Example /// ```javascript /// const result = await solver.solveWithHistory({ /// values: [4, -1, -1, 4], /// colIndices: [0, 1, 0, 1], /// rowPtrs: [0, 2, 4], /// rows: 2, cols: 2, /// rhs: [1, 1], /// }); /// result.convergenceHistory.forEach(entry => { /// console.log(`Iter ${entry.iteration}: residual = ${entry.residual}`); /// }); /// ``` #[napi] pub async fn solve_with_history(&self, config: SolveConfig) -> Result { let tolerance = config.tolerance.unwrap_or(self.default_tolerance); let max_iterations = config .max_iterations .map(|m| m as usize) .unwrap_or(self.default_max_iterations); let algo = parse_algorithm(config.algorithm.as_deref().unwrap_or("jacobi"))?; let algo_name = algo.to_string(); let rows = config.rows as usize; let cols = config.cols as usize; validate_csr_input( &config.values, &config.col_indices, &config.row_ptrs, rows, cols, )?; if config.rhs.len() != rows { return Err(Error::new( Status::InvalidArg, format!( "rhs length {} does not match rows = {}", config.rhs.len(), rows ), )); } let values = config.values; let col_indices: Vec = config.col_indices.iter().map(|&c| c as usize).collect(); let row_ptrs: Vec = config.row_ptrs.iter().map(|&p| p as usize).collect(); let rhs = config.rhs; let result = tokio::task::spawn_blocking(move || { let start = Instant::now(); let (solution, iterations, residual, converged, history) = dispatch_solver( algo, &row_ptrs, &col_indices, &values, &rhs, rows, tolerance, max_iterations, ); let elapsed_us = start.elapsed().as_micros().min(u32::MAX as u128) as u32; let convergence_history: Vec = history .into_iter() .map(|(iter, res)| ConvergenceEntry { iteration: iter as u32, residual: res, }) .collect(); SolveWithHistoryResult { solution, iterations: iterations as u32, residual, converged, algorithm: algo_name, time_us: elapsed_us, convergence_history, } }) .await .map_err(|e| Error::from_reason(format!("Solver task failed: {}", e)))?; Ok(result) } } // --------------------------------------------------------------------------- // Serde types for JSON solve interface // --------------------------------------------------------------------------- #[derive(serde::Deserialize)] struct SolveJsonInput { values: Vec, col_indices: Vec, row_ptrs: Vec, rows: u32, cols: u32, rhs: Vec, tolerance: Option, max_iterations: Option, algorithm: Option, } #[derive(serde::Serialize)] struct SolveJsonOutput { solution: Vec, iterations: u32, residual: f64, converged: bool, algorithm: String, time_us: u32, } // --------------------------------------------------------------------------- // Standalone functions // --------------------------------------------------------------------------- /// Get the library version. #[napi] pub fn version() -> String { env!("CARGO_PKG_VERSION").to_string() } /// Get library information. #[napi] pub fn info() -> LibraryInfo { LibraryInfo { name: "ruvector-solver-node".to_string(), version: env!("CARGO_PKG_VERSION").to_string(), description: "Sublinear-time sparse linear solver for Node.js".to_string(), algorithms: vec![ "neumann".to_string(), "jacobi".to_string(), "gauss-seidel".to_string(), "conjugate-gradient".to_string(), ], features: vec![ "async-solve".to_string(), "json-interface".to_string(), "pagerank".to_string(), "complexity-estimation".to_string(), "convergence-history".to_string(), ], } } /// Library information. #[napi(object)] pub struct LibraryInfo { pub name: String, pub version: String, pub description: String, pub algorithms: Vec, pub features: Vec, } /// List available solver algorithms. #[napi] pub fn available_algorithms() -> Vec { vec![ "neumann".to_string(), "jacobi".to_string(), "gauss-seidel".to_string(), "conjugate-gradient".to_string(), ] }