Skip to content

Add public same_sparsity_structure(A, B)#496

Merged
ChrisRackauckas merged 4 commits into
JuliaArrays:masterfrom
ChrisRackauckas-Claude:feature/same-sparsity-structure
Jul 23, 2026
Merged

Add public same_sparsity_structure(A, B)#496
ChrisRackauckas merged 4 commits into
JuliaArrays:masterfrom
ChrisRackauckas-Claude:feature/same-sparsity-structure

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Jul 23, 2026

Copy link
Copy Markdown

Draft — please ignore until reviewed by @ChrisRackauckas.

What

Adds a public, documented same_sparsity_structure(A, B) -> Bool: an allocation-free query that returns true when A and B store 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:

ArrayInterface already owns the generic sparse-structure interface (has_sparsestruct, findstructralnz), so the matrix-vs-matrix structural-equality query belongs here. Unlike findstructralnz (which allocates (rowind, colind) via findnz), this allocates nothing on its fast paths, so it's usable in hot loops.

Semantics

  • Dense arrays store every position → same structure iff same shape.
  • Structured / sparse matrices compare by pattern: shape (plus uplo/format) for Diagonal/Bidiagonal/Tridiagonal/SymTridiagonal; stored index arrays (colptr/rowval and GPU equivalents) for CSC/CSR. These dispatch on the base type, so differing type parameters (e.g. Tridiagonal{Float64} vs Tridiagonal{Int}) still compare as the same structure.
  • Different base types (a structured matrix vs a dense one, or two different structured types) → false: a layout isn't reused across categories.
  • Same base type, no specialized methodMethodError, not false. We don't fabricate an answer for a type we haven't been taught about (e.g. two UpperTriangular, which do share a structure); the type should define its own method. has_sparsestruct can gate applicability.

This avoids the two failure modes of a naive f(A,B) = false catch-all: reporting identical dense matrices as differing, and silently guessing false for a same-type pair whose structure it can't actually determine.

Methods

  • AbstractArray fallback → different base type false, same base type MethodError; DenseArray → shape
  • Diagonal / Bidiagonal (with uplo) / Tridiagonal / SymTridiagonal — by shape (core)
  • AbstractSparseMatrixCSC — via getcolptr/rowvals (SparseArrays ext)
  • CuSparseMatrixCSC / CuSparseMatrixCSR — via their device index arrays (CUDA ext)

Validation

  • public declaration + docstring + docs/src/sparsearrays.md entry (public and documented together)
  • Tests (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/size
  • Full GROUP=Core suite passes locally (214/214); fast paths confirmed allocation-free (@allocated == 0 for CSC / Diagonal / dense in a function barrier)
  • Minor version bump 7.27.0 → 7.28.0 (additive public API)
  • GPU (CUDA CSC/CSR) methods are correct-by-construction against the CUSPARSE field/interface but not locally tested (no GPU here) — rely on the repo's GPU CI

Note: AbstractGPUSparseMatrixCSC does not subtype SparseArrays.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 shared getcolptr/rowvals interface; I kept this to the existing per-backend pattern to avoid a new weakdep.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LWooL8qipuUf7nTrDUUzqY

ChrisRackauckas and others added 3 commits July 23, 2026 03:54
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

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.33333% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.56%. Comparing base (3b48690) to head (c488273).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
ext/ArrayInterfaceCUDAExt.jl 0.00% 4 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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
ChrisRackauckas marked this pull request as ready for review July 23, 2026 10:06
@ChrisRackauckas
ChrisRackauckas merged commit 80e3539 into JuliaArrays:master Jul 23, 2026
20 of 21 checks passed
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.

2 participants