Repository navigation
Conversation
This is a trivial Documentation issue.
The position of docstring was mistaken, it leads to docstring unvisible.
`copy(F::UmfpackLU, [ws::UmfpackWS])` has a docstring in the source, but
it is not attached to
anything: a comment sits between it and the definition.
```julia
julia> using SparseArrays
julia> b = Base.Docs.Binding(Base, :copy);
julia> filter(s -> occursin("Umfpack", string(s)), collect(keys(Base.Docs.meta(Base)[b].docs)))
Type[]
julia> hasmethod(Base.copy, Tuple{SparseArrays.UMFPACK.UmfpackLU})
true
```
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f423f93)
Fixes #758. `getindex(::Sparse, i, j)` binary-searched the row indices of a column unconditionally, but CHOLMOD only guarantees that a column's row indices are sorted when the struct's `sorted` flag is set. `S'S` returns an unsorted result, so lookups silently missed stored entries — and since `Array` and `show` go through `getindex`, those conversions were wrong too: ```julia julia> A = sparse([2, 1, 2], [1, 2, 2], Float64[1, 2, 3]); julia> P = CHOLMOD.Sparse(A)' * CHOLMOD.Sparse(A); # unsorted julia> P[1, 2] # before: 0.0, after: 3.0 3.0 julia> Array(P) # before: [1.0 0.0; 3.0 0.0] 2×2 Matrix{Float64}: 1.0 3.0 3.0 13.0 ``` Changes: * Use `searchsortedfirst` only when `sorted` is set; fall back to a linear scan over the column otherwise. * While computing the range of entries belonging to a column, also handle unpacked matrices (`packed == 0`), where a column ends after `nz[j]` entries rather than at `p[j + 1]`. Previously those reads ran past the end of the column into uninitialized storage. Tests cover the matrix from the issue plus an explicitly unsorted *and* unpacked matrix built with `allocate_sparse`, over both index types. Note that `Sparse` is not a public API, so this is not user-facing today; it matters if/when we start relying on this indexing path internally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014KpwdQ6bpWQKzjGV5prtAR Co-authored-by: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 38288ed)
`isone(A::AbstractSparseMatrixCSC)` only validated the stored entries it
encountered and used `nnz(A) >= n` as a proxy for "every column has a
diagonal entry". A matrix with a stored zero and a missing diagonal
entry therefore passed:
```julia
julia> M = sparse([1 0; 1 1]) * sparse([1 0; -1 0])
2×2 SparseMatrixCSC{Int64, Int64} with 2 stored entries:
1 ⋅
0 ⋅
julia> isone(M)
true
```
This PR tracks, per column, whether a stored diagonal entry equal to one
was seen, and returns `false` otherwise. Added regression tests covering
the issue's example, stored zeros on and off the diagonal, and a missing
diagonal entry.
Fixes #763
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01XzN6CtuVBJWDVc88ShNYso
Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 4ce0756)
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## release-1.13 #1089 +/- ##
================================================
+ Coverage 84.47% 84.98% +0.51%
================================================
Files 13 13
Lines 9404 9484 +80
================================================
+ Hits 7944 8060 +116
+ Misses 1460 1424 -36 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
```julia
julia> spdiagm(3, 3)
ERROR: UndefVarError: `T` not defined in static parameter matching
julia> diagm(3, 3)
3×3 Matrix{Bool}
```
Since #751 the zero-pair call lands on
`spdiagm_eltype(kv::Pair{<:Integer,<:AbstractVector{T}}...) where T`,
whose `T` is unbound with no arguments. Aqua's `unbound_args` check
flagged exactly this, but it was marked `broken=true`. `_spdiagm` now
has a no-pair method returning `spzeros(Bool, m, n)` (0×0 for
`spdiagm()`), matching `diagm`. The empty case is handled there rather
than in `spdiagm_eltype` so the `Vararg` inference guarantee from #751
still holds. The variadic `spdiagm_eltype` method and `_absspvec_vcat`
(the only other hit, not reachable with zero arguments) now take a
leading argument, so the Aqua check is un-brokened and guards this from
here on.
Tested `linalg`, `sparsevector` and the Aqua/ambiguity job on nightly
(1.14.0-DEV). Also fails on 1.13.0, so this is a backport candidate.
Written with Claude Code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit da1d02e)
Backport adjustment: test/ambiguous.jl keeps `unbound_args=(; broken=true)`, since release-1.13 has further unbound type parameters (`_absspvec_hcat`, `_getmode`) that main fixed in PRs not backported here.
…#884) Fixes #811. Assigning through a repeated index corrupted a `SparseMatrixCSC`: a repeated row stored the same row twice in a column, and a repeated column dropped values. Dense arrays let the last write win. ```julia julia> A = spzeros(2, 2); A[[1, 1], 1] = [5.0, 6.0]; A 2×2 SparseMatrixCSC{Float64, Int64} with 2 stored entries: # now 1 stored entry, 6.0 5.0 ⋅ ⋅ ⋅ julia> sum(A), sum(Matrix(A)) (11.0, 6.0) # now (6.0, 6.0) ``` `setindex!(A, V, I, J)` sorts `I` and `J` and merges `V` into each column, assuming the sorted indices are strictly increasing. A new helper, `_setindex_lastwrites`, returns the stable sort permutation with all but the last position of each repeated index removed, so `V[pI, pJ]` holds exactly the writes that survive; it returns `nothing` when the index is already strictly increasing, which leaves the common path with one extra `issorted` pass. The linear-index method `setindex!(A, x, I)` already tried to skip repeats but compared `I[S[k]]` with its neighbour in the original order rather than the sorted one, so `A[[1, 2, 1]] = x` also stored a duplicate; it now compares with `I[S[k+1]]`. The scalar-fill, logical-mask and `SparseVector` methods were already correct. Found by sweeping 3000 random `A[I, J] = V`, `A[I, j] = v`, `A[L] = x`, `A[I, J] .= c` and `v[L] = x` assignments with repeated, sorted and unsorted indices against `Matrix`/`Vector`: about two thirds of the matrix cases and a quarter of the linear ones disagreed before, none after. The new tests in `test/indexing.jl` fail on `main`. Tested with the `indexing`, `sparsematrix`, `fixed`, `issues` and `higherorderfns` suites on nightly (1.14). No dispatch or public API changes. A bug fix, so a candidate for `backport 1.x`. Written by Claude Code (Claude Fable 5.1). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit ed2e0c0) Backport adjustment: src/indexing.jl does not exist on release-1.13; the hunks were applied to src/sparsematrix.jl and the tests added to test/sparsematrix_constructors_indexing.jl.
…ex!` (#891) Fixes #389. Scalar `setindex!` decided whether to insert with `v !== zero(eltype(A))`. A heap-allocated zero such as `big(0)` is never egal to a fresh one, so it was stored, and `S .= M` with a `Matrix{BigFloat}` source densified `S` because the generic broadcast assigns every element: ```julia julia> A = spzeros(BigInt, 3, 3); A[1] = 0; nnz(A) 1 julia> S = spzeros(BigFloat, 4, 4); S .= Matrix(Diagonal(rand(BigFloat, 4))); nnz(S) 16 ``` Insertion now goes through a `_isimplicitzero(v, Tv)` helper: egality for isbits values, which keeps `-0.0`, `missing` and dual numbers with nonzero partials exactly as #296 intended, and a value comparison for non-isbits values, keeping the sign of a `BigFloat` zero. The value comparison does not build `zero(Tv)`, so `BigFloat` inserts no longer allocate a zero per call: filling a 300×300 sparse matrix from a mostly-zero `Matrix{BigFloat}` drops from 4.6 ms / 90k allocs to 0.45 ms / 17 allocs, and `Float64`, `Int`, `ComplexF64` and `Union{Missing,Float64}` are unchanged. Array elements are always stored, as before. The matrix and vector scalar `setindex!` and the dense source of `copyto!` into a sparse view share the helper. Overwriting an existing stored entry with zero keeps it stored, as before. Tested with the full suite and the whitespace check on Julia nightly (1.14.0-DEV). Candidate for `backport 1.x`: it restores behaviour lost in #296 without new API. Written with Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com> (cherry picked from commit ba46cbf) Backport adjustment: applied to src/sparsematrix.jl (for src/indexing.jl) and the tests to test/sparsematrix_constructors_indexing.jl and test/sparsematrix_ops.jl. The `_splice_source` hunk and its `copyto!` test are dropped: that code comes from #799, which is not on release-1.13.
`repeat(A, m, n)` fails for any sparse matrix whose index type is not
`Int`, while `repeat(A, m)` works:
```julia
julia> A = SparseMatrixCSC{Float64,Int32}(sprand(4, 3, 0.5));
julia> repeat(A, 2, 3)
ERROR: MethodError: no method matching SparseMatrixCSC{Float64, Int32}(::Int64, ::Int64, ::Vector{Int64}, ::Vector{Int32}, ::Vector{Float64})
```
The two-count method built its column pointers with `cumsum(vcat(1,
repeat(diff(colptr), n)))`, which is a `Vector{Int}` regardless of `Ti`,
while `rowval` kept `Ti`, so the constructor had no matching method. The
fix builds `colptr` with `similar` on the column pointers of the
row-repeated matrix and fills it by offsetting each block of pointers by
the block's `nnz`, so the result is `SparseMatrixCSC{Tv,Ti}` like the
single-count form and like `hcat`/`vcat` in the same file, which also
keep `Ti` without widening. This also drops the three temporary vectors
the old expression allocated.
Tested with `Pkg.test(test_args=["sparsematrix", "concatenation"])` on
Julia 1.14.0-DEV.3359 (nightly); the new test compares `repeat(A32, m,
n)` against the dense result for `m, n` in `0:2` × `0:3` and asserts the
type; whitespace check clean. No dispatch change, no docs change.
Backport candidate (`backport 1.13`): pure bug fix, no new methods.
Written by Claude Code (Fable 5.1) from a type-stability review of
`main`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit ca9a6bc)
Backport adjustment: applied to src/sparsematrix.jl (for src/concatenation.jl) and test/sparsematrix_constructors_indexing.jl.
…mented (#925) The `sprand` docstring says that a custom `rfn` "should be a function `rfn(k)`... alternatively, if `rng` is supplied, it should instead be a function `rfn(rng, k)`". The matrix form honours this, the vector form does not: ```julia julia> sprand(10, 10, 0.5, k -> rand(Int8, k)) # works 10×10 SparseMatrixCSC{Int8, Int64} with 52 stored entries: ... julia> sprand(10, 0.5, k -> rand(Int8, k)) ERROR: MethodError: no method matching (::var"#1#2")(::TaskLocalRNG, ::Int64) ``` `sprand(n, p, rfn)` forwarded `rfn` unchanged to `sprand(rng, n, p, rfn)`, which calls `rfn(rng, k)`. The fix wraps it as `(r, i) -> rfn(i)`, exactly as the matrix method `sprand(m, n, p, rfn)` does, so both forms follow the same contract; the rng methods are unchanged. The test seeds the default rng and checks that the no-rng vector form with a one-argument `rfn` produces the same vector as the explicit-rng form with the two-argument `rfn`, and that both matrix forms accept the same functions. Tested with `Pkg.test(test_args=["sparsevector", "constructors"])` on Julia 1.14.0-DEV.3359 (nightly); whitespace check clean. No dispatch change; the docstring already describes the fixed behaviour. Deliberately left out: the `sprand(n, p, rfn, T)` vector method still calls `rfn(rng, T, k)` (a three-argument form neither documented nor shared with the matrix method); changing it would alter working calls, so it is not touched here. Backport candidate: pure bug fix making a documented call work, no new methods. Note that a two-argument `rfn` passed to the vector form *without* an rng, which the docs never allowed and the matrix form never accepted, now needs an explicit rng. Written by Claude Code (Fable 5.1) from a type-stability review of `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit f32603a)
`fill!` on a view of a sparse matrix returns the parent instead of the
view when either index range is empty, which also makes the return type
a `Union{SubArray, SparseMatrixCSC}`:
```julia
julia> A = sprand(4, 4, 0.5);
julia> fill!(view(A, 1:0, 1:2), 3.0) === A
true
julia> fill!(view(A, 1:2, 1:2), 3.0) === A
false
```
The early return for an empty `I` or `J` in `Base.fill!(V::SubArray{Tv,
<:Any, <:AbstractSparseMatrixCSC{Tv}, ...}, x)` in `src/indexing.jl`
wrote `return A` (the parent) where every other path returns `V`. The
fix returns `V`. The test asserts `@inferred fill!(view, x) === view`
for a non-empty view and for both empty-range views, next to the
existing "fill! for SubArrays" check that the non-empty path returns the
view.
Tested with `Pkg.test(test_args=["sparsematrix", "indexing", "fixed"])`
on Julia 1.14.0-DEV.3359 (nightly); whitespace check clean. No dispatch
change, no docs. Backport candidate: one-line pure bug fix, no new
methods or behaviour beyond the documented `fill!` return value.
Written by Claude Code (Fable 5.1) from a type-stability review of
`main`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 84b5114)
Backport adjustment: applied to src/sparsematrix.jl (for src/indexing.jl) by hand, the surrounding `fill!` code differs (no fixed-pattern branch).
…a dimension of an empty sparse array (#928) `maximum`, `minimum` and `extrema` along a dimension of an empty sparse array return a sparse array, where dense input gives an `Array` and every non-empty sparse input does too. The result type is therefore inferred as a `Union` for all sparse inputs, empty or not. ```julia julia> maximum(spzeros(3, 0); dims = 1) 1×0 SparseMatrixCSC{Float64, Int64} with 0 stored entries julia> maximum(zeros(3, 0); dims = 1) 1×0 Matrix{Float64} julia> Base.return_types(A -> maximum(A; dims = 1), (SparseMatrixCSC{Float64,Int},)) 1-element Vector{Any}: Union{SparseMatrixCSC{Float64, Int64}, Matrix{Float64}} ``` Base's `reducedim_init` for `max`, `min` and `_extrema_rf` seeds the result from the first slice of the reduction, and when that slice is empty it returns `map(f, slice)` as the result, which for a view of a sparse array is sparse; a non-empty slice goes through `reducedim_initarray`, which this package already makes dense. The fix defines `reducedim_init` for those three ops on the sparse types (matrices, column-subset views, their adjoints and transposes, and vectors) to run Base's own seeding, NaN and `missing` handling on a dense copy of the first slice (one row, column or element, O(slice) to build), so the empty case is a dense `map` too. Base's `abs`/`abs2` seed for `max` is restated on the sparse types to stay unambiguous with the new method. The `sparse = true` keyword path is unchanged, since it initializes from a dense stand-in. Touches dispatch: adds `Base.reducedim_init` methods for the sparse types; the `ambiguous` job passes. Tested with `reductions` and `ambiguous` on Julia nightly 1.14.0-DEV.3359. Left out: `Adjoint`/`Transpose` of a `SparseVector` (not covered by the existing `reducedim_initarray` hook either, so every reduction of it along a dimension is sparse, and `extrema(v'; dims = 1)` throws a `MethodError`), and the inference of `maximum(A'; dims = 1)` also needs the constant propagation fix for the `sparse` keyword on adjoints (separate PR). A backport candidate: a pure bug fix (result type of an empty input) adding no new behaviour; no label applied. Written by Claude Code (Fable 5.1) from a type-stability review of `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 1b001a6) Backport adjustment: applied to src/sparsematrix.jl and test/sparsematrix_ops.jl (for src/reductions.jl and test/reductions.jl). The `reducedim_init` methods are defined for `AbstractSparseMatrixCSC` and `AbstractCompressedVector` only: on release-1.13, reductions over adjoints and views of sparse matrices do not go through the sparse kernels (#798 is not backported), so the adjoint and view cases and the `sparse = true` keyword test are dropped.
… from `sparse(F.L)` (#933) `F \ B`, `F' \ B`, `F.L \ B` and `F \ B'` for a `CHOLMOD.Factor` with a `SparseMatrixCSC` right-hand side, and `sparse(F.L)` / `sparse(F.LD)`, infer `Union{Symmetric{T,SparseMatrixCSC{T,Ti}}, SparseMatrixCSC{T,Ti}}` (`Hermitian` for complex), although they always return a `SparseMatrixCSC`. Before: ```julia julia> using LinearAlgebra, SparseArrays julia> using SparseArrays: CHOLMOD julia> Base.return_types(\, (CHOLMOD.Factor{Float64,Int}, SparseMatrixCSC{Float64,Int})) 1-element Vector{Any}: Union{Symmetric{Float64, SparseMatrixCSC{Float64, Int64}}, SparseMatrixCSC{Float64, Int64}} julia> Base.return_types(sparse, (CHOLMOD.FactorComponent{Float64,:L,Int},)) 1-element Vector{Any}: Union{Symmetric{Float64, SparseMatrixCSC{Float64, Int64}}, SparseMatrixCSC{Float64, Int64}} ``` After: ```julia julia> Base.return_types(\, (CHOLMOD.Factor{Float64,Int}, SparseMatrixCSC{Float64,Int})) 1-element Vector{Any}: SparseMatrixCSC{Float64, Int64} julia> Base.return_types(sparse, (CHOLMOD.FactorComponent{Float64,:L,Int},)) 1-element Vector{Any}: SparseMatrixCSC{Float64, Int64} ``` All of them wrapped their `CHOLMOD.Sparse` result with `sparse(::Sparse)`, which branches on the runtime `stype` and returns a `Symmetric`/`Hermitian` view when it is nonzero. The output of `cholmod_spsolve` and of `cholmod_factor_to_sparse` is always unsymmetric (`stype == 0`; checked at runtime for `cholesky` and `ldlt`, real and complex), so the four call sites (`\(::Factor, ::SparseMatrixCSC)`, `\(::FactorComponent, ::SparseMatrixCSC)`, `sparse(::FactorComponent{_,:L})`, `sparse(::FactorComponent{_,:LD})`) now use `SparseMatrixCSC(::Sparse)`, which is inferred and throws an `ArgumentError` if a symmetric one ever appeared. The adjoint-factor and adjoint-right-hand-side methods route through these and are fixed with them. `sparse(F::Factor)` is unchanged; it already infers `SparseMatrixCSC`. No method signatures change, so dispatch is untouched. Tested on Julia nightly 1.14.0-DEV: `solvers/cholmod` passes, `.ci/check-whitespace.jl` clean; the added `@inferred` tests fail on `main` without the source change. They cover `F \ B`, `F' \ B`, `F \ B'` and `F.PtL \ B` for real `cholesky` and `ldlt`, one complex `cholesky` solve, and `sparse(F.L)` / `sparse(F.LD)` in the existing factor-extraction tests. UMFPACK and SPQR are unaffected: their solves and factor components already infer concrete types. Independent of #912 and #915, which do not touch these lines. Backport candidate for 1.13: an inference-only fix with no new methods or behaviour (the values returned are the same objects as before); no label applied. Written by Claude Code (Fable 5.1) from a type-stability review of `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com> (cherry picked from commit 7f0badd) Backport adjustment: tests in test/cholmod.jl (for test/solvers/cholmod.jl) assert the index type `Int`, since `Ti` is not bound there on release-1.13. The `F' \\ B` case is dropped: on release-1.13 the adjoint of a CHOLMOD factor solved with a sparse right-hand side returns a `CHOLMOD.Sparse`, which main changed in a PR not labelled for backport.
…`prod` and `mapreduce` as for dense (#932) `sum`, `prod` and `mapreduce` with `+` or `*` over a sparse array of small integers give a different result type than dense input when unstored entries are involved: `prod` and `sum` keep the narrow type (or give `Int` for unsigned), and a plain `+` widens to `Int` where dense wraps in the element type. The scalar results therefore infer as a `Union`. Before: ```julia julia> prod(sparse(Int8[2 0; 0 3])), sum(spzeros(UInt8, 2, 2)), mapreduce(abs2, +, sparse(Int8[2 0; 0 3])) (0, 0, 13) # Int8, Int64, Int64 julia> Base.return_types(prod, (SparseMatrixCSC{Int8,Int},)) 1-element Vector{Any}: Union{Int64, Int8} ``` After, as for dense: ```julia julia> prod(sparse(Int8[2 0; 0 3])), sum(spzeros(UInt8, 2, 2)), mapreduce(abs2, +, sparse(Int8[2 0; 0 3])) (0, 0x0000000000000000, 13) # Int64, UInt64, Int8 julia> Base.return_types(prod, (SparseMatrixCSC{Int8,Int},)) 1-element Vector{Any}: Int64 ``` The unstored entries are folded in at once as `f(0) * nzeros` or `f(0)^nzeros`, so they missed the `reduce_first` widening Base applies to the stored ones, and the count's `Int` leaked into a plain `+`. The fix seeds their contribution with `Base.reduce_first(op, f(0))`, converted to the accumulator's type so an `init` or another mapped value that is wider still wins, and multiplies a bit integer by the count in its own type so a plain `+` on `Int8` wraps as dense does. Tested with `reductions` and `sparsevector` on Julia nightly 1.14.0-DEV.3359, comparing type and value against dense for `Int8` and `Bool`, with stored and unstored zeros, scalar, with `dims`, with `init` and with `sparse = true`. No dispatch change, no docs needed. A backport candidate: a result-type bug fix with no new methods. Written by Claude Code (Fable 5.1). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com> (cherry picked from commit 6657313) Backport adjustment: applied to src/sparsematrix.jl and test/sparsematrix_ops.jl (for src/reductions.jl and test/reductions.jl); the `sparse = true` comparison is dropped from one test, since that keyword comes from #798, which is not backported.
…operty access (#930) `F.L`, `F.PtL`, `F.UP`, `F.D`, ... on a `CHOLMOD.Factor` infer to `FactorComponent{Tv,_A,Ti} where _A`, so every solve through a component, including the documented `F.UP \ (F.PtL \ b)` idiom, infers `Any` and dispatches dynamically. `F.prow` / `F.pcol` on a `QRSparse` infer `Vector` and a four-way `Union{ReentrantLock, QRSparseQ, SparseMatrixCSC, Vector}`, and the same through `lq` (`AdjointQRSparse`). Before: ```julia using LinearAlgebra, SparseArrays using SparseArrays: CHOLMOD, SPQR Base.return_types(F -> F.L, (CHOLMOD.Factor{Float64,Int},)) # FactorComponent{Float64, _A, Int64} where _A Base.return_types((F, b) -> F.UP \ (F.PtL \ b), (CHOLMOD.Factor{Float64,Int}, Vector{Float64})) # Any Base.return_types(F -> F.pcol, (SPQR.QRSparse{Float64,Int},)) # Union{ReentrantLock, QRSparseQ{Float64,Int64}, SparseMatrixCSC{Float64,Int64}, Vector} ``` After: ```julia Base.return_types(F -> F.L, (CHOLMOD.Factor{Float64,Int},)) # SparseArrays.CHOLMOD.FactorComponent{Float64, :L, Int64} Base.return_types((F, b) -> F.UP \ (F.PtL \ b), (CHOLMOD.Factor{Float64,Int}, Vector{Float64})) # Vector{Float64} Base.return_types(F -> F.pcol, (SPQR.QRSparse{Float64,Int},)) # Vector{Int64} ``` `getproperty(::Factor)` is `@inline` and sees a constant `sym`, but the symbol is not constant-propagated into `FactorComponent(F, sym)`, which is what puts it in the type parameter; `Base.@constprop :aggressive` on that constructor makes `F.L` infer `FactorComponent{Float64,:L,Int}`, `F.L'` infer the `:U` component, and the component solves infer `Vector{T}`. The `QRSparse` `getproperty` branches for `:prow` / `:pcol` read `F.rpivinv` / `F.cpiv` (and `P.pcol` / `P.prow` in the adjoint method) through `getproperty` again, and inference refuses the recursive edge; reading the fields with `getfield` makes all four infer `Vector{Ti}`. No method signature changes, so dispatch is untouched. Tested on Julia nightly 1.14.0-DEV (linux x64): `solvers/cholmod` and `solvers/spqr` pass (1812 tests), `.ci/check-whitespace.jl` clean; the added `@inferred` tests fail on `main` without the source change. Real and complex factors from `cholesky` and `ldlt`, and both index types for `qr`/`lq`. The After output above was re-checked on nightly (macOS). Independent of #912 and #915: #912 edits the neighbouring `getproperty(::Factor)` and #915 the neighbouring `copy(::QRSparse)`, but neither touches these lines, so no conflict is expected. Backport candidate for 1.13: an inference-only fix that adds no methods or behaviour; no label applied. Written by Claude Code (Fable 5.1) from a type-stability review of `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 53b8970) Backport adjustment: the `AdjointQRSparse` (`lq`) hunk and its test are dropped, as `lq` of a sparse matrix is not on release-1.13; the CHOLMOD tests assert the index type `Int`, since `Ti` is not bound there in test/cholmod.jl.
…ting-point eltypes (#921) `\(::AbstractSparseMatrixCSC, B)`, its `Adjoint`/`Transpose` variants and `factorize(::AbstractSparseMatrixCSC)` wrap any square matrix that passes `ishermitian` in `Hermitian`. For an integer eltype that reaches LinearAlgebra's generic `factorize(::HermOrSym)`, which is a *dense* Bunch–Kaufman in `Rational{BigInt}` (O(n²) memory, exact arithmetic), while a non-symmetric integer matrix converts to `Float64` and goes to UMFPACK. Dense `Matrix{Int} \ Vector{Int}` returns `Vector{Float64}` in both cases. Before: ```julia julia> sparse([2 1; 1 2]) \ [1, 2] 2-element Vector{Rational{BigInt}}: ... julia> sparse([2 1; 3 2]) \ [1, 2] 2-element Vector{Float64}: ... julia> Base.return_types(\, (SparseMatrixCSC{Int,Int}, Vector{Int})) 1-element Vector{Any}: Union{Vector{Rational{BigInt}}, Vector{Float64}} julia> factorize(sparse([2 1; 1 2])) BunchKaufman{Rational{BigInt}, Matrix{Rational{BigInt}}, Vector{Int64}} ... ``` After: ```julia julia> sparse([2 1; 1 2]) \ [1, 2] 2-element Vector{Float64}: 0.0 1.0 julia> sparse([2 1; 3 2]) \ [1, 2] 2-element Vector{Float64}: 0.0 1.0 julia> Base.return_types(\, (SparseMatrixCSC{Int,Int}, Vector{Int})) 1-element Vector{Any}: Vector{Float64} (alias for Array{Float64, 1}) julia> factorize(sparse([2 1; 1 2])) SparseArrays.UMFPACK.UmfpackLU{Float64, Int64} L factor: 2×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries: 1.0 ⋅ 0.5 1.0 U factor: 2×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries: 0.666667 0.333333 ⋅ 0.5 ``` The same happened for `A'`, `transpose(A)`, `Complex{Int}` and a `SparseVector` right-hand side. The fix gates the three Hermitian branches on the eltype not being an `Integer` or `Complex{<:Integer}`, so integer matrices fall through to the `lu` branch, which already converts to floating point and to the promoted result type. Every other eltype keeps its current path: floating point still goes to the sparse Cholesky/LDLt, and `Rational` still solves exactly through LinearAlgebra's generic factorization (an earlier revision rerouted `Rational` too, which returned inexact binary fractions; a test now pins the exact result). No floating-point path changes. The triangular and diagonal branches already matched dense for integers and are untouched. Tested on Julia nightly: `solvers/solvers` and `linalg` suites (1242 pass), `ambiguous` (13 pass, 1 known broken), and the whitespace check; the new testset fails without the source change. No new methods or dispatch changes beyond a private helper, no docs impact. Left out: an explicit `Hermitian(sparse_int) \ b` by the user still goes through LinearAlgebra's generic dense path; that is LinearAlgebra's `factorize(::HermOrSym)` and out of scope here. A non-Hermitian `Rational` sparse matrix already went through floating-point UMFPACK on `main` and is unchanged. Backport candidate: yes, a pure bug fix with no new methods or behaviour for float eltypes, so labelled `backport 1.13`. Written by Claude Code (Fable 5.1) from a type-stability review of `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com> (cherry picked from commit 683d9b0) Backport adjustment: the reroute to `lu` applies to a dense right-hand side only. On release-1.13 UMFPACK cannot solve with a sparse right-hand side, so a sparse one keeps the previous Hermitian path, which works there; the test in test/linalg_solvers.jl (for test/solvers/solvers.jl) checks only values for a sparse right-hand side, skips the complex transpose case that fails on release-1.13 before and after this change, and does not use `@inferred`, as `\\` is not inferred for these eltypes on release-1.13.
…apply `f` to the empty case (#922) The index returned by `findmin`/`findmax` on a `SparseVector` (and so `argmin`/`argmax`) depends on which branch runs, and with no stored entries `findmin(f, x)` ignores `f`. Before: ```julia julia> using SparseArrays julia> Base.return_types(findmax, (SparseVector{Float64,Int},)) 1-element Vector{Any}: Tuple{Float64, Union{Nothing, Int64}} julia> Base.return_types(argmin, (SparseVector{Float64,Int32},)) 1-element Vector{Any}: Union{Nothing, Int32, Int64} julia> x = SparseVector(5, Int32[2, 4], [2.0, -1.0]); typeof(findmax(x)[2]), typeof(findmin(x -> 1.0, x)[2]) (Int32, Int64) julia> findmin(x -> x + 1, spzeros(3)), findmin(x -> x + 1, zeros(3)) ((0.0, 1), (1.0, 1)) ``` After: ```julia julia> Base.return_types(findmax, (SparseVector{Float64,Int},)) 1-element Vector{Any}: Tuple{Float64, Int64} julia> Base.return_types(argmin, (SparseVector{Float64,Int32},)) 1-element Vector{Any}: Int64 julia> x = SparseVector(5, Int32[2, 4], [2.0, -1.0]); typeof(findmax(x)[2]), typeof(findmin(x -> 1.0, x)[2]) (Int64, Int64) julia> findmin(x -> x + 1, spzeros(3)), findmin(x -> x + 1, zeros(3)) ((1.0, 1), (1.0, 1)) ``` Base's `Int` position was returned when every entry is stored, `nzinds[index]::Ti` when a stored entry wins, and the `Union{Nothing,Int}` from `findfirst` in the search for the first zero. Every path now returns an `Int`, as dense `findmin`/`findmax` do (`Vector{Int}` keys). The stored winner from `findmin(f, nonzeros(x))` is compared with `f(0)` at the first implicit zero using the same test Base's dense scan uses to move on to a later element (`Base.isgreater` for `findmin`, `isless` for `findmax`), with a tie going to the lower index. That replaces the `<`/`isnan` special-casing and the two `findfirst` searches with one O(nnz) scan, and it also makes the result agree with dense when `f` of a stored value ties with `f(0)`, and on NaN and signed zeros (before, `findmin(x -> x^2 - x, SparseVector(3, [1], [1.0]))` gave index 2 where dense gives 1, and `findmin(SparseVector(3, [3], [-0.0]))` gave `(0.0, 1)` where dense gives `(-0.0, 3)`). The empty case returns `f(zero(T))`. Tested on nightly 1.14.0-DEV.3359: `sparsevector` suite (5663 passed), whitespace check, and a local sweep of 27,000 random sparse vectors (`Ti` in `Int`, `Int32`, `Int8`; stored zeros, `-0.0`, NaN; six `f`s; complex through `abs`/`abs2`) checked `===` against `Vector(x)` with `@inferred`. The committed test is a small subset with `Ti = Int32`: the empty, implicit-zero-first, stored-zero, all-stored, `-0.0` and NaN vectors, a tie with `f(0)` and NaN at the implicit zero, one complex case and the empty `f` case. No dispatch change, no docs change. Left out: `length`/`axes` of a `SparseVector{Tv,Int32}` are themselves `Int32` (the `n::Ti` field), which is why `firstindex(x)` needed an `Int` conversion here; that is a separate issue. `findmin(x; dims=1)` on a sparse vector goes through Base's generic `reducedim` path and is untouched. Backport candidate for 1.13: a bug fix with no new methods or public surface (the helper `_firstimplicitzero` is internal). Written by Claude Code (Fable 5.1) from a type-stability review of `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com> (cherry picked from commit f265466)
…dimensions without touching its pattern (#964) Indexing a `FixedSparseCSC` with an unsorted row vector throws, because `getindex_general` in `src/indexing.jl` permutes the rows of the already fixed result of `getindex_I_sorted`, and `F[I, j]` rebuilds a `SparseVector` from the read-only buffers of a fixed `F[I, [j]]`. `cumsum`, `cumprod` and `accumulate` along a dimension of a fixed array throw too, because Base allocates their result with the pattern-keeping `similar(A, T)`. **Before** ```julia julia> F = SparseArrays.fixed(sparse([1,2,3],[1,2,3],[1.0,2.0,3.0])); julia> F[[2,1],[1,2]] ERROR: ArgumentError: cannot modify a ReadOnly array, the sparsity pattern of a fixed sparse array is read-only julia> F[[1,2], 1] ERROR: MethodError: no method matching SparseVector(::Int64, ::SparseArrays.ReadOnly{Int64, 1, Vector{Int64}}, ::Vector{Float64}) julia> cumsum(SparseArrays.fixed(sparse([1,2],[1,2],[1.0,2.0])), dims=1) ERROR: ArgumentError: cannot store a new entry at (2, 1) in a FixedSparseCSC, its sparsity pattern is read-only ``` **After** ```julia julia> F[[2,1],[1,2]] 2×2 SparseArrays.FixedSparseCSC{Float64, Int64} with 2 stored entries: ⋅ 2.0 1.0 ⋅ julia> F[[1,2], 1] 2-element SparseArrays.FixedSparseVector{Float64, Int64} with 1 stored entry: [1] = 1.0 julia> cumsum(SparseArrays.fixed(sparse([1,2],[1,2],[1.0,2.0])), dims=1) 2×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries: 1.0 ⋅ 1.0 2.0 ``` `getindex_general` and the `F[I, j]` path now index `_unsafe_unfix(A)`, permute or rewrap the plain result, and fix its pattern afterwards, so indexing keeps returning an array with the fixedness of its input as the existing test requires; the input's pattern is never written. For the accumulations, Base offers no shape-taking `similar` hook, so `cumsum`, `cumprod` and `accumulate` on `FixedSparseCSC` and `FixedSparseVector` delegate to the plain array and return a writable `SparseMatrixCSC`/`SparseVector`, the way `circshift` already does: the result has fill-in, so there is no pattern to share. Left out: `v[[1,3]]` on a `FixedSparseVector` still throws a `MethodError`, since that path builds a `SparseMatrixCSC` from the read-only index vector; it is a separate kernel. Touches dispatch. Backport candidate. Written by Claude Code (Claude Fable 5.1) in an autonomous session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 2b6db54) Backport adjustment: src/fixed.jl does not exist on release-1.13; the `cumsum`/`cumprod`/`accumulate` methods go to src/sparsevector.jl after the other `FixedSparseVector` methods, and the `getindex` hunks to src/sparsematrix.jl and src/sparsevector.jl (for src/indexing.jl).
…D.Factor` (#942) `ldiv!(x, F, b)` with an `UmfpackLU` or a `CHOLMOD.Factor` rejects only `x === b`, so overlapping views pass, and with UMFPACK iterative refinement on the solve reads `b` after writing `x` (`src/solvers/umfpack.jl` `solve!`, `src/solvers/cholmod.jl` `ldiv!`). **Before** ```julia using LinearAlgebra, SparseArrays A = sparse([2.0 1 0; 1 3 1; 0 1 4]) F = lu(A) F.control[SparseArrays.UMFPACK.JL_UMFPACK_IRSTEP] = 2 B = A * ones(3, 2) ldiv!(view(B, :, 1), F, view(vec(B), 1:3)); B[:, 1] # [0.2592592592592592, -0.07407407407407393, 0.07407407407407399] ldiv!(B, F, B) # ERROR: ArgumentError: output array must not be aliased with input array deepcopy(SparseArrays.CHOLMOD.CholmodWS(cholesky(A))).Y[] # shares the source's Y/E handles ``` **After** ```julia ldiv!(view(B, :, 1), F, view(vec(B), 1:3)); B[:, 1] # [1.0000000000000002, 0.9999999999999998, 1.0] ldiv!(B, F, B) # 3×2 Matrix{Float64}: ones deepcopy(SparseArrays.CHOLMOD.CholmodWS(cholesky(A))).Y[] # Ptr{cholmod_dense_struct}(0x0000000000000000) ``` The `===` check becomes `Base.mightalias(x, b) && (b = copy(b))` in UMFPACK's `solve!` and CHOLMOD's `ldiv!`, and once up front in `_Aq_ldiv_B!` so a matrix right-hand side is copied once rather than per column. `copy` rather than `Base.unalias`, because unaliasing a `SubArray` copies its whole parent. `deepcopy(::CholmodWS)` returns a fresh workspace instead of a field-wise copy whose shared `Y`/`E` handles both copies would free. Left out: SPQR, which already gathers `B` into its workspace before writing `X`. Touches dispatch (adds `deepcopy_internal` for `CholmodWS`, memoized in the `IdDict` so repeated references resolve to one copy). Backport candidate. Written by Claude Code (Claude Fable 5.1) in an autonomous session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 0bf1d1b) Backport adjustment: release-1.13 has no `CholmodWS` and no workspace keyword on CHOLMOD `ldiv!`, so the `deepcopy` hunk and the workspace parts of the test are dropped. Where an aliased right-hand side is copied, the solver is called again with the copy rather than rebinding `b`: in release-1.13's UMFPACK matrix kernel, `b` is a column view, and rebinding it to a `Vector` made it type-unstable and broke the zero-allocation test in test/umfpack.jl.
Indexing a `FixedSparseVector` with an index vector, a Bool mask or a
step range throws, because the two `getindex(x::AbstractSparseVector,
I::AbstractVector)` / `I::AbstractArray` methods in
`src/sparsevector.jl` punt to `SparseMatrixCSC` built from
`nonzeroinds(x)`, which is a `ReadOnly` vector for a fixed vector and
not a valid `SparseMatrixCSC` field.
**Before**
```julia
julia> v = SparseArrays.fixed(sparsevec([1, 3], [1.0, 2.0], 4));
julia> v[[3, 1]]
ERROR: MethodError: no method matching SparseMatrixCSC(::Int64, ::Int64, ::Vector{Int64}, ::SparseArrays.ReadOnly{Int64, 1, Vector{Int64}}, ::Vector{Float64})
julia> v[[true, false, true, false]]
ERROR: MethodError: no method matching SparseMatrixCSC(::Int64, ::Int64, ::Vector{Int64}, ::SparseArrays.ReadOnly{Int64, 1, Vector{Int64}}, ::Vector{Float64})
```
**After**
```julia
julia> v[[3, 1]]
2-element SparseArrays.FixedSparseVector{Float64, Int64} with 2 stored entries:
[1] = 2.0
[2] = 1.0
julia> v[[true, false, true, false]]
2-element SparseArrays.FixedSparseVector{Float64, Int64} with 2 stored entries:
[1] = 1.0
[2] = 2.0
```
Both methods now build the scratch matrix from `_unsafe_unfix(x)`, which
shares the underlying buffers without copying, and wrap the result in
`@if_move_fixed x`, the same rule the unit-range and colon paths already
follow: a fixed input gives a fixed result with fresh buffers, and an
unfixed input is unchanged. A matrix-shaped index on a fixed vector
returns a `FixedSparseCSC`, matching what a fixed matrix returns for the
same index.
Left out: the matrix-side `getindex_general` fixedness, which is handled
elsewhere. Backport candidate.
Written by Claude Code (Claude Fable 5.1) in an autonomous session.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 7fbaf6a)
Backport adjustment: applied by hand to src/sparsevector.jl, whose surrounding code differs slightly (the `Bool` mask methods have no `checkbounds` on release-1.13).
#965) `istriu(A, k)` and `istril(A, k)` for `AbstractSparseMatrixCSC` in `src/sparsematrix.jl` scan the columns `1:min(n, m-1)` and `2:n`, which is only right for `k == 0`: `istriu` skips the columns that gain sub-band rows when `k > 0`, and `istril` skips column 1 when `k < 0`. Through LinearAlgebra's forwarding the same answers come back from `istril(adjoint(S), -k)` and `istriu(transpose(S), -k)`. **Before** ```julia julia> A = sparse([1], [2], [1.0], 1, 2); istriu(A, 2), istriu(Matrix(A), 2) (true, false) julia> B = sparse([1], [1], [1.0], 2, 1); istril(B, -1), istril(Matrix(B), -1) (true, false) julia> istril(adjoint(A), -2), istril(Matrix(adjoint(A)), -2) (true, false) ``` **After** ```julia julia> A = sparse([1], [2], [1.0], 1, 2); istriu(A, 2), istriu(Matrix(A), 2) (false, false) julia> B = sparse([1], [1], [1.0], 2, 1); istril(B, -1), istril(Matrix(B), -1) (false, false) julia> istril(adjoint(A), -2), istril(Matrix(adjoint(A)), -2) (false, false) ``` The per-column walk already compared each stored row against `col - k`; the column ranges now account for `k` as well. Both predicates handle offsets outside the matrix's diagonal range before doing arithmetic with `k`. Within that range, `istriu` computes the equivalent of `min(n, m+k-1)` by comparing before adding, avoiding overflow even for a matrix with `typemax(Int)` rows; `istril` starts at `max(1, k+2)`. Both still touch stored entries only, and a stored zero still does not count as a nonzero. Regression tests cover rectangular and empty matrices, real and complex values, adjoint/transpose forwarding, stored zeros, and extreme integer offsets. Extreme-offset tests assert the expected result directly because nightly's dense predicates also overflow for some of these inputs. Backport candidate. Written by Claude Code (Claude Fable 5.1) in an autonomous session. Overflow handling and regression tests updated by OpenAI Codex. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com> (cherry picked from commit 76665a5) Backport adjustment: applied by hand: release-1.13's `istriu`/`istril` still index `colptr` directly (#852 rewrote them with `nzrange` on main), so the band checks and loop bounds were carried over into that form.
…loat` and `complex` of fixed-pattern arrays (#960) `is_hermsym` in src/sparsematrix.jl advances a `copy` of the column pointers as its tracker; for a `FixedSparseCSC` that copy is still `ReadOnly`, so `issymmetric`, `ishermitian`, `factorize` and `\` throw on a fixed matrix. Unary `-`, `float` and `complex` of a fixed matrix also lose the fixed type, and `float`/`complex` of an integer `FixedSparseVector` throw. **Before** ```julia julia> using SparseArrays, LinearAlgebra; using SparseArrays: fixed julia> F = fixed(sparse([4.0 1 0; 1 4 1; 0 1 4])); julia> issymmetric(F) # same for ishermitian(F), factorize(F), F \ [1.0, 2, 3] ERROR: ArgumentError: cannot modify a ReadOnly array, the sparsity pattern of a fixed sparse array is read-only julia> typeof(-F), typeof(-fixed(sparsevec([1, 3], [1.0, 2.0], 4))) (SparseMatrixCSC{Float64, Int64}, SparseArrays.FixedSparseVector{Float64, Int64}) julia> typeof(float(fixed(sparse([1 0; 0 2])))) # Base's generic array conversion SparseMatrixCSC{Float64, Int64} julia> float(fixed(sparsevec([1, 3], [1, 2], 4))) ERROR: MethodError: no method matching SparseVector(::Int64, ::SparseArrays.ReadOnly{Int64, 1, Vector{Int64}}, ::Vector{Float64}) ``` **After** ```julia julia> issymmetric(F), ishermitian(F), typeof(factorize(F)), F \ [1.0, 2, 3] (true, true, SparseArrays.CHOLMOD.Factor{Float64, Int64}, [0.17857142857142855, 0.28571428571428575, 0.6785714285714286]) julia> typeof(-F), typeof(-fixed(sparsevec([1, 3], [1.0, 2.0], 4))) (SparseArrays.FixedSparseCSC{Float64, Int64}, SparseArrays.FixedSparseVector{Float64, Int64}) julia> typeof(float(fixed(sparse([1 0; 0 2])))), typeof(float(fixed(sparsevec([1, 3], [1, 2], 4)))) (SparseArrays.FixedSparseCSC{Float64, Int64}, SparseArrays.FixedSparseVector{Float64, Int64}) ``` The tracker is now `Vector(getcolptr(A))`. Unary `-` of a fixed array returns a fixed array sharing the source pattern with fresh nonzeros, as `copy` of a fixed array does; the vector method sits next to the `SparseVector` one instead of going through broadcast. `float`/`complex(::FixedSparseCSC)` are defined next to the `SparseMatrixCSC` methods and share the pattern the same way; the vector methods build their result through a `_withnonzeros` helper that keeps the input's type. Stored zeros stay stored. Left out: `conj(::FixedSparseCSC{<:Complex})` still returns a `SparseMatrixCSC`; an indefinite Hermitian fixed matrix still fails in `factorize`/`\` because the Cholesky fallback calls `ldlt!`, which takes a `SparseMatrixCSC` only (a `@test_broken` in test/solvers/solvers.jl). Touches dispatch. Backport candidate. Other batch PRs edit src/sparsematrix.jl and src/sparsevector.jl in disjoint regions. Written by Claude Code (Claude Fable 5.1) in an autonomous session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 069abf7) Backport adjustment: the hunks for src/constructors.jl go to src/sparsematrix.jl, and the tests from test/solvers/solvers.jl and test/fixed.jl go to test/fixed.jl with a local `same_pattern` helper. The `factorize`/`\\` checks on a fixed Hermitian matrix are dropped: on release-1.13 they still fail later in `copytrito!`, which main fixed in PRs not labelled for backport.
…970) The pair and N-ary `map`/`broadcast` kernels in `src/higherorderfns.jl` convert their end-of-column sentinel `numrows(C) + 1` to the input index type, so any sparse array whose row count equals `typemax(Ti)` throws before the kernel starts. The vector column bound `colboundind` converts `nnz + 1` the same way, so a fully stored vector of that length fails too. **Before** ```julia julia> A = SparseMatrixCSC(127, 1, Int8[1, 2], Int8[1], [1.0]); x = SparseVector(127, Int8[1], [1.0]); julia> map(+, A, A) # likewise A .+ A, map(+, A, A, A), x .+ x ERROR: InexactError: trunc(Int8, 128) julia> y = SparseVector(127, Int8.(1:127), ones(127)); y .+ y ERROR: InexactError: trunc(Int8, 128) ``` **After** ```julia julia> map(+, A, A) == 2Array(A) && SparseArrays.indtype(map(+, A, A)) == Int8 true julia> (A .+ A, map(+, A, A, A), x .+ x, y .+ y) .== (2Array(A), 3Array(A), 2Array(x), 2Array(y)) (true, true, true, true) julia> n = Int128(typemax(Int)) + 1; w = SparseVector(n, [n], [1.0]); # indices wider than Int are kept julia> nonzeroinds(map!(+, SparseVector(n, [n], [0.0]), w, w)) == [n] true ``` Each kernel compares rows in a working type `W = promote_type(Int, indtype(A), indtype(B), ...)`: the sentinels are `W(numrows(C)) + one(W)` and the stored row indices are loaded as `W(storedinds(A)[Ak])` (the N-ary helpers `_rowforind`, `_fusedupdate`, `_initrowforcol` and `_fusedupdatebc` use `oftype(rowsentinel, ...)`), so small index types widen to `Int`, `Int128` stays `Int128`, and every comparison variable has one concrete type. The vector `colstartind`/`colboundind` return `Int` storage positions, since a vector has no `Ti`-typed column pointer; the matrix versions keep reading `colptr`. The `Ci::indtype(C)` declarations and the typed `for Ci::indtype(C)` loops go, because with a wider source they lowered to `Union{Ti,Int}` temporaries; the values written are at most `numrows(C)` and convert in `storedinds(C)[Ck] = Ci` as before. Loop order and results are unchanged; a seeded sweep over the pair, N-ary and vertically expanding kernels with `Int8`/`Int32`/`Int64` indices hashes identically to main. The new tests assert the kernels have no `Union{Ti,Int}` local for `Int8` and `Int32`. Left out: a dense-structured 127-row `Int8` matrix cannot exist because its column pointer would need 128, so the non-zero-preserving case is tested on a vector only. Backport candidate. Written by Claude Code (Claude Fable 5.1) in an autonomous session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 5f2a954) Backport adjustment: the two-argument and n-ary kernel hunks were applied by hand, as release-1.13 has no `_checkfixedpattern` (that hunk is dropped). The test defines the `hasunionlocal` helper locally (it lives in test/testhelpers.jl on main) and qualifies `SparseArrays.nonzeroinds`.
… `setindex!` kernel and check the value's shape (#946) `setindex!(A::AbstractSparseMatrixCSC, x, I::AbstractMatrix{Bool})` in `src/indexing.jl` has its own column walk that never compares `length(x)` with `count(I)`, so it reads `x` past its end under `@inbounds`, and it duplicates the linear-index kernel below it, which in turn accepts a Bool vector of the wrong length, writes a fixed pattern partially before throwing, and reads `I` and `x` while rewriting the storage they may alias. **Before** ```julia julia> S = sparse([1 0; 0 0.0]); S[[true false; false true]] = [1.0]; S 2×2 SparseMatrixCSC{Float64, Int64} with 2 stored entries: 1.0 ⋅ ⋅ 6.6578e-310 # uninitialized memory; BoundsError under --check-bounds=yes julia> S[[true false; false true]] = [1.0, 2.0, 3.0] # tail silently dropped julia> S[falses(2, 2)] = [1.0] # accepted julia> S[[true, false, true]] = [1.0, 2.0] # accepted julia> F = SparseArrays.fixed(sparse([1 0; 0 2.0])); F[[true true; false true]] = [5.0, 6.0, 7.0] ERROR: ArgumentError: cannot store a new entry at (1, 2) in a FixedSparseCSC, its sparsity pattern is read-only julia> F[1, 1] 5.0 # written before the throw julia> A = sparse(reshape([2, 1], 2, 1)); A[nonzeros(A)] = [20, 10] # I aliases the storage ERROR: BoundsError # under --check-bounds=yes; a segfault otherwise julia> A = sparse(reshape([1, 2], 2, 1)); A[trues(2, 1)] = view(nonzeros(A), 2:-1:1); Matrix(A) 2×1 Matrix{Int64}: 2 2 # should be [2; 1] ``` **After** ```julia julia> S[[true false; false true]] = [1.0] ERROR: DimensionMismatch: tried to assign 1 elements to 2 destinations julia> S[[true false; false true]] = [1.0, 2.0, 3.0] ERROR: DimensionMismatch: tried to assign 3 elements to 2 destinations julia> S[falses(2, 2)] = [1.0] ERROR: DimensionMismatch: tried to assign 1 elements to 0 destinations julia> S[[true, false, true]] = [1.0, 2.0] ERROR: BoundsError: attempt to access 2×2 SparseMatrixCSC{Float64, Int64} with 1 stored entry at index [Bool[1, 0, 1]] julia> F[[true true; false true]] = [5.0, 6.0, 7.0] ERROR: ArgumentError: cannot store a new entry at (1, 2) in a FixedSparseCSC, its sparsity pattern is read-only julia> F[1, 1] 1.0 # untouched julia> A = sparse(reshape([2, 1], 2, 1)); A[nonzeros(A)] = [20, 10]; Matrix(A) 2×1 Matrix{Int64}: 10 20 julia> A = sparse(reshape([1, 2], 2, 1)); A[trues(2, 1)] = view(nonzeros(A), 2:-1:1); Matrix(A) 2×1 Matrix{Int64}: 2 1 ``` 2000×2000, 1% stored, min of 5 runs, same Julia, Before = main, After = this PR's final commit. `Matrix{Bool}` is flat because scanning the dense mask dominates; the new path allocates the index vector (8 bytes per true); the unaliasing check is not measurable. The last four rows are a mask with a single true on a stored entry in the last column (an update, the worst case for a scan of the mask). | mask | trues | values | Before | After | |---|---|---|---|---| | BitMatrix | 0.1% | random | 3.52 ms, 2.07 MiB | 0.27 ms, 2.21 MiB | | BitMatrix | 0.1% | zeros | 3.41 ms, 0 B | 0.16 ms, 31 KiB | | Matrix{Bool} | 0.1% | random | 2.44 ms, 2.07 MiB | 1.42 ms, 2.21 MiB | | sparse Bool | 0.1% | random | 8.12 ms, 2.07 MiB | 0.24 ms, 2.21 MiB | | sparse Bool | 0.1% | zeros | 8.01 ms, 0 B | 0.12 ms, 31 KiB | | BitMatrix | 0.1% in pattern | updates | 3.42 ms, 0 B | 0.15 ms, 31 KiB | | sparse Bool | 0.1% in pattern | updates | 7.98 ms, 0 B | 0.11 ms, 31 KiB | | BitMatrix | 1% | random | 3.84 ms, 2.07 MiB | 1.00 ms, 2.50 MiB | | BitMatrix | 1% | zeros | 4.14 ms, 0 B | 0.78 ms, 315 KiB | | Matrix{Bool} | 1% | random | 2.72 ms, 2.07 MiB | 2.13 ms, 2.50 MiB | | sparse Bool | 1% | random | 16.71 ms, 2.07 MiB | 0.79 ms, 2.50 MiB | | sparse Bool | 1% | zeros | 17.15 ms, 0 B | 0.58 ms, 315 KiB | | BitMatrix | 1% in pattern | updates | 3.96 ms, 0 B | 0.53 ms, 314 KiB | | sparse Bool | 1% in pattern | updates | 16.73 ms, 0 B | 0.33 ms, 314 KiB | | column-range view of sparse Bool | 1, in pattern | update | 2257 µs, 0 B | 2.8 µs, 80 B | | Transpose of sparse Bool | 1, in pattern | update | 2283 µs, 0 B | 2.8 µs, 80 B | | Adjoint of sparse Bool | 1, in pattern | update | 2282 µs, 0 B | 2.8 µs, 80 B | | transpose of a column-range view of sparse Bool | 1, in pattern | update | 2847 µs, 0 B | 3.0 µs, 80 B | The logical method becomes `checkbounds(A, I)` followed by the linear-index method on `_masklinearindices(I)`: a walk of the stored entries for a sparse mask, its column views (`SparseMatrixCSCOrColumnSubset`) and the `Transpose`/`Adjoint` of either (parent walked, indices sorted), and `findall(vec(I))` otherwise. The linear method first copies the resolved indices and `x` when `Base.mightalias` says they share storage with `nonzeros`, `rowvals` or `getcolptr` of `A`, runs `setindex_shape_check(x, n)` and a `checkbounds` for Bool vectors before the fixed-pattern branch and the empty return, always reserves `n` slots when it grows the storage (the `x == 0` test was dead for an array `x`), and sends a fixed `A` to `_setindex_fixed!`, which locates every index and rejects a nonzero outside the pattern before writing anything. Left out: scalar `S[mask] = v`, which still takes Base's per-element path. Backport candidate. Written by Claude Code (Claude Fable 5.1) in an autonomous session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 2c4594f) Backport adjustment: ported by hand to src/sparsematrix.jl (for src/indexing.jl). The fixed-pattern part (`_setindex_fixed!`) is dropped: release-1.13's linear-index kernel has no fixed-pattern branch and no `_throwfixedinsert`, both from later fixed-pattern work on main. The sparse mask walk is defined for `AbstractSparseMatrixCSC{Bool}` and its transposes, as release-1.13 has no `SparseMatrixCSCOrColumnSubset`; views of a sparse mask take the `findall` path. The fixed-pattern test cases are dropped accordingly.
…he diagonal scaling loops (#944) `D \ A` and `A / D` with a sparse `A` and a singular `Diagonal` return `Inf` instead of throwing, because the out-of-place kernels `ldiv!(C, D, A)` and `LinearAlgebra._rdiv!(C, A, D)` in `src/linalg.jl` never check the diagonal; they and `lmul!`/`rmul!`/`ldiv!`/`rdiv!` also carry five copies of the same stored-entry loop. **Before** ```julia julia> D = Diagonal([1.0, 0.0]); A = sparse([1.0 2.0; 3.0 4.0]); julia> D \ A 2×2 SparseMatrixCSC{Float64, Int64} with 4 stored entries: 1.0 2.0 Inf Inf julia> A / D 2×2 SparseMatrixCSC{Float64, Int64} with 4 stored entries: 1.0 Inf 3.0 Inf ``` **After** ```julia julia> D \ A ERROR: SingularException(2) julia> A / D ERROR: SingularException(2) ``` All six diagonal scaling methods now share one `_scalecols!` / `_scalerows!` pair under a "diagonal scaling" section of `src/linalg.jl`, with `_checkscaledims` and `_checknonsingular` (first zero -> `SingularException(i)`) run before anything is written; the 2-arg `ldiv!`/`rdiv!` are one-liners over the 3-arg kernels, and `rdiv!(A, D)` no longer leaves `A` partially scaled when the zero is not the first entry. Tests compare a complex matrix and vector against dense, and assert the destination is unchanged after the throw. Left out: `D \ x` for a sparse vector `x` keeps LinearAlgebra's generic 3-arg `ldiv!`, which already throws. Touches dispatch (the 2-arg `ldiv!` union method is now two methods). Backport candidate. Written by Claude Code (Claude Fable 5.1) in an autonomous session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 82b5f16) Backport adjustment: the `rmul!`/`lmul!` hunks (src/matmul.jl on main) and the removal of the old `ldiv!`/`_rdiv!` methods were applied by hand to src/linalg.jl, which holds all of them on release-1.13.
…ed!` (#1052) `SparseArrays.dropstored!(A, I::AbstractVector{<:Integer}, J::AbstractVector{<:Integer})` mishandles its index vectors in three ways: it returns before checking bounds when nothing is stored, it reads a Boolean mask as a vector of the indices 0 and 1 (and so throws a `BoundsError` for any mask with a `false`), and it reads index vectors that are the matrix's own buffers while it compacts them. ```julia using SparseArrays using SparseArrays: dropstored! show_or_err(f) = try display(Matrix(f())) catch e; println(sprint(showerror, e)) end show_or_err(() -> dropstored!(spzeros(2, 2), [5], [1])) show_or_err(() -> dropstored!(sparse([1.0 2 3; 4 5 6; 7 8 9]), [true, false, true], [1, 3])) B = sparse([11.0 21; 12 0]) show_or_err(() -> dropstored!(B, view(rowvals(B), 1:1), 1:2)) ``` **Before** ``` 2×2 Matrix{Float64}: 0.0 0.0 0.0 0.0 BoundsError: attempt to access 3×3 SparseMatrixCSC{Float64, Int64} with 9 stored entries at index [Bool[0, 1, 1], [1, 3]] 2×2 Matrix{Float64}: 0.0 21.0 12.0 0.0 ``` **After** ``` BoundsError: attempt to access 2×2 SparseMatrixCSC{Float64, Int64} with 0 stored entries at index [[5], [1]] 3×3 Matrix{Float64}: 0.0 2.0 0.0 4.0 5.0 6.0 0.0 8.0 0.0 2×2 Matrix{Float64}: 0.0 0.0 12.0 0.0 ``` The method checked bounds by hand, on the sorted vectors, after the `nnz(A) == 0` early return, and never lowered its indices; the kernel then tests `rowval[ridx] in I` while it overwrites `rowval`. It now calls `_lower_indices(A, I, J)` first, which checks the bounds as `getindex` does and turns a mask into the indices it selects, and passes both vectors through `_unalias_setindex`, the helper `setindex!` already uses, which copies a vector only when it may share storage with one of the matrix's buffers. The hand-written check is removed. Split from #1021. This is a bug fix and a backport candidate, provided the release branch has `_lower_indices` and `_unalias_setindex`. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 6aa11ed) Backport adjustment: ported by hand to src/sparsematrix.jl: release-1.13 has no `_lower_indices` helper (from #857) and no `_checkpatternwritable`, so the lowering (`to_indices`, `checkbounds`, `ensure_indexable`) is written out in `dropstored!`. The tests go to test/sparsematrix_constructors_indexing.jl unguarded and compare with `==`, as release-1.13 has neither comprehensive mode nor `mismatch`.
#1053) Two-index `setindex!` accepts a Boolean mask whose length is not that of the dimension it indexes, where a dense array throws `BoundsError`. This holds both for a sparse destination and for assigning a sparse matrix into a `Matrix` through masks. ```julia using SparseArrays A = sparse([1.0 2 0; 0 3 4]) try; A[[true], 1] = [9.0]; println("sparse dest: no error, A = ", Matrix(A)); catch e; println("sparse dest: ", typeof(e)); end M = zeros(2, 3) try; M[[true], [1]] = sparse(fill(9.0, 1, 1)); println("dense dest: no error, M = ", M); catch e; println("dense dest: ", typeof(e)); end D = zeros(2, 3) try; D[[true], 1] = [9.0]; println("dense: no error"); catch e; println("dense: ", typeof(e)); end ``` **Before** ``` sparse dest: no error, A = [9.0 2.0 0.0; 0.0 3.0 4.0] dense dest: no error, M = [9.0 0.0 0.0; 0.0 0.0 0.0] dense: BoundsError ``` **After** ``` sparse dest: BoundsError dense dest: BoundsError dense: BoundsError ``` `setindex!(A::AbstractSparseMatrixCSC, V::AbstractVecOrMat, Ix, Jx)` called `Base.ensure_indexable(to_indices(A, (Ix, Jx)))` and only then `checkbounds`. `to_indices` turns a mask into a `Base.LogicalIndex`, which still knows the mask, but `ensure_indexable` collects it into the vector of true positions, so the bounds check saw in-range integers and the mask's length was gone. The method now lowers its indices with `_lower_indices`, which `getindex` already uses and which checks bounds between the two steps. That check covers every index, so the hand-written test of the first and last index further down, already unreachable, is removed. The five `setindex!(A::Matrix, x::AbstractSparseMatrixCSC, I, J)` mask methods lose the length in the same way through `findall`, and now call `checkbounds(A, I, J)` first. In both cases the throw happens before the destination is written. Split from #1021. This is a bug fix with no new methods, so it is a backport candidate. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 5f4149f) Backport adjustment: ported by hand to src/sparsematrix.jl: release-1.13 has no `_lower_indices` helper (from #857), so the lowering with the bounds check before `ensure_indexable` is written out; the test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `exact_equal`.
#1068) `mapreduce(f, +, A; dims=2)` returns `NaN` for a row that stores every entry when `f(0)` is not finite, where dense input gives the sum of the row. The same kernel serves `dims=1` of an adjoint or transpose and a column view. ```julia using SparseArrays f = x -> x == 0 ? Inf : x D = [1.0 2.0; 0.0 3.0] mapreduce(f, +, sparse(D); dims=2), mapreduce(f, +, D; dims=2) ``` **Before** (`main`, and Julia 1.13) ``` ([NaN; Inf;;], [3.0; Inf;;]) ``` **After** ``` ([3.0; Inf;;], [3.0; Inf;;]) ``` The `+` method of `_mapreducecols!` counts the entries each row does not store and then adds `f(0) * count` to every row. For a row with a count of zero that is `Inf * 0`, which is `NaN`. Such a row has no `f(0)` to add, so it is now skipped. The path is only reached when the matrix has some unstored entry and `f(0)` is not zero; a matrix stored in full already avoided evaluating `f(0)`. The `dims=1` kernel and the whole-array reduction go through `_mapreducezeros`, which returns early on a count of zero, and a sweep of `mapreduce(f, +, ...)` and `sum(f, ...)` over `dims` in `1`, `2`, `(1, 2)` and `:`, with and without `sparse=true`, on a matrix, its adjoint and a column view agrees with dense on this branch. Split from #1022. A candidate for backport: a wrong result, with no change to any type or signature. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 763cc76) Backport adjustment: applied to src/sparsematrix.jl and test/sparsematrix_ops.jl (for src/reductions.jl and test/reductions.jl).
…1058) `A[I, J]` throws a `MethodError` when the row range `I` is a `StepRangeLen`, whatever its step, while the same index works on a dense matrix. **Before** ```julia julia> A = sparse([1.0 2 0; 0 3 4]); julia> A[StepRangeLen(2, -1, 2), :] ERROR: MethodError: no method matching getindex_traverse_col(::StepRangeLen{Int64, Int64, Int64, Int64}, ::Int64, ::Int64) julia> A[StepRangeLen(1, 0, 3), :] ERROR: MethodError: no method matching getindex_traverse_col(::StepRangeLen{Int64, Int64, Int64, Int64}, ::Int64, ::Int64) ``` **After** ```julia julia> A[StepRangeLen(2, -1, 2), :] == Matrix(A)[StepRangeLen(2, -1, 2), :] true julia> A[StepRangeLen(1, 0, 3), :] 3×3 SparseMatrixCSC{Float64, Int64} with 6 stored entries: 1.0 2.0 ⋅ 1.0 2.0 ⋅ 1.0 2.0 ⋅ ``` `getindex(A, I::AbstractRange, J::AbstractVector)` accepts any range, but `getindex_traverse_col`, which picks the direction to walk a column in, had methods only for `AbstractUnitRange` and `StepRange`. Its `StepRange` method only reads `step(I)`, so it now takes `AbstractRange`. A `StepRangeLen` can also have a zero step, which `rangesearch` divides by, so a range whose step is zero or not an integer is collected and indexed as a vector. Touches dispatch: `getindex_traverse_col(::StepRange, ...)` (internal) is widened to `AbstractRange`. Split from #1021. A bug fix with no change to a result type, so a backport candidate. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 85bbcd0) Backport adjustment: applied by hand to src/sparsematrix.jl (for src/indexing.jl), whose `getindex` does not lower its indices with `_lower_indices` on release-1.13; the test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `mismatch`.
A unit range of `Bool` as the row index with a scalar column was read as
a range of row numbers, where a dense matrix reads it as a mask.
`false:true` so selected rows 0 and 1 of the column instead of row 2,
and `false:false` returned one element instead of none.
**Before**
```julia
julia> A = sparse([1.0 2 0; 0 3 4]);
julia> A[false:true, 2]
2-element SparseVector{Float64, Int64} with 1 stored entry:
[2] = 2.0
julia> Array(A)[false:true, 2]
1-element Vector{Float64}:
3.0
julia> sparse(fill(5.0, 1, 1))[false:false, 1]
1-element SparseVector{Float64, Int64} with 0 stored entries
```
**After**
```julia
julia> A[false:true, 2]
1-element SparseVector{Float64, Int64} with 1 stored entry:
[1] = 3.0
julia> sparse(fill(5.0, 1, 1))[false:false, 1]
0-element SparseVector{Float64, Int64} with 0 stored entries
```
`getindex(::AbstractSparseMatrixCSC, ::AbstractUnitRange, ::Integer)`
slices the column by `first(I)` and `last(I)`, and a `Bool` range
reached it because it is more specific than the `AbstractVector{Bool}`
mask method. The new method collects the range and goes through the mask
path, as the existing `AbstractRange{Bool}` method does for a vector of
columns.
Touches dispatch: adds `getindex(::AbstractSparseMatrixCSC,
::AbstractUnitRange{Bool}, ::Integer)`.
Split from #1021. A bug fix that adds no behaviour beyond matching
dense, so a backport candidate.
Written by Claude Code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6a4f7a4)
Backport adjustment: the method goes next to the logical `getindex` methods in src/sparsematrix.jl (for src/indexing.jl); the test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `mismatch`. On release-1.13 `M[false:true, 2]` returned a wrong result, a vector of length 2, without throwing.
…#1071) In-place `map!` and `broadcast!` with a function that is not zero at the zeros store every entry of the destination. They densified it first and only then hit failures that could have been found beforehand, so a throw left the destination corrupt: column pointers half rewritten, or a full pattern with uninitialized values. ```julia using SparseArrays C = spzeros(Float64, Int8, 12, 12) try; C .= 1.0; catch e; println(typeof(e)); end println(SparseArrays.getcolptr(C)) Ci = sparse([1 0; 0 2]) try; Ci .= Ci .+ 0.5; catch e; println(typeof(e)); end println(nnz(Ci), " ", Ci == [1 0; 0 2]) ``` **Before** ``` InexactError Int8[1, 13, 25, 37, 49, 61, 73, 85, 97, 109, 121, 1, 1] InexactError 4 false ``` **After** ``` InexactError Int8[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] InexactError 2 true ``` `_densestructure!` resized the buffers and wrote the column pointers before the last one, `m * n + 1`, turned out not to fit the index type, and the fill kernels called it before `fill!` converted the fill value to the element type. `_checkdensestructure` now holds what `_densestructure!` checks before it writes anything (the fixed-pattern check, and for a matrix that the last column pointer converts), and `_guardfill` runs it and the conversion of the fill value ahead of `_map_notzeropres!`, `_broadcast_notzeropres!` and the zero-argument `C .= f.()` path. The conversion is skipped for an empty destination, which stores nothing. Fixed destinations throw the same errors as before. `map!(cos, C, A)` and `C .= A .+ 1.0` at 10^6 entries take the same time and allocations as on `main`. Left out: an `f` that throws part-way through the kernel, after the destination is densified, still leaves it with values that are not its own. That is a follow-up stacked on this branch. Split from #1022. A backport candidate: the destination is corrupt after the throw. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 4cbae1e) Backport adjustment: release-1.13 densifies with its own `_densestructure!` in src/higherorderfns.jl (without the fixed-pattern checks of main), so `_checkdensestructure` and `_guardfill` are defined there and check only the index type and the fill value. `S .= x` with a scalar goes through `fill!` and `_fillnonzero!` on release-1.13, not through `_densestructure!` as on main, so `_fillnonzero!` gets the same checks before it resizes anything. The test is a new testset in test/higherorderfns.jl that compares with `==`.
#1061) `fill!` on a view of a sparse matrix, which is what `S[I, J] .= x` calls, throws when `I` or `J` is a decreasing range. ```julia S = sparse([1.0 2 0; 0 3 4; 5 0 6]) S[3:-1:1, 1] .= 0.0; println(Matrix(S), " nnz=", nnz(S)) S[:, 3:-2:1] .= 2.0; println(Matrix(S), " nnz=", nnz(S)) fill!(view(S, 3:-1:2, 3:-1:1), 7.0); println(Matrix(S), " nnz=", nnz(S)) ``` **Before**, each of the three statements: ``` CanonicalIndexError: setindex! not defined for StepRange{Int64, Int64} ``` **After** ``` [0.0 2.0 0.0; 0.0 3.0 4.0; 0.0 0.0 6.0] nnz=6 [2.0 2.0 2.0; 2.0 3.0 2.0; 2.0 0.0 2.0] nnz=8 [2.0 2.0 2.0; 7.0 7.0 7.0; 7.0 7.0 7.0] nnz=9 ``` The kernels behind this method need each index in increasing order without repeats, and the method got there with `sort!(unique(I))`. `unique` of a range returns the range, which cannot be sorted in place. The new internal helper `_strictlysorted` returns an integer or a unit range as it is, reverses a range with a negative step, and sorts a copy of any other vector that is not already strictly increasing, so each index keeps its type where it can and the caller stays type-stable. Split from #1021. A bug fix with no change to results that worked before, so a candidate for backporting. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 49515df) Backport adjustment: applied by hand to the `fill!` method in src/sparsematrix.jl (for src/indexing.jl), which on release-1.13 has no fixed-pattern branch; the test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `mismatch`.
Two-index `setindex!(A, V, I, J)` reads its index vectors while it
rewrites the storage, so an index vector that is one of the matrix's own
buffers changes under it. A row index that is `rowvals(S)` throws a
`BoundsError` and leaves the matrix corrupt; as a column index it
silently assigns the wrong entries.
```julia
using SparseArrays, LinearAlgebra
S = sparse(Diagonal([1.0, 2, 3, 4]))
try
S[rowvals(S), 2] = [10.0, 20, 30, 40]
println(Matrix(S))
catch e
println(typeof(e), ": length(rowvals(S)) = ", length(rowvals(S)), ", colptr = ", SparseArrays.getcolptr(S))
end
S = sparse(Diagonal([1.0, 2, 3, 4]))
S[2, rowvals(S)] = [10.0, 20, 30, 40]
println(Matrix(S))
```
**Before**
```
BoundsError: length(rowvals(S)) = 8, colptr = [1, 2, 3, 4, 5]
[1.0 0.0 0.0 0.0; 10.0 20.0 0.0 0.0; 0.0 0.0 3.0 0.0; 0.0 0.0 0.0 4.0]
```
**After**
```
[1.0 10.0 0.0 0.0; 0.0 20.0 0.0 0.0; 0.0 30.0 3.0 0.0; 0.0 40.0 0.0 4.0]
[1.0 0.0 0.0 0.0; 10.0 20.0 30.0 40.0; 0.0 0.0 3.0 0.0; 0.0 0.0 0.0 4.0]
```
The kernel resizes `rowval` and `nzval` and shifts their entries as it
inserts, and keeps indexing `I` and `J` throughout. The linear-index
method already passes its indices through `_unalias_setindex`, which
copies a vector only when `Base.mightalias` reports that it shares
memory with `nzval`, `rowval` or `colptr`; the two-index method now does
the same for `I` and `J` once they are lowered. An index that is not one
of the buffers is returned as it is, without an allocation.
Split from #1021. This is a backport candidate, since the failure
corrupts the matrix.
Written by Claude Code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 3d6d696)
Backport adjustment: applied by hand to src/sparsematrix.jl (for src/indexing.jl), after the index lowering that release-1.13 writes out instead of `_lower_indices`; the test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `mismatch`.
`setindex!(A, x::AbstractArray, I::AbstractVector{<:Integer})` with
linear indices converts each value as it stores it, after it has started
to rewrite the pattern. A value that does not convert to `eltype(A)`
throws part of the way through and leaves a matrix whose `colptr` no
longer matches its buffers.
```julia
using SparseArrays
S = sparse([1 0 0; 0 2 0; 0 0 3])
try
S[[2, 4, 6]] = [7.0, 8.0, 8.5]
catch e
showerror(stdout, e); println()
end
@show SparseArrays.getcolptr(S) length(rowvals(S)) length(nonzeros(S))
```
**Before**
```
InexactError: Int64(8.5)
SparseArrays.getcolptr(S) = [1, 3, 3, 4]
length(rowvals(S)) = 6
length(nonzeros(S)) = 6
```
**After**
```
InexactError: Int64(8.5)
SparseArrays.getcolptr(S) = [1, 2, 3, 4]
length(rowvals(S)) = 3
length(nonzeros(S)) = 3
```
The method grows `rowval` and `nzval` and shifts entries while it walks
the sorted indices, and fixes up `colptr` only at the end. It now calls
`_checkconvertible(eltype(A), x)` after the bounds check and before the
first write, the check `copyto!` already makes; the loop compiles away
when `eltype(x) <: eltype(A)`. `_checkconvertible` is widened from
`AbstractVector` to `AbstractArray`, since `x` may be any array with the
right number of elements.
Touches dispatch: the internal helper `_checkconvertible` now accepts
`AbstractArray`; no public method changes.
Split from #1021.
Backport candidate: a throwing assignment corrupts the matrix.
Written by Claude Code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b93de0b)
Backport adjustment: release-1.13 has no `_checkconvertible` (main added it for `copyto!` in a PR that is not labelled for backport), so the helper is defined next to the linear-index `setindex!` in src/sparsematrix.jl, with the `AbstractArray` signature this PR gives it. The test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `exact_equal`.
`opnorm(A::AbstractSparseMatrixCSC, p)` accepts any `p` when a dimension
of `A` is zero, where the dense method throws: only the 1, 2 and Inf
operator norms exist.
```julia
using SparseArrays, LinearAlgebra
for A in (spzeros(0, 0), spzeros(3, 0), spzeros(1, 4), spzeros(4, 1), sparse([1.0 2; 3 4]))
r = try opnorm(A, 3) catch e; typeof(e) end
d = try opnorm(Matrix(A), 3) catch e; typeof(e) end
println(rpad(string("opnorm(", size(A), " sparse, 3) = ", r), 44), "dense: ", d)
end
```
**Before**
```
opnorm((0, 0) sparse, 3) = 0.0 dense: ArgumentError
opnorm((3, 0) sparse, 3) = 0.0 dense: ArgumentError
opnorm((1, 4) sparse, 3) = ArgumentError dense: ArgumentError
opnorm((4, 1) sparse, 3) = ArgumentError dense: ArgumentError
opnorm((2, 2) sparse, 3) = ArgumentError dense: ArgumentError
```
**After**
```
opnorm((0, 0) sparse, 3) = ArgumentError dense: ArgumentError
opnorm((3, 0) sparse, 3) = ArgumentError dense: ArgumentError
opnorm((1, 4) sparse, 3) = ArgumentError dense: ArgumentError
opnorm((4, 1) sparse, 3) = ArgumentError dense: ArgumentError
opnorm((2, 2) sparse, 3) = ArgumentError dense: ArgumentError
```
The method returned zero for an empty matrix before looking at `p`, and
rejected an invalid `p` only by falling through every branch to a
trailing `throw`. It now checks `p in (1, 2, Inf)` first, with the same
`ArgumentError`, so the single-row and single-column branches and the
last `elseif` no longer test `p` again and the trailing `throw` goes
away. Results for a valid `p` are unchanged.
Split from #1023. A bug fix, so a backport candidate.
Written by Claude Code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 7bee547)
Backport adjustment: release-1.13's `opnorm` has no 2-norm estimate for a general sparse matrix (main added `opnorm2est` in a PR not labelled for backport), so only the up-front check of `p` is ported, with release-1.13's error message; the rest of the function is unchanged. The test goes to test/linalg.jl unguarded.
… does (#1072) `findmin` and `findmax` of a sparse matrix that holds a `missing` throw, or return a `NaN` that dense ranks below the `missing`. **Before** ```julia julia> findmin(sparse([1 missing; 0 2])) ERROR: TypeError: non-boolean (Missing) used in boolean context julia> findmax(sparse([NaN missing; 0.0 2.0])) (NaN, CartesianIndex(1, 1)) ``` **After** ```julia julia> findmin(sparse([1 missing; 0 2])) # as findmin([1 missing; 0 2]) (missing, CartesianIndex(1, 2)) julia> findmax(sparse([NaN missing; 0.0 2.0])) (missing, CartesianIndex(1, 2)) ``` The two comparison helpers of `_findr` spelled out the `NaN` rule with `==` and `!=`, which return `missing` next to a `missing`. They now use the replacement test of Base's dense `findmin` and `findmax`, `Base.isgreater(b, a)` and `isless(b, a)`, which are total: `missing` ranks above `NaN`, `NaN` above every number, and `-0.0` below `0.0`. Results for `NaN` and a stored `-0.0` are unchanged. A sweep of 240,000 comparisons against dense over random matrices up to 4×4 drawn from `0.0`, `-0.0`, `±1`, `±Inf`, `NaN` and `missing`, with a random subset of the zeros stored, finds no mismatch. | 10^4 × 10^4, 10^6 stored entries | `main` | branch | |---|---|---| | `findmin(A)` | 1.127 ms | 1.126 ms | | `findmax(A; dims=1)` | 1.795 ms | 1.795 ms | Split from #1022. A bug fix, so a backport candidate. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 0671810) Backport adjustment: applied to src/sparsematrix.jl and test/sparsematrix_ops.jl (for src/reductions.jl and test/reductions.jl).
`getindex(A, I)` with an index array `I` that is neither a vector nor a matrix takes the method for a matrix of linear indices, which sizes its result from `size(I, 1)` and `size(I, 2)` only. With three or more dimensions it writes past the end of the result's buffers; with a zero-dimensional `I` it returns a 1×1 sparse matrix. Sparse vectors go through the same method. ```julia using SparseArrays A = sparse([1.0 2 0; 0 3 4]); x = sparse([1, 2, 3]) show(x[fill(3)]); println() show(A[fill(2)]); println() show(A[reshape([1, 3, 4, 6, 1, 3, 4, 6], 2, 2, 2)]); println() ``` **Before** ``` sparse([1], [1], [3], 1, 1) sparse(Int64[], Int64[], Float64[], 1, 1) sparse([1, 2, 1, 2], [1, 1, 2, 2], [Fault at memory address: 0x30000722250b5 [4140366] signal 11 (1): Segmentation fault ``` **After** ``` fill(3) fill(0.0) [1.0 3.0; 2.0 4.0;;; 1.0 3.0; 2.0 4.0] ``` There is no sparse array of such a shape, so `getindex(A::AbstractSparseMatrixCSC, I::AbstractArray)` now hands any `I` with `ndims(I) != 2` to `_getindex_dense`, which checks the bounds and fills an `Array` of `size(I)` with one binary search per element, as indexing a dense array does. The elements of `I` may be integers or `CartesianIndex`es, and anything else (a float, a `Bool`) throws in `to_index` as it does for a dense array. A stored entry is copied, and the implicit zero comes from `_densezero`, so the eltype is that of `Array(A)` and no `zero` is needed when only stored entries are selected. The result being dense, its length need not fit the index type. For a sparse vector, `getindex(x, I::AbstractArray)` checks the bounds against `x` first, so that the `BoundsError` names the vector and not the one-column matrix it is indexed through. Split from #1021. Touches dispatch: no new `getindex` signature, but index arrays with `ndims != 2` now return an `Array` instead of a `SparseMatrixCSC`. This is a bug fix for an out-of-bounds write, so a backport candidate. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit c463cce) Backport adjustment: ported to src/sparsematrix.jl (for src/indexing.jl). release-1.13 has no `_densezero` (the zero of an element type whose zero is of another type, from main), so an unstored entry is `zero(Tv)` and the result is always an `Array{Tv}`; the test for such an element type is dropped. On release-1.13 a 3-d index array segfaulted in the matrix kernel. The tests go to test/sparsematrix_constructors_indexing.jl unguarded.
…NaN` next to a zero (#1065) `prod` of a sparse matrix with an unstored entry returns zero without looking at the stored ones, but a stored `Inf` or `NaN` times zero is `NaN`, which is what the dense `prod` gives. **Before** (`main`, and Julia 1.13) ```julia julia> prod(sparse([0.0 Inf; 1.0 2.0])), prod([0.0 Inf; 1.0 2.0]) (0.0, NaN) julia> prod(sparse([0.0 NaN; 1.0 2.0]); dims=(1, 2)) 1×1 Matrix{Float64}: 0.0 ``` **After** ```julia julia> prod(sparse([0.0 Inf; 1.0 2.0])), prod([0.0 Inf; 1.0 2.0]) (NaN, NaN) julia> prod(sparse([0.0 NaN; 1.0 2.0]); dims=(1, 2)) 1×1 Matrix{Float64}: NaN ``` The `*` kernel of `Base._mapreduce` for a `SparseMatrixCSC` or a column view of one short-circuits when the unstored entries multiply to zero. That is kept only where `f` returns an integer (`Base.promote_op(f, T) <: Integer`), which has no value that survives a zero. Otherwise the zero is multiplied by the product of the stored entries. That product can overflow to `Inf` on its own and turn the zero into a `NaN` that the dense fold never sees, so when the result is nonzero the stored entries are folded into the zero one at a time instead: `prod(sparse([0.0 1e200; 1e200 1.0]))` is still `0.0`. The cost is that `prod` of a float matrix with unstored entries now reads the stored entries, O(nnz) where it was O(1). For a 2000×2000 `Float64` matrix with 10^6 stored entries: | | time | allocations | |---|---|---| | `main` | 0.00006 ms | 0 bytes | | this PR | 0.096 ms | 0 bytes | Left out: `prod` of a sparse vector and `prod(A; dims=1)` or `dims=2` go through other kernels and still give `NaN` where the stored entries of the vector or slice overflow next to a zero, as on `main` (`prod(sparsevec([0.0, 1e200, 1e200]))` is `NaN`, dense gives `0.0`). Split from #1022. This is a wrong result on released Julia, so it is a backport candidate. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 775677b) Backport adjustment: applied to src/sparsematrix.jl and test/sparsematrix_ops.jl (for src/reductions.jl and test/reductions.jl).
…ense destination (#1074) `mul!(C, A, B, α, β)` with `A` a `Symmetric` or `Hermitian` sparse matrix and a dense `C` mishandles `α`: a `false` `α` is not a strong zero, so a `NaN` in `B` reaches `C`; `α` is multiplied on the wrong side for a non-commutative eltype; and `α` and `β` are converted to `eltype(C)`, which throws for a result that the destination can hold. ```julia using SparseArrays, LinearAlgebra include(joinpath(Sys.BINDIR, "..", "share", "julia", "test", "testhelpers", "Quaternions.jl")) using .Quaternions: Quaternion mul!(ones(2, 2), Symmetric(sparse([1.0 2; 3 4])), [NaN 1; 1 1], false, 1.0) H = sparse([Quaternion(1.0, 0, 0, 0) Quaternion(0.0, 1, 2, 3); Quaternion(0.0, 0, 0, 0) Quaternion(2.0, 0, 0, 0)]) B = [Quaternion(0.0, 0, 0, 1) Quaternion(1.0, 1, 0, 0); Quaternion(1.0, 0, 0, 0) Quaternion(0.0, 0, 1, 0)] j = Quaternion(0.0, 0, 1, 0) mul!(zeros(eltype(B), 2, 2), Hermitian(H), B, j, false) == Matrix(Hermitian(H)) * B * j mul!(zeros(Int, 2, 2), Symmetric(sparse([1 2; 3 4])), [2 2; 4 4], 0.5, 1) ``` **Before** ``` [NaN 1.0; NaN 1.0] false ERROR: InexactError: Int64(0.5) ``` **After** ``` [1.0 1.0; 1.0 1.0] true [5 5; 10 10] ``` `spdensemul!` passed `T(alpha), T(beta)` with `T = eltype(C)`, and `_symherm_mul!` computed `C[col,k] += α * sumcol`, with `α` on the left, and always ran the loop. Now the scalars are passed as they are, `_symherm_mul!` multiplies `α` on the right in both places (`B[col,k] * α` and `sumcol * α`), as `A*B*α` does, skips the multiplication for a `Bool` `α`, and returns after scaling `C` by `β` when `α === false`. It also specializes on its three function arguments. `mul!(y, Symmetric(A), b)` real and `mul!(yc, Hermitian(Ac), bc, 2.0, 3.0)` complex, n = 100 000 with 9·10^5 stored entries, minimum of 25 runs: | | `main` | this PR | |---|---|---| | real, 3-arg | 0.68 ms / 0 bytes | 0.67 ms / 0 bytes | | complex, 5-arg | 0.82 ms / 0 bytes | 0.76 ms / 0 bytes | Split from #1023. The first two cases are wrong results, so this is a backport candidate. It touches no dispatch: the signature of the internal `_symherm_mul!` changes, the `mul!` methods do not. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 6324de6) Backport adjustment: ported by hand to src/linalg.jl, where release-1.13 has the kernel as `_mul!` with an `nzrang` argument (main renamed it `_symherm_mul!` in src/matmul.jl). The test goes to test/linalg.jl unguarded, loads Base's Quaternions test helper directly and uses literal values in place of the `fixturedense` fixture.
…1079) `mul!` of a sparse and a dense matrix into a dense destination that shares storage with the dense operand silently returns a wrong product: the kernels read the operand while they write the destination. Dense `mul!` throws for the same call. **Before** ```julia julia> S = sparse([1.0 2 0; 0 3 4; 5 0 6]); D = [1.0 2 3; 4 5 6; 7 8 10]; julia> C = copy(D); mul!(C, S, C) == S * D false julia> C = copy(D); mul!(C, S, UpperTriangular(C)) == S * UpperTriangular(D) false julia> C = copy(D); mul!(C, D, D) ERROR: ArgumentError: output matrix must not be aliased with input matrix ``` **After** ```julia julia> C = copy(D); mul!(C, S, C) ERROR: ArgumentError: output matrix must not be aliased with input matrix julia> C = copy(D); mul!(C, S, UpperTriangular(C)) ERROR: ArgumentError: output matrix must not be aliased with input matrix julia> C == D true ``` `_checknoalias(C, B, α)` runs after the size checks of the kernels that write a dense destination from a dense operand (`_spmatmul!`, `_At_or_Ac_mul_B!`, `_A_mul_Bt_or_Bc!`, `_trimatmul!`, `_mattrimul!`, `_symherm_mul!`, `_A_mul_symherm!`), so a shape error is still reported first and the destination is untouched. It strips the operand's adjoint, transpose, symmetric, triangular and Hessenberg wrappers and compares with `Base.mightalias`, so a triangle or an overlapping view of the destination is caught and non-overlapping views of one array are not. A `false` `α` never reads the operand and skips the check. Behaviour to review: the check also applies to a vector destination, `mul!(x, S, x)`, and to the triangular kernels unless the destination is the operand itself, which is the in-place `lmul!`/`rmul!`. Dense `mul!` returns the right product for some aliased calls that now throw, such as `mul!(C, A, view(C, :, :))`. Through a symmetric or Hermitian sparse factor on the left a `false` `α` is converted to the eltype before it reaches the kernel, so that aliased call throws too. Left out: `mul!(C, UpperTriangular(C), S)`, with a dense triangle of the destination on the left, reaches LinearAlgebra's dense triangular kernel and is wrong for a dense `S` too. The check is one comparison of pointers per call. Minimum of 25 runs, ms / bytes: | | `main` | this PR | |---|---|---| | `mul!(y, A, b)`, n = 100 000, 9·10^5 stored entries | 1.29 / 0 | 1.28 / 0 | | `mul!(C, D, S)`, 2000×2000 dense, 1% sparse | 14.84 / 0 | 14.82 / 0 | Split from #1023. This is a fix for a silently wrong result and a backport candidate. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit ead43a0) Backport adjustment: ported by hand to the product kernels in src/linalg.jl (src/matmul.jl on main), with release-1.13's `DenseViewWrappers` for main's `MatrixWrappers`. release-1.13 has no kernels of its own for a dense matrix times a symmetric/Hermitian sparse matrix (`_A_mul_symherm!`) or times a triangular sparse matrix (`_mattrimul!`); those products go through LinearAlgebra's generic code, so the check is not added there and their two test cases are dropped. The test goes to test/linalg.jl unguarded.
…oduct `x .* y'` (#1077) `x .* y'` for sparse vectors drops a non-finite product with an unstored zero: `Inf * 0` and `NaN * 0` are `NaN` in the dense result and an unstored zero in the sparse one. `kron(x, y')` goes through the same broadcast and has the same defect. **Before** ```julia julia> x, y = sparsevec([Inf, 0.0]), sparsevec([0.0, 1.0]); julia> Array(x .* y') 2×2 Matrix{Float64}: 0.0 Inf 0.0 0.0 julia> Array(x) .* Array(y)' 2×2 Matrix{Float64}: NaN Inf 0.0 0.0 ``` **After** ```julia julia> Array(x .* y') 2×2 Matrix{Float64}: NaN Inf 0.0 0.0 ``` `Array(kron(x, y'))` changes in the same way and now equals `kron(Array(x), Array(y)')`. The outer-product shortcut `_outer` multiplies stored entries with stored entries alone, which is the product only when every stored entry times a zero is a zero. `_copy(*, x, y')` now checks that with `_outerkeepszeros`, an O(nnz) pass over both vectors, and otherwise takes the general broadcast kernel. Both branches return the same `SparseMatrixCSC` type, so the result stays inferable. `x .* y'` with 10^3 stored `Float64` entries in each vector of length 10^4 (10^6 stored results), minimum of 20 runs on a shared machine: | | time | allocations | |---|---|---| | `main` | 2.87 ms | 9 (16087432 bytes) | | this PR | 2.77 ms | 9 (16087432 bytes) | Left out: `x .* y'` for an eltype without `zero`, such as a vector of matrices, throws a `MethodError` as it does on `main`. Split from #1022. Backport candidate: a wrong result, also on Julia 1.13. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 3d2e6df) Backport adjustment: applied by hand to src/higherorderfns.jl, where the outer product dispatches on `SparseVectorUnion` on release-1.13. release-1.13 has no `_densezero`, so the zeros are `zero(eltype(x))` and `zero(eltype(y))`, and `wrapperop` is called qualified. The test is a new testset in test/higherorderfns.jl, unguarded.
…ap` and `broadcast` kernels (#1087) `map` and `broadcast!` of a function that is not zero at zero compare each result with the fill value `f(0)` using `!=`. That throws when the comparison is `missing`, and it leaves the fill value in place of a result that equals it but has another type. **Before** ```julia julia> A = sparse([1.0 0; 0 2.0]); julia> map(x -> x == 0 ? 1.0 : missing, A) ERROR: TypeError: non-boolean (Missing) used in boolean context julia> typeof.(map(x -> x == 0 ? 1 : 1.0, A)) 2×2 Matrix{DataType}: Int64 Int64 Int64 Int64 ``` **After** ```julia julia> map(x -> x == 0 ? 1.0 : missing, A) 2×2 SparseMatrixCSC{Union{Missing, Float64}, Int64} with 4 stored entries: missing 1.0 1.0 missing julia> typeof.(map(x -> x == 0 ? 1 : 1.0, A)) # as for Array(A) 2×2 Matrix{DataType}: Float64 Int64 Int64 Float64 ``` The `_map_notzeropres!` and `_broadcast_notzeropres!` kernels (one, two and N arguments) fill the destination with `f(0)` and then skip the store of a result that equals it. They now skip only a result that is identical to it (`!==`/`===`), which is always a `Bool` and is the only case in which the store is redundant. Time at 10^6 stored entries (4000×4000, `x -> x + 1.0` and `(x, y) -> x + y + 1.0`, `Float64`, minimum of 15 runs): the one-argument kernel is unchanged (`map!` 13.0 ms on both), the two-argument kernel goes from 18.6 ms to 21.1 ms in `map!` and from 36–39 ms to 38–40 ms in `map`. Allocations are unchanged. Split from #1022. Backport candidate: a bug fix, with no new method and no change of a result type that was computed correctly before. Written by Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 079bcc1) Backport adjustment: the test is a new testset in test/higherorderfns.jl, with a literal matrix in place of the `fixturepair` fixture.
Member
|
Wow, thanks! |
Member
Author
|
Nothing to thank for, I just gave instructions. |
Member
|
Given the extensive nature of these fixes, I wonder if we should do a pkgeval run. |
This branch has not been deployed
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.
Backports of the merged PRs labelled
backport 1.13, cherry-picked with-xin the order they were merged onmain, one per push, with CI checked before the next. Ticked PRs are included and passed CI.copy(::UmfpackLU)#750isonefor sparse matrices with stored zeros #764Boolmatrix fromspdiagmwithout diagonals #853setindex!#884big(0)in scalarsetindex!#891repeat(A::AbstractSparseMatrixCSC, m, n)#919rfnfrom the vectorsprand(n, p, rfn)as documented #925fill!on an empty view of a sparse matrix #926Arrayfrommaximum,minimumandextremaalong a dimension of an empty sparse array #928SparseMatrixCSCfrom\with a sparse right-hand side and fromsparse(F.L)#933sum,prodandmapreduceas for dense #932CHOLMOD.Factorcomponents andQRSparsepermutations from property access #930\andfactorizeonly for floating-point eltypes #921Intindex fromfindmin/findmaxonSparseVectorand applyfto the empty case #922FixedSparseCSCwith unsorted rows and accumulate along its dimensions without touching its pattern #964ldiv!with anUmfpackLUorCHOLMOD.Factor#942FixedSparseVectorwith an index vector #967kinistriuandistrilof sparse matrices #965issymmetric,ishermitian, unary-,floatandcomplexof fixed-pattern arrays #960Introw sentinels in the sparsemapandbroadcastkernels #970setindex!kernel and check the value's shape #946SingularExceptionfrom sparseD \ AandA / Dand share the diagonal scaling loops #944dropstored!#1052setindex!on a Boolean mask of the wrong length #1053mapreduce(f, +, A; dims=2)addingf(0)to a row stored in full #1068StepRangeLenrow ranges ingetindexof a sparse matrix #1058CartesianIndex{1}as a row or column index ingetindex#1062 (not applicable to release-1.13, see below)Boolunit range as a mask ingetindex(A, I, j)#1057map!andbroadcast!#1071fill!on a view of a sparse matrix indexed by a decreasing range #1061setindex!#1054setindex!writes #1060pin sparseopnormbefore the empty-matrix shortcut #1064missinginfindminandfindmaxof a sparse matrix as dense does #1072getindexwith an N-d index array #1059NaNfromprodof a sparse matrix that stores anInforNaNnext to a zero #1065alphainmul!of a symmetric/Hermitian sparse matrix into a dense destination #1074mul!when a dense destination aliases the dense operand #1079x .* y'#1077mapandbroadcastkernels #1087Adjustments
mainreorganizedsrc/andtest/by area in #872 and #874, after the 1.13 branch point. From #884 on, hunks forsrc/indexing.jl,src/concatenation.jl,src/matmul.jl,src/reductions.jlandsrc/constructors.jlgo to the release branch file that holds the same code (mostlysrc/sparsematrix.jl), and tests go to the corresponding release test file. Tests thatmainguards with@static if COMPREHENSIVEand checks withmismatchare added unguarded and compared with==, sincerelease-1.13has neither. Each commit message ends with aBackport adjustment:line when more than that was needed. Nontrivial ones:Boolmatrix fromspdiagmwithout diagonals #853:test/ambiguous.jlkeepsunbound_args=(; broken=true).release-1.13has further unbound type parameters (_absspvec_hcat,_getmode) thatmainfixed in PRs not backported here; the code fix itself is unchanged.big(0)in scalarsetindex!#891: the_splice_sourcehunk and itscopyto!-into-a-view test are dropped. That code comes from Splice sparse sources into views of sparse arrays incopyto!#799, which is not onrelease-1.13.Arrayfrommaximum,minimumandextremaalong a dimension of an empty sparse array #928: thereducedim_initmethods are defined forAbstractSparseMatrixCSCandAbstractCompressedVectoronly. Onrelease-1.13, reductions over adjoints and views of sparse matrices don't go through the sparse kernels (Add asparsekeyword to reductions along a dimension and reduce views, adjoints and column subsets through the sparse kernels #798 isn't backported), so the adjoint and view cases and thesparse = truekeyword test are dropped.SparseMatrixCSCfrom\with a sparse right-hand side and fromsparse(F.L)#933: the new tests assert the index typeInt, sinceTiisn't bound in that part oftest/cholmod.jlonrelease-1.13. TheF' \ Bcase is dropped: onrelease-1.13, solving with the adjoint of a CHOLMOD factor and a sparse right-hand side still returns aCHOLMOD.Sparse.mainchanged that in a PR that isn't labelled for backport.CHOLMOD.Factorcomponents andQRSparsepermutations from property access #930: theAdjointQRSparse(lq) hunk and its test are dropped, because sparselqisn't onrelease-1.13. The CHOLMOD tests assert the index typeInt, as for Return aSparseMatrixCSCfrom\with a sparse right-hand side and fromsparse(F.L)#933.\andfactorizeonly for floating-point eltypes #921: the reroute toluapplies to a dense right-hand side only. Onrelease-1.13, UMFPACK can't solve with a sparse right-hand side;mainadded that in a later restructuring of\. A sparse right-hand side therefore keeps the previous Hermitian path, which works there, and every sparse case behaves as before. The test checks only values for a sparse right-hand side. It skips the complex transpose case, which fails onrelease-1.13both before and after this change. It drops@inferred, because\isn't inferred for these eltypes onrelease-1.13.ldiv!with anUmfpackLUorCHOLMOD.Factor#942:release-1.13has noCholmodWSand no workspace keyword on CHOLMODldiv!, so thedeepcopyhunk and the workspace parts of the test are dropped. Where an aliased right-hand side is copied,solve!/ldiv!is called again with the copy instead of rebindingb. Inrelease-1.13's UMFPACK matrix kernelbis a column view, so rebinding it to aVectormade it type-unstable and broke the zero-allocation test intest/umfpack.jl.issymmetric,ishermitian, unary-,floatandcomplexof fixed-pattern arrays #960: thefactorizeand\checks on a fixed-pattern Hermitian matrix are dropped. With this fixishermitianno longer throws, but onrelease-1.13the call then fails later, incopytrito!(it threw before this change too).mainfixed that later step in PRs that aren't labelled for backport. The test uses a localsame_patternhelper.setindex!kernel and check the value's shape #946: ported by hand. The fixed-pattern part (_setindex_fixed!) is dropped:release-1.13's linear-index kernel has no fixed-pattern branch and no_throwfixedinsert, both from later fixed-pattern work onmain. The walk over a sparse mask is defined forAbstractSparseMatrixCSC{Bool}and its transposes, sincerelease-1.13has noSparseMatrixCSCOrColumnSubset; views of a sparse mask take thefindallpath. The fixed-pattern test cases are dropped accordingly.dropstored!#1052: ported by hand.release-1.13has no_lower_indiceshelper (from Lower indices withto_indicesin sparsegetindex#857) and no_checkpatternwritable, so the lowering (to_indices,checkbounds,ensure_indexable) is written out indropstored!.Boolunit range as a mask ingetindex(A, I, j)#1057: onrelease-1.13,M[false:true, 2]returned a wrong result (a vector of length 2) without throwing. The method goes next to the logicalgetindexmethods insrc/sparsematrix.jl.map!andbroadcast!#1071:release-1.13densifies with its own_densestructure!, withoutmain's fixed-pattern checks, so_checkdensestructureand_guardfillcheck only the index type and the fill value.S .= xwith a scalar goes throughfill!and_fillnonzero!onrelease-1.13, not_densestructure!, so_fillnonzero!gets the same checks before it resizes anything.pin sparseopnormbefore the empty-matrix shortcut #1064:release-1.13'sopnormhas no 2-norm estimate for a general sparse matrix;mainaddedopnorm2estin a PR that isn't labelled for backport. Only the up-front check ofpis ported, withrelease-1.13's error message.getindexwith an N-d index array #1059: onrelease-1.13, a 3-d index array segfaulted in the matrix kernel.release-1.13has no_densezero(for an element type whose zero is of another type), so an unstored entry iszero(Tv), the result is always anArray{Tv}, and the test for such an element type is dropped.alphainmul!of a symmetric/Hermitian sparse matrix into a dense destination #1074:release-1.13has the symmetric/Hermitian kernel as_mul!insrc/linalg.jl, whichmainrenamed_symherm_mul!and moved tosrc/matmul.jl. The test loads Base's Quaternions test helper directly and uses literal values in place of thefixturedensefixture.mul!when a dense destination aliases the dense operand #1079: the aliasing check goes intorelease-1.13's product kernels, which are insrc/linalg.jl.release-1.13has no kernels of its own for dense times symmetric/Hermitian sparse or dense times triangular sparse; those products go through LinearAlgebra's generic code. So those two aliased cases still don't throw there, and their test cases are dropped.Not included:
logabsdet. Fix soundness defects in the CHOLMOD, UMFPACK and SPQR wrappers #1029 isn't labelled for backport, and onrelease-1.13logabsdettakes no lock (neither doeslu!), so the code it fixes isn't there.CartesianIndex{1}as a row or column index ingetindex#1062: not applicable without Lower indices withto_indicesin sparsegetindex#857, which made sparsegetindexlower its indices withto_indicesand isn't labelled for backport. Onrelease-1.13, a vector ofCartesianIndex{1}as a row or column index throws aMethodErrorbefore reaching the code Accept a vector ofCartesianIndex{1}as a row or column index ingetindex#1062 changes. Supporting it there would mean adding newgetindexmethods.🤖 Generated with Claude Code
https://claude.ai/code/session_01AffnVrPcPNrYXezDCHacNT