From a309cfd8acca17bdf64c62c5a47b4dcfcca025c3 Mon Sep 17 00:00:00 2001 From: Andy Dienes <51664769+adienes@users.noreply.github.com> Date: Mon, 25 May 2026 12:05:01 -0400 Subject: [PATCH 1/7] sparsematrix: fix swaprows! storage moves (#721) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reported on discourse, mostly oneshot by codex 5.5 ---------------------------------------------------------------------------------------------------------------- Fix SparseMatrixCSC row swaps when exactly one swapped row is stored in a column. The j search range used a local i index as an absolute storage index, and the single-row move rotations were reversed. On master: ```julia julia> A = sparse([1.0 2.0 3.0; 0.0 0.0 0.0; 4.0 5.0 6.0]); Base.swaprows!(A, 1, 2); A 3×3 SparseMatrixCSC{Float64, Int64} with 6 stored entries: ⋅ 1.0 ⋅ 2.0 ⋅ 3.0 4.0 5.0 6.0 julia> B = sparse(reshape([1.0, 2.0, 3.0, 4.0, 0.0, 0.0], 6, 1)); Base.swaprows!(B, 2, 6); rowvals(B) 4-element Vector{Int64}: 1 4 6 3 ``` --------- Co-authored-by: OpenAI Codex (cherry picked from commit 85f7b741d739f49e69d707f90b7daf5a3de64e9f) --- src/sparsematrix.jl | 12 +++++------- test/sparsematrix_constructors_indexing.jl | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/sparsematrix.jl b/src/sparsematrix.jl index 6aadb355..73dd2ba8 100644 --- a/src/sparsematrix.jl +++ b/src/sparsematrix.jl @@ -4513,7 +4513,7 @@ function Base.swaprows!(A::AbstractSparseMatrixCSC, i, j) iidx = searchsortedfirst(@view(rows[rr]), i) has_i = iidx <= length(rr) && rows[rr[iidx]] == i - jrange = has_i ? (iidx:last(rr)) : rr + jrange = has_i ? (rr[iidx]:last(rr)) : rr jidx = searchsortedlast(@view(rows[jrange]), j) has_j = jidx != 0 && rows[jrange[jidx]] == j @@ -4527,18 +4527,16 @@ function Base.swaprows!(A::AbstractSparseMatrixCSC, i, j) # Update the rowval and then rotate both nonzeros # and the remaining rowvals into the correct place rows[rr[iidx]] = j - jidx == 0 && continue rotate_range = rr[iidx]:jrange[jidx] - circshift!(@view(vals[rotate_range]), 1) - circshift!(@view(rows[rotate_range]), 1) + circshift!(@view(vals[rotate_range]), -1) + circshift!(@view(rows[rotate_range]), -1) else # Same as i, but in the opposite direction @assert has_j rows[jrange[jidx]] = i - iidx > length(rr) && continue rotate_range = rr[iidx]:jrange[jidx] - circshift!(@view(vals[rotate_range]), -1) - circshift!(@view(rows[rotate_range]), -1) + circshift!(@view(vals[rotate_range]), 1) + circshift!(@view(rows[rotate_range]), 1) end end return nothing diff --git a/test/sparsematrix_constructors_indexing.jl b/test/sparsematrix_constructors_indexing.jl index a0396f40..52b09677 100644 --- a/test/sparsematrix_constructors_indexing.jl +++ b/test/sparsematrix_constructors_indexing.jl @@ -1426,6 +1426,21 @@ using Base: swaprows!, swapcols! f!(Scopy, i, j); f!(Sdense, i, j) @test Scopy == Sdense end + + for (A, i, j) in ( + (sparse([1.0 2.0 3.0; + 0.0 0.0 0.0; + 4.0 5.0 6.0]), 1, 2), + (sparse([1.0 0.0 5.0; + 0.0 2.0 0.0; + 0.0 3.0 6.0; + 7.0 4.0 0.0]), 1, 2), + (sparse(reshape([1.0, 2.0, 3.0, 4.0, 0.0, 0.0], 6, 1)), 2, 6)) + Scopy = copy(A) + Sdense = Array(A) + swaprows!(Scopy, i, j); swaprows!(Sdense, i, j) + @test Scopy == Sdense + end end @testset "sprandn with type $T" for T in (Float64, Float32, Float16, ComplexF64, ComplexF32, ComplexF16) From 7eb244fda3091ab43f04da11e4999443d7873c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateus=20Ara=C3=BAjo?= Date: Mon, 25 May 2026 18:08:00 +0200 Subject: [PATCH 2/7] fix findmin/findmax with nan (#722) Closes #714 (cherry picked from commit 3f9b6fbbbd70962825f957e4a7f263d386190db9) --- src/sparsevector.jl | 2 +- test/sparsevector.jl | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sparsevector.jl b/src/sparsevector.jl index 1d7daf0b..65077517 100644 --- a/src/sparsevector.jl +++ b/src/sparsevector.jl @@ -1704,7 +1704,7 @@ for (fun, comp, word) in ((:findmin, :(<), "minimum"), (:findmax, :(>), "maximum m == n && return val, index nzinds = nonzeroinds(x) zeroval = f(zero(T)) - $comp(val, zeroval) && return val, nzinds[index] + ($comp(val, zeroval) || isnan(val)) && return val, nzinds[index] # we need to find the first zero, which could be stored or implicit # we try to avoid findfirst(iszero, x) sindex = findfirst(_iszero, nzvals) # first stored zero, if any diff --git a/test/sparsevector.jl b/test/sparsevector.jl index c91f0d69..a3ad5fe0 100644 --- a/test/sparsevector.jl +++ b/test/sparsevector.jl @@ -1052,6 +1052,11 @@ end @test all(!iszero, v) @test !any(iszero, v) end + + let v = sparse([0, NaN]) #issue #714 + @test findmin(v) === (NaN, 2) + @test findmax(v) === (NaN, 2) + end end ### linalg From 5fc293c0d82b5e253c3e9d7f5255c491ae8b6412 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Thu, 7 May 2026 17:02:57 +0100 Subject: [PATCH 3/7] Fix and test `reuse_symbolic=false` for UMFPACK `lu!()` (#712) The `reuse_symbolic=false` option for UMFPACK's `lu!()` is broken, due to undefined `Tv` and `Ti`. This PR fixes that bug, and adds a test that uses `reuse_symbolic=false`. (cherry picked from commit 40d5de202ea747ff7b05110f83c46eada1400892) --- src/solvers/umfpack.jl | 3 ++- test/umfpack.jl | 16 +++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/solvers/umfpack.jl b/src/solvers/umfpack.jl index 7a8458d9..fc4b9c17 100644 --- a/src/solvers/umfpack.jl +++ b/src/solvers/umfpack.jl @@ -471,7 +471,8 @@ function lu!(F::UmfpackLU{Tv, Ti}, S::AbstractSparseMatrixCSC; return lu!(F; reuse_symbolic, check, q) end -function lu!(F::UmfpackLU; check::Bool=true, reuse_symbolic::Bool=true, q=nothing) +function lu!(F::UmfpackLU{Tv, Ti}; check::Bool=true, reuse_symbolic::Bool=true, + q=nothing) where {Tv, Ti} if !reuse_symbolic && _isnotnull(F.symbolic) F.symbolic = Symbolic{Tv, Ti}(C_NULL) end diff --git a/test/umfpack.jl b/test/umfpack.jl index 59dc9041..0a4525e2 100644 --- a/test/umfpack.jl +++ b/test/umfpack.jl @@ -408,7 +408,7 @@ end umfpack_report(x.b) end - @testset "Reuse symbolic LU factorization" begin + @testset "Do/do not reuse symbolic LU factorization" for reuse ∈ (true, false) A1 = sparse(increment!([0,4,1,1,2,2,0,1,2,3,4,4]), increment!([0,4,0,2,1,2,1,4,3,2,1,2]), [2.,1.,3.,4.,-1.,-3.,3.,9.,2.,1.,4.,2.], 5, 5) @@ -420,7 +420,7 @@ end b = Tv[8., 45., -3., 3., 19.] F = lu(A) umfpack_report(F) - lu!(F, B) + lu!(F, B; reuse_symbolic=reuse) umfpack_report(F) @test F\b ≈ B\b ≈ Matrix(B)\b @@ -429,14 +429,20 @@ end C[4, 3] = Tv(0) F = lu(A) umfpack_report(F) - @test_throws SingularException lu!(F, C) + @test_throws SingularException lu!(F, C; reuse_symbolic=reuse) # change of nonzero pattern D = copy(B) D[5, 1] = Tv(1.0) F = lu(A) umfpack_report(F) - @test_throws ArgumentError lu!(F, D) - umfpack_report(F) + if reuse + @test_throws ArgumentError lu!(F, D; reuse_symbolic=reuse) + umfpack_report(F) + else + lu!(F, D; reuse_symbolic=reuse) + umfpack_report(F) + @test F\b ≈ D\b ≈ Matrix(D)\b + end end end end From 853e709b22422c1b1765600e2d3a1efa24de52bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateus=20Ara=C3=BAjo?= Date: Wed, 6 May 2026 19:16:11 +0200 Subject: [PATCH 4/7] make zero preserve indextype (#710) Closes #574 The appropriate function to extract the index type is `indtype`, not `keytype`. Since it already works correctly the only thing to do is fix the `zero` method. (cherry picked from commit 405a8d5d060fc128a323b192b25918f7063cb609) --- src/SparseArrays.jl | 2 +- test/issues.jl | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/SparseArrays.jl b/src/SparseArrays.jl index 46b9c01c..2418f9e2 100644 --- a/src/SparseArrays.jl +++ b/src/SparseArrays.jl @@ -85,7 +85,7 @@ if Base.USE_GPL_LIBS include("solvers/spqr.jl") end -zero(a::AbstractSparseArray) = spzeros(eltype(a), size(a)...) +zero(a::AbstractSparseArray{Tv,Ti}) where {Tv,Ti} = spzeros(Tv, Ti, size(a)...) LinearAlgebra.diagzero(D::Diagonal{<:AbstractSparseMatrix{T}},i,j) where {T} = spzeros(T, size(D.diag[i], 1), size(D.diag[j], 2)) diff --git a/test/issues.jl b/test/issues.jl index f198812a..9f46a639 100644 --- a/test/issues.jl +++ b/test/issues.jl @@ -805,6 +805,13 @@ end 7 16 4]) end +@testset "Issue #574" begin + a = spzeros(Float32, Int16, 2, 3) + v = spzeros(Float32, Int16, 2) + @test eltype(rowvals(zero(a))) <: Int16 + @test eltype(rowvals(zero(v))) <: Int16 +end + end # SparseTestsBase end # module From 019a7827241bac483a4437185b3441472360f88a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateus=20Ara=C3=BAjo?= Date: Fri, 1 May 2026 15:24:55 +0200 Subject: [PATCH 5/7] fix cholesky linear solve with sparse rhs (#709) Closes #630 (cherry picked from commit 754280c8c6ed6faf3b5d1fa18acef5ec0c0a035c) --- src/solvers/cholmod.jl | 3 ++- test/cholmod.jl | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/solvers/cholmod.jl b/src/solvers/cholmod.jl index 504f5d43..797c7c03 100644 --- a/src/solvers/cholmod.jl +++ b/src/solvers/cholmod.jl @@ -990,7 +990,7 @@ function Sparse(A::SparseMatrixCSC{<:Union{ComplexF16, ComplexF32}}, stype::Inte end # convert SparseVectors into CHOLMOD Sparse types through a mx1 CSC matrix -Sparse(A::SparseVector) = Sparse(SparseMatrixCSC(A)) +Sparse(A::SparseVector) = Sparse(SparseMatrixCSC(A), 0) function Sparse{Tv, Ti}(A::SparseMatrixCSC) where {Tv<:VTypes, Ti<:ITypes} o = Sparse{Tv, Ti}(A, 0) # check if array is symmetric and change stype if it is @@ -1135,6 +1135,7 @@ function SparseVector{Tv, Ti}(A::Sparse{Tv, Ti}) where {Tv, Ti<:ITypes} end args = _extract_args(s, Tv) s.sorted == 0 && _sort_buffers!(args...); + _trim_nz_builder!(args...) return SparseVector(args[1], args[4], args[5]) end diff --git a/test/cholmod.jl b/test/cholmod.jl index 4224c002..240289bd 100644 --- a/test/cholmod.jl +++ b/test/cholmod.jl @@ -806,6 +806,14 @@ end @test chI \ sparseI ≈ sparseI end +@testset "Issue 630" begin + sparseI = sparse(1.0I, 1, 1) + @test cholesky(sparseI) \ sparse([1.0]) == [1] + sparseI = sparse(1.0I, 2, 2) + res = cholesky(sparseI) \ spzeros(2) + @test isempty(nonzeros(res)) +end + @testset "Real factorization and complex rhs" begin A = sprandn(5, 5, 0.4) |> t -> t't + I B = complex.(randn(5, 5), randn(5, 5)) From f647b920330de56aae342ee110fc195962612049 Mon Sep 17 00:00:00 2001 From: PatrickHaecker <152268010+PatrickHaecker@users.noreply.github.com> Date: Sun, 24 May 2026 13:09:43 +0200 Subject: [PATCH 6/7] test: avoid method-overwrite warnings in forbidproperties.jl (#716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrick Häcker Co-authored-by: Copilot (cherry picked from commit f7b1e1991d53f6158e59d6f3bd042c5061e57650) --- test/forbidproperties.jl | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/forbidproperties.jl b/test/forbidproperties.jl index 6550462c..d07d9cab 100644 --- a/test/forbidproperties.jl +++ b/test/forbidproperties.jl @@ -1,5 +1,18 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license using SparseArrays -Base.getproperty(::SparseMatrixCSC, ::Symbol) = error("use accessor function") -Base.getproperty(::SparseVector, ::Symbol) = error("use accessor function") +# Only define each `getproperty` override if it isn't already defined. This +# file is `include`d from multiple sibling test modules, and without this +# guard each subsequent include would re-set the same `Base` methods, +# producing spurious method-overwrite warnings (depending on `--warn-overwrite`). +# `hasmethod` is not sufficient here because the generic `Base.getproperty` +# fallback for `Any` always exists; we instead check whether `which` returns +# that fallback method. +let fallback = which(Base.getproperty, Tuple{Any, Symbol}) + if which(Base.getproperty, Tuple{SparseMatrixCSC, Symbol}) === fallback + Base.getproperty(::SparseMatrixCSC, ::Symbol) = error("use accessor function") + end + if which(Base.getproperty, Tuple{SparseVector, Symbol}) === fallback + Base.getproperty(::SparseVector, ::Symbol) = error("use accessor function") + end +end From 32f78fca21b6164d45ac29fab4471b5ca29b2e71 Mon Sep 17 00:00:00 2001 From: "Viral B. Shah" Date: Sat, 23 May 2026 23:00:10 -0400 Subject: [PATCH 7/7] Add trailing whitespace check (#720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds `.ci/check-whitespace.jl`, adapted from [JuliaLang/julia's `contrib/check-whitespace.jl`](https://github.com/JuliaLang/julia/blob/master/contrib/check-whitespace.jl), with patterns trimmed to files relevant to this repo (`*.jl`, `*.md`, `*.yml`, `*Makefile`). - Adds `.github/workflows/Whitespace.yml` to run the check on push to `main` and on pull requests. - Fixes pre-existing whitespace violations (trailing whitespace, trailing blank lines, a non-breaking space) so the new check passes. Companion to JuliaLang/LinearAlgebra.jl#1633. ## Test plan - [x] `julia .ci/check-whitespace.jl` reports no issues locally - [x] Whitespace workflow passes in CI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Viral B. Shah Co-authored-by: Claude Opus 4.7 (1M context) (cherry picked from commit 5926672fcc66664a253e8c143a8c352eab33b022) --- .ci/check-whitespace.jl | 98 ++++++++++++++++++++++++++++++++ .github/workflows/Whitespace.yml | 26 +++++++++ gen/Makefile | 2 +- src/solvers/spqr.jl | 4 +- src/sparseconvert.jl | 1 - test/cholmod.jl | 2 +- test/fixed.jl | 2 +- 7 files changed, 129 insertions(+), 6 deletions(-) create mode 100755 .ci/check-whitespace.jl create mode 100644 .github/workflows/Whitespace.yml diff --git a/.ci/check-whitespace.jl b/.ci/check-whitespace.jl new file mode 100755 index 00000000..604af8dc --- /dev/null +++ b/.ci/check-whitespace.jl @@ -0,0 +1,98 @@ +#!/usr/bin/env julia + +const patterns = split(""" + *.jl + *.md + *.yml + *Makefile +""") + +const is_gha = something(tryparse(Bool, get(ENV, "GITHUB_ACTIONS", "false")), false) + +# Note: `git ls-files` gives `/` as a path separator on Windows, +# so we just use `/` for all platforms. +allow_tabs(path) = + endswith(path, "Makefile") || + endswith(path, ".make") || + endswith(path, ".mk") + +function check_whitespace() + errors = Set{Tuple{String,Int,String}}() + files_to_check = filter(arg -> !startswith(arg, "-"), ARGS) + if isempty(files_to_check) + if "--stdin" in ARGS + files_to_check = collect(eachline(stdin)) + else + files_to_check = collect(eachline(`git ls-files -- $patterns`)) + end + end + + files_fixed = 0 + if "--fix" in ARGS + for path in files_to_check + content = newcontent = read(path, String) + isempty(content) && continue + if !allow_tabs(path) + tabpattern = r"^([ \t]+)"m => (x -> replace(x, r"((?: {4})*)( *\t)" => s"\1 ")) # Replace tab sequences at start of line after any number of 4-space groups + newcontent = replace(newcontent, tabpattern) + end + newcontent = replace(newcontent, + r"\s*$" => '\n', # Remove trailing whitespace and normalize line ending at eof + r"\s*?[\r\n]" => '\n', # Remove trailing whitespace and normalize line endings on each line + r"\xa0" => ' ' # Replace non-breaking spaces + ) + if content != newcontent + write(path, newcontent) + files_fixed += 1 + end + end + if files_fixed > 0 + println(stderr, "Fixed whitespace issues in $files_fixed files.") + end + end + + for path in files_to_check + lineno = 0 + non_blank = 0 + + file_err(msg) = push!(errors, (path, 0, msg)) + line_err(msg) = push!(errors, (path, lineno, msg)) + + isfile(path) || continue + for line in eachline(path, keep=true) + lineno += 1 + contains(line, '\r') && file_err("non-UNIX line endings") + contains(line, '\ua0') && line_err("non-breaking space") + allow_tabs(path) || + contains(line, '\t') && line_err("tab") + endswith(line, '\n') || line_err("no trailing newline") + line = chomp(line) + endswith(line, r"\s") && line_err("trailing whitespace") + contains(line, r"\S") && (non_blank = lineno) + end + non_blank < lineno && line_err("trailing blank lines") + end + + if isempty(errors) + println(stderr, "Whitespace check found no issues.") + exit(0) + else + println(stderr, "Whitespace check found $(length(errors)) issues:") + for (path, lineno, msg) in sort!(collect(errors)) + if lineno == 0 + println(stderr, "$path -- $msg") + if is_gha + println(stdout, "::warning title=Whitespace check,file=", path, "::", msg) + end + else + println(stderr, "$path:$lineno -- $msg") + if is_gha + println(stdout, "::warning title=Whitespace check,file=", path, ",line=", lineno, "::", msg) + end + end + end + exit(1) + end +end + +check_whitespace() diff --git a/.github/workflows/Whitespace.yml b/.github/workflows/Whitespace.yml new file mode 100644 index 00000000..3931cba2 --- /dev/null +++ b/.github/workflows/Whitespace.yml @@ -0,0 +1,26 @@ +name: Whitespace + +permissions: {} + +on: + push: + branches: + - main + pull_request: + +jobs: + whitespace: + name: Check whitespace + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Checkout the JuliaSparse/SparseArrays.jl repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: julia-actions/setup-julia@f6f565d9f7cf12f53dc8045742460d6260ad3b39 # v3.0.1 + with: + version: '1.11.6' + - name: Check whitespace + run: | + julia .ci/check-whitespace.jl diff --git a/gen/Makefile b/gen/Makefile index e19103fc..132889fc 100755 --- a/gen/Makefile +++ b/gen/Makefile @@ -7,7 +7,7 @@ all: clean download julia --project generator.jl ./SuiteSparse-$(VER) clean: - rm -fr *.tar.gz SuiteSparse* + rm -fr *.tar.gz SuiteSparse* download: curl -L -O https://github.com/JuliaBinaryWrappers/SuiteSparse_jll.jl/releases/download/SuiteSparse-v$(VER)%2B0/SuiteSparse.v$(VER).x86_64-linux-gnu.tar.gz diff --git a/src/solvers/spqr.jl b/src/solvers/spqr.jl index 5e9fa07e..72ebe550 100644 --- a/src/solvers/spqr.jl +++ b/src/solvers/spqr.jl @@ -85,7 +85,7 @@ function _qr!(ordering::Integer, tol::Real, econ::Integer, getCTX::Integer, # Free memory allocated by SPQR. This call will make sure that the # correct deallocator function is called and that the memory count in # the common struct is updated - Ti === Int64 ? + Ti === Int64 ? cholmod_l_free(n, sizeof(Ti), e, CHOLMOD.getcommon(Ti)) : cholmod_free(n, sizeof(Ti), e, CHOLMOD.getcommon(Ti)) end @@ -100,7 +100,7 @@ function _qr!(ordering::Integer, tol::Real, econ::Integer, getCTX::Integer, # Free memory allocated by SPQR. This call will make sure that the # correct deallocator function is called and that the memory count in # the common struct is updated - Ti === Int64 ? + Ti === Int64 ? cholmod_l_free(m, sizeof(Ti), hpinv, CHOLMOD.getcommon(Ti)) : cholmod_free(m, sizeof(Ti), hpinv, CHOLMOD.getcommon(Ti)) end diff --git a/src/sparseconvert.jl b/src/sparseconvert.jl index 479654b7..80854331 100644 --- a/src/sparseconvert.jl +++ b/src/sparseconvert.jl @@ -280,4 +280,3 @@ function _sparse_gen(m, n, newcolptr, newrowval, newnzval) newcolptr[1] = 1 SparseMatrixCSC(m, n, newcolptr, newrowval, newnzval) end - diff --git a/test/cholmod.jl b/test/cholmod.jl index 240289bd..f5e9ef93 100644 --- a/test/cholmod.jl +++ b/test/cholmod.jl @@ -1025,7 +1025,7 @@ end end f = ones(size(K, 1)) - u = K \ f + u = K \ f residual = norm(f - K * u) / norm(f) @test residual < 1e-6 end diff --git a/test/fixed.jl b/test/fixed.jl index 59c10346..e477363d 100644 --- a/test/fixed.jl +++ b/test/fixed.jl @@ -118,7 +118,7 @@ end @test f(x, y, z) == 0 t = similar(x) @test typeof(t) == typeof(x) - @test struct_eq(t, x) + @test struct_eq(t, x) end @testset "Issue #190" begin