Add public same_sparsity_structure(A, B)#496
Merged
ChrisRackauckas merged 4 commits intoJul 23, 2026
Merged
Conversation
A conservative, allocation-free query returning `true` only when `A` and `B` are known to share an identical sparsity pattern (and shape) -- the condition under which a preallocated structure (or a cached symbolic factorization) may be reused for values-only updates without a structural rebuild. Motivation: this check is currently reinvented in several places (e.g. LinearSolve's internal `pattern_changed`, and a local helper in OrdinaryDiffEq's calc_J!). ArrayInterface already owns the generic sparse- structure interface (`has_sparsestruct`, `findstructralnz`); this adds the matrix-vs-matrix structural-equality query alongside them. Unlike `findstructralnz`, it allocates nothing on its fast paths, so it is usable in hot loops (e.g. deciding per Jacobian evaluation whether a sparse jac_prototype needs rebuilding). Methods: - generic fallback -> false (conservative: false means "not cheaply provable", not "provably different") - Diagonal / Bidiagonal (with uplo) / Tridiagonal / SymTridiagonal by shape - AbstractSparseMatrixCSC via colptr/rowval comparison (SparseArrays ext) - CuSparseMatrixCSC / CuSparseMatrixCSR via their index arrays (CUDA ext) Declared `public`, documented in docs/src/sparsearrays.md, and tested. Minor version bump for the additive public API. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWooL8qipuUf7nTrDUUzqY
Remove the generic `same_sparsity_structure(A, B) = false` catch-all. Unlike `has_sparsestruct(::Type) = false` (where `false` is correct — "no special structure"), a blanket `false` here is semantically wrong: it would claim two identical dense matrices have different structure. An unsupported argument pair now throws `MethodError` instead of silently returning a possibly-wrong default, so callers get a positive true/false from a real method or a clear signal that the type needs one. `has_sparsestruct` can gate applicability. Tests updated: the dense and mixed-type cases now assert `@test_throws MethodError`. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWooL8qipuUf7nTrDUUzqY
Replace the removed catch-all with category-aware methods so every AbstractArray pair gets a real answer instead of a MethodError: - dense (`DenseArray`) pair: identical shape ⇒ identical (full) structure - different structural categories (structured-vs-dense, or two different structured types): `false` -- a layout is not reused across categories, so this is treated as "not the same structure" rather than compared position-by-position The dense method is what makes this correct where the original blanket `false` was wrong (it had reported identical dense matrices as differing). Fast paths remain allocation-free (`@allocated == 0` for CSC/Diagonal/dense in a function barrier). Tests updated accordingly. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWooL8qipuUf7nTrDUUzqY
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #496 +/- ##
==========================================
+ Coverage 59.89% 60.56% +0.66%
==========================================
Files 15 15
Lines 591 606 +15
==========================================
+ Hits 354 367 +13
- Misses 237 239 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The `AbstractArray` fallback returned `false` for every unhandled pair, which
fabricated an answer for two arrays of the *same* base type that simply lack a
specialized method (e.g. two `UpperTriangular`, which do share a structure). Split
the fallback:
- different base type (parameterless_type differs) -> false (different category)
- same base type, no specialized method -> throw MethodError (unknown; define one)
Also make the Tridiagonal/SymTridiagonal methods dispatch on the base type
instead of `A::T, B::T`, so differing type parameters (e.g. Tridiagonal{Float64}
vs Tridiagonal{Int}) still compare as the same structure rather than falling
through to the fallback.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWooL8qipuUf7nTrDUUzqY
ChrisRackauckas
marked this pull request as ready for review
July 23, 2026 10:06
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a public, documented
same_sparsity_structure(A, B) -> Bool: an allocation-free query that returnstruewhenAandBstore their nonzeros in exactly the same structural positions (same pattern and shape). That's the condition under which a preallocated sparse structure — or a cached symbolic factorization — can be reused for a values-only update without rebuilding the structure.Why
This check is currently reinvented in several places:
pattern_changed(fact, A)(colptr/rowval comparison, run on every sparse solve, with GPU methods) — but it's not public and is shaped as factorization-vs-matrix.calc_J!(Skip redundant sparse-Jacobian structure rebuild in calc_J! SciML/OrdinaryDiffEq.jl#3980) guarding a per-Jacobian-eval sparse-structure rebuild.ArrayInterface already owns the generic sparse-structure interface (
has_sparsestruct,findstructralnz), so the matrix-vs-matrix structural-equality query belongs here. Unlikefindstructralnz(which allocates(rowind, colind)viafindnz), this allocates nothing on its fast paths, so it's usable in hot loops.Semantics
uplo/format) forDiagonal/Bidiagonal/Tridiagonal/SymTridiagonal; stored index arrays (colptr/rowvaland GPU equivalents) for CSC/CSR. These dispatch on the base type, so differing type parameters (e.g.Tridiagonal{Float64}vsTridiagonal{Int}) still compare as the same structure.false: a layout isn't reused across categories.MethodError, notfalse. We don't fabricate an answer for a type we haven't been taught about (e.g. twoUpperTriangular, which do share a structure); the type should define its own method.has_sparsestructcan gate applicability.This avoids the two failure modes of a naive
f(A,B) = falsecatch-all: reporting identical dense matrices as differing, and silently guessingfalsefor a same-type pair whose structure it can't actually determine.Methods
AbstractArrayfallback → different base typefalse, same base typeMethodError;DenseArray→ shapeDiagonal/Bidiagonal(withuplo) /Tridiagonal/SymTridiagonal— by shape (core)AbstractSparseMatrixCSC— viagetcolptr/rowvals(SparseArrays ext)CuSparseMatrixCSC/CuSparseMatrixCSR— via their device index arrays (CUDA ext)Validation
publicdeclaration + docstring +docs/src/sparsearrays.mdentry (public and documented together)test/core.jl): dense same/diff-shape/vector, structured same + cross-eltype, different-base-type → false, same-base-no-method →@test_throws MethodError, CSC same/different/similar/sizeGROUP=Coresuite passes locally (214/214); fast paths confirmed allocation-free (@allocated == 0for CSC / Diagonal / dense in a function barrier)Note:
AbstractGPUSparseMatrixCSCdoes not subtypeSparseArrays.AbstractSparseMatrixCSC, so GPU CSC needs the explicit CUDA-ext method rather than being covered by the SparseArrays one. A future GPUArrays-level method could cover CUDA/AMDGPU/Metal generically via the sharedgetcolptr/rowvalsinterface; I kept this to the existing per-backend pattern to avoid a new weakdep.🤖 Generated with Claude Code
https://claude.ai/code/session_01LWooL8qipuUf7nTrDUUzqY