Skip to content

Add selinv_extract: read the selected inverse at a sparse pattern - #12

Merged
timweiland merged 2 commits into
mainfrom
feat/selinv-extract
Jun 11, 2026
Merged

timweiland merged 2 commits into
mainfrom
feat/selinv-extract

Conversation

@timweiland

Copy link
Copy Markdown
Owner

Summary

Adds selinv_extract / selinv_extract! / selinv_extract_setup — a primitive that reads the selected-inverse values at a given sparse pattern straight from the supernodal blocks, without materializing the full sparse(selinv(F).Z).

It is the read analogue of the existing dot(S::SupernodalMatrix, B::SparseMatrixCSC): the same block traversal, but instead of accumulating the scalar tr(ΣB), it stores Σ's values at B's nonzero positions.

Motivation (GaussianMarkovRandomFields.jl #168): the workspace selinv path materializes the entire sparse(selinv.Z) on every refactorization, but the real consumer — diag(A Σ Aᵀ) for predictor marginals — only needs Σ at the observation-local pattern (≈ pattern(AᵀA)), a tiny subset of the factor fill. Streaming the extract (and reusing a precomputed plan) avoids that materialization and restores parallel scaling.

API

selinv_extract(S, B)                      # allocating: SparseMatrixCSC with B's pattern
selinv_extract!(dest, S, B)               # in-place into a dest carrying B's pattern
plan = selinv_extract_setup(S, B)         # precompute reuse plan (depermute baked in)
selinv_extract!(dest, S, plan)            # O(nnz), zero allocation, reusable

Semantics. Z = selinv_extract(S, B) has exactly B's sparsity pattern, and Z[i,j] == sparse(S)[i,j] for every (i,j) ∈ pattern(B) — including positions outside the stored selinv pattern, which are kept as 0.0. A generic AbstractMatrix fallback handles the simplicial case (where selinv(F).Z is a Symmetric/SparseMatrixCSC), so callers never branch on factor type.

The supernodal structure is invariant across refactorizations of the same symbolic factor (only S.vals changes), so selinv_extract_setup builds a plan once and selinv_extract!(dest, S, plan) reuses it with zero allocation.

Benchmark

benchmark/bench_extract.jl, 2D Laplacian factor n = 14400, observation-local pattern (B/fill ≈ 0.18), depermuted supernodal .Z:

method min time allocated
1. sparse(S) + mask 22.9 ms 97 MiB
2. per-entry getindex over S 14.8 ms 30 MiB
3. selinv_extract (allocating) 9.8 ms 12 MiB
4. selinv_extract! + plan 0.27 ms 0 B

The plan-based fill is ~85× faster than materialize-and-mask with zero allocation; even the one-shot allocating selinv_extract is ~2.3× faster while bit-identical.

Tests

test/test_extract.jl (wired into runtests.jl) covers:

  • supernodal and simplicial factors,
  • depermute = true / false,
  • random sprand patterns and obs-style AᵀA patterns,
  • exact pattern preservation including off-pattern (zero) entries,
  • the allocation-free reuse path (@allocated == 0).

Full suite (including Aqua) passes locally.

Notes

  • Project.toml version bumped 0.2.0 → 0.2.1 so GaussianMarkovRandomFields can depend on the new release.

🤖 Generated with Claude Code

timweiland and others added 2 commits June 11, 2026 19:22
Adds `selinv_extract` / `selinv_extract!` / `selinv_extract_setup`, the read
analogue of `dot(S, B)`: the same supernodal block traversal, but storing Σ's
values at B's nonzero positions instead of accumulating `tr(ΣB)`. This lets a
consumer read Σ at a small pattern (e.g. `pattern(AᵀA)`) without materializing
`sparse(selinv(F).Z)`.

- `selinv_extract(S, B)`: allocating; returns a SparseMatrixCSC with exactly B's
  pattern (off-pattern positions kept as 0.0), bit-identical to `sparse(S)`
  masked to B's pattern.
- `selinv_extract_setup(S, B)` + `selinv_extract!(dest, S, plan)`: precompute a
  reuse plan once (the supernodal structure is invariant across
  refactorizations), then fill in O(nnz) with zero allocation.
- Generic AbstractMatrix fallback for the simplicial case, where `selinv(F).Z`
  is a `Symmetric`/`SparseMatrixCSC` rather than a `SupernodalMatrix`, so callers
  need not branch on factor type.

Benchmark (benchmark/bench_extract.jl; 2D Laplacian factor, n=14400, obs-local
pattern B/fill ≈ 0.18): materialize+mask 22.9 ms, getindex 14.8 ms,
selinv_extract 9.8 ms, extract!+plan 0.27 ms / 0 alloc (≈85x vs materialize).

Tests in test/test_extract.jl cover supernodal + simplicial factors, both
depermute modes, random and AᵀA patterns, off-pattern zeros, and the
allocation-free reuse path. Bump version to 0.2.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the selected-inverse extraction functions to the SupernodalMatrix API
page so Documenter's checkdocs=:exports passes (selinv_extract,
selinv_extract!, selinv_extract_setup were exported but undocumented).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@timweiland
timweiland merged commit b725a20 into main Jun 11, 2026
3 checks passed
@timweiland
timweiland deleted the feat/selinv-extract branch June 11, 2026 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant