Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sparse Operations

Sparse matrices support scaling, matrix-vector and matrix-matrix products against a dense buffer, sparse-sparse addition, sparse-sparse multiplication, and pruning near-zero entries. Each operation is a free function taking the sparse matrix by value or reference, rather than a method — and each is provided in both a CSR and a CSC variant.

Scaling every stored value by a constant:

#![allow(unused)]
fn main() {
use rustebra::sparse::{CscMatrix, CsrMatrix, scale_csc, scale_csr};

pub(crate) fn run() {
    println!("\n== scale ==");

    let csr =
        CsrMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![4.0_f64, 6.0]).expect("valid CSR");
    let scaled = scale_csr(csr, 0.5);
    println!("scale_csr by 0.5: values = {:?}", scaled.values());

    let csc =
        CscMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![4.0_f64, 6.0]).expect("valid CSC");
    let scaled_csc = scale_csc(csc, 0.5);
    println!("scale_csc by 0.5: values = {:?}", scaled_csc.values());
}
}

Multiplying a sparse matrix by a dense vector:

#![allow(unused)]
fn main() {
use rustebra::sparse::{CscMatrix, CsrMatrix, matvec_csc, matvec_csr};

pub(crate) fn run() {
    println!("\n== matvec ==");

    // [2  0]   [1]   [ 2]
    // [0  5] × [3] = [15]
    let m_csr =
        CsrMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![2.0_f64, 5.0]).expect("valid CSR");
    let y = matvec_csr(&m_csr, &[1.0, 3.0]).expect("dimensions match");
    println!("matvec_csr: [2 0; 0 5] × [1, 3] = {y:?}");

    let m_csc =
        CscMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![2.0_f64, 5.0]).expect("valid CSC");
    let y = matvec_csc(&m_csc, &[1.0, 3.0]).expect("dimensions match");
    println!("matvec_csc: [2 0; 0 5] × [1, 3] = {y:?}");
}
}

Multiplying a sparse matrix by a dense matrix:

#![allow(unused)]
fn main() {
use rustebra::sparse::{CscMatrix, CsrMatrix, matmat_csc, matmat_csr};

pub(crate) fn run() {
    println!("\n== matmat ==");

    // [2  0]   [1  0]   [2  0]
    // [0  3] × [0  4] = [0 12]   (x is row-major)
    let x = [1.0_f64, 0.0, 0.0, 4.0];

    let m_csr =
        CsrMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![2.0_f64, 3.0]).expect("valid CSR");
    let y = matmat_csr(&m_csr, &x, 2).expect("dimensions match");
    println!("matmat_csr: [2 0; 0 3] × [1 0; 0 4] = {y:?}");

    let m_csc =
        CscMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![2.0_f64, 3.0]).expect("valid CSC");
    let y = matmat_csc(&m_csc, &x, 2).expect("dimensions match");
    println!("matmat_csc: [2 0; 0 3] × [1 0; 0 4] = {y:?}");
}
}

Adding two sparse matrices of the same shape:

#![allow(unused)]
fn main() {
use rustebra::sparse::{CscMatrix, CsrMatrix, add_csc, add_csr};

pub(crate) fn run() {
    println!("\n== add ==");

    // [2  0]   [1  3]   [3  3]
    // [0  5] + [0  4] = [0  9]
    let a = CsrMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![2.0_f64, 5.0]).expect("valid CSR");
    let b = CsrMatrix::new(2, 2, vec![0, 2, 3], vec![0, 1, 1], vec![1.0_f64, 3.0, 4.0])
        .expect("valid CSR");
    let c = add_csr(&a, &b).expect("shapes match");
    println!(
        "add_csr: col_indices={:?}  values={:?}",
        c.col_indices(),
        c.values()
    );

    let a = CscMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![2.0_f64, 5.0]).expect("valid CSC");
    let b = CscMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0_f64, 4.0]).expect("valid CSC");
    let c = add_csc(&a, &b).expect("shapes match");
    println!(
        "add_csc: row_indices={:?}  values={:?}",
        c.row_indices(),
        c.values()
    );
}
}

Sparse-sparse matrix multiplication (spmm_csr), which multiplies two CsrMatrix operands and returns a sorted result:

#![allow(unused)]
fn main() {
use rustebra::sparse::{CsrMatrix, spmm_csr};

pub(crate) fn run() {
    println!("\n== spmm ==");

    // [1  0]   [1  2]   [1  2]
    // [0  2] × [3  4] = [6  8]
    let a = CsrMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0_f64, 2.0]).expect("valid CSR");
    let b = CsrMatrix::new(
        2,
        2,
        vec![0, 2, 4],
        vec![0, 1, 0, 1],
        vec![1.0_f64, 2.0, 3.0, 4.0],
    )
    .expect("valid CSR");
    let c = spmm_csr(&a, &b).expect("inner dimensions match");
    println!("spmm_csr: [1 0; 0 2] × [1 2; 3 4]:");
    println!(
        "  row_ptr={:?}  col_indices={:?}  values={:?}",
        c.row_ptr(),
        c.col_indices(),
        c.values()
    );
}
}

Pruning entries within a tolerance of zero:

#![allow(unused)]
fn main() {
use rustebra::sparse::{CsrMatrix, prune_csr};

pub(crate) fn run() {
    println!("\n== prune ==");

    // Entries within [-tolerance, tolerance] are dropped.
    let m = CsrMatrix::new(
        1,
        3,
        vec![0, 3],
        vec![0, 1, 2],
        vec![1e-15_f64, 2.0, -1e-15],
    )
    .expect("valid CSR");
    let pruned = prune_csr(m, 1e-10).expect("dimensions fit within limits");
    println!("prune_csr (tolerance=1e-10, two near-zeros removed):");
    println!("  nnz={}", pruned.nnz());
    println!(
        "  col_indices={:?}  values={:?}",
        pruned.col_indices(),
        pruned.values()
    );
}
}

Sparse matrices also implement SparseLinearOp, an abstraction for “apply this matrix to a dense vector, writing into a caller-supplied buffer” — the interface Krylov solvers are written against so they work with any sparse format:

#![allow(unused)]
fn main() {
use rustebra::sparse::{CsrMatrix, SparseLinearOp};

let eye = CsrMatrix::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0_f64, 1.0]).unwrap();
let mut y = [0.0; 2];
eye.apply(&[3.0, 5.0], &mut y).unwrap();
assert_eq!(y, [3.0, 5.0]);
}

Gotchas

  • Every operation here returns Result (e.g. DimensionMismatch for shape-incompatible operands) rather than panicking — sparse matrices carry their shape as runtime fields, so there’s no type-level guarantee two operands are compatible the way there is for StaticMatrix.
  • apply (from SparseLinearOp) writes into a caller-supplied output buffer instead of allocating a new one, so solver loops built on top of it can reuse the same buffer across iterations without allocating per call.
  • validate_csr/validate_csc (in rustebra::sparse) check the canonical format invariants — including that no stored value is an explicit zero — separately from construction. CsrMatrix::new/CscMatrix::new accept explicit zeros; only validate_csr/validate_csc flag them.