Getting started

Add feral to your Cargo.toml:

[dependencies]
feral = "0.10"

feral is primarily a sparse symmetric indefinite solver. The ergonomic entry point is the Solver type: build a matrix, factor it once, then solve it against one or many right-hand sides.

Sparse quickstart

use feral::Solver;
use feral::numeric::solver::FactorStatus;
use feral::sparse::csc::CscMatrix;

// Build a symmetric matrix from triplets of its LOWER triangle
// (row, col, value), `n` rows/cols. Duplicate entries are summed.
let n = 5;
let rows = [0, 1, 2, 3, 4, 1, 2, 3, 4];
let cols = [0, 0, 0, 0, 0, 1, 2, 3, 4];
let vals = [10.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let a = CscMatrix::from_triplets(n, &rows, &cols, &vals)?;

// Factor once. Pass `None` to skip the inertia check, or
// `Some(expected)` to verify the matrix has the inertia you expect.
let mut solver = Solver::new();
let status = solver.factor(&a, None);
assert_eq!(status, FactorStatus::Success);

// The factorization carries a certified inertia (n_pos, n_neg, n_zero).
println!("inertia: {:?}", solver.inertia());

// Solve A·x = b for a single right-hand side.
let b = [1.0, 2.0, 3.0, 4.0, 5.0];
let x = solver.solve(&b)?;

solver.factor returns a FactorStatus; match on it to distinguish Success, Singular, WrongInertia, and FatalError. Once factored, the Solver keeps the factor, so any number of solves reuse it.

Many right-hand sides (batched solve)

The same factorization can be solved against many right-hand sides at once. solve_many shares the supernodal traversal across columns, so it is substantially cheaper than looping a single-RHS solve — and for wide nrhs it runs each supernode's dense panel as register-blocked TRSM + GEMM kernels.

B and X are column-major n × nrhs matrices, stored as flat slices of length n * nrhs (column c occupies [c*n .. (c+1)*n]):

let nrhs = 64;
// b_many[c*n + i] is row i of right-hand-side column c.
let b_many: Vec<f64> = make_columns(n, nrhs);
let x_many = solver.solve_many(&b_many, nrhs)?;   // length n * nrhs

This is the path behind batched KKT back-solves, jax.jacrev over a solve, sensitivity analysis, and parameter sweeps. On 2-D Laplacians it is roughly 3–6× faster per RHS than looping single-RHS solves (the exact factor depends on size, CPU SIMD width, and cache). See GitHub issue #57 and the 02_multi_rhs_batched notebook.

Iterative refinement

With ZeroPivotAction::ForceAccept (the default), an unrefined solve can leave a residual on near-singular pivots. solve_refined runs a few steps of iterative refinement against the original matrix and returns the best iterate:

let x = solver.solve_refined(&a, &b)?;                       // single RHS
let x_many = solver.solve_many_refined(&a, &b_many, nrhs)?;  // batched

solve_many_refined keeps per-column best-iterate convergence but, for wide right-hand sides, refines through the same batched panel kernel as solve_many — one batched solve per refinement step over the still- unconverged columns — so the refined path amortizes too (issue #58).

Dense path

For small dense systems there is a direct API that mirrors the sparse one. factor returns the factors and the certified inertia:

use feral::{factor, solve, BunchKaufmanParams, SymmetricMatrix};

// Lower-triangle entries (row >= col) of an n×n symmetric matrix.
let a = SymmetricMatrix::from_lower_triangle(n, &entries);
let (factors, inertia) = factor(&a, &BunchKaufmanParams::default())?;
let x = solve(&factors, &b)?;

More

  • Runnable Rust programs: examples/ exercises the dense and sparse paths, scaling, and refinement.
  • Python bindings and notebooks: see Python bindings.
  • Inertia guarantees on singular matrices: see Inertia semantics.