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.DimensionMismatchfor 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 forStaticMatrix. apply(fromSparseLinearOp) 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(inrustebra::sparse) check the canonical format invariants — including that no stored value is an explicit zero — separately from construction.CsrMatrix::new/CscMatrix::newaccept explicit zeros; onlyvalidate_csr/validate_cscflag them.