From 77927278d4e6e2017c347ba0875adad82c46ed69 Mon Sep 17 00:00:00 2001 From: rokke <66498307+rokke-git@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:21:23 -0400 Subject: [PATCH 1/4] use normal entrypoints into base's display pipeline Base and SparseArrays had some funky interactions going on with replace_in_print_matrix, this fully disentangles the print funcs. it also makes SparseArrays smarter about swaping to braille, fixes aliasing, and generally makes printing more robust. --- src/sparsematrix.jl | 77 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/src/sparsematrix.jl b/src/sparsematrix.jl index 51ff5caf..f4c78510 100644 --- a/src/sparsematrix.jl +++ b/src/sparsematrix.jl @@ -334,10 +334,7 @@ function Base.isstored(A::AdjOrTrans{<:Any,<:AbstractSparseMatrixCSC}, i::Intege return false end -Base.replace_in_print_matrix(A::AbstractSparseMatrixCSCInclAdjointAndTranspose, i::Integer, j::Integer, s::AbstractString) = - Base.isstored(A, i, j) ? s : Base.replace_with_centered_mark(s) - -function Base.array_summary(io::IO, S::AbstractSparseMatrixCSCInclAdjointAndTranspose, dims::Tuple{Vararg{Base.OneTo}}) +function Base.summary(io::IO, S::AbstractSparseMatrixCSCInclAdjointAndTranspose) _checkbuffers(S) xnnz = nnz(S) @@ -347,12 +344,23 @@ function Base.array_summary(io::IO, S::AbstractSparseMatrixCSCInclAdjointAndTran nothing end -# called by `show(io, MIME("text/plain"), ::AbstractSparseMatrixCSCInclAdjointAndTranspose)` -function Base.print_array(io::IO, S::AbstractSparseMatrixCSCInclAdjointAndTranspose) - if max(size(S)...) < 16 - Base.print_matrix(io, S) - else +using Base: show_circular +function Base.show(io::IO, ::MIME"text/plain", S::AbstractSparseMatrixCSCInclAdjointAndTranspose) + isempty(S) && get(io, :compact, false) && return show(io, S) + summary(io, S) + isempty(S) && return + + screen = displaysize(io) + get(io, :limit, false) && screen[1] <= 4 && return print(io, ": …") + println(io, ":") + + show_circular(io, S) && return + io = IOContext(io, :compact=>true, :typeinfo=>eltype(S), :SHOWN_SET=>S) + + if (screen[1] < size(S, 1) + 4) | (screen[2] < 3size(S, 2)) _show_with_braille_patterns(io, S) + else + _show_with_dotted_zeros(io, S) end end @@ -405,7 +413,15 @@ function _show_with_braille_patterns(io::IO, S::AbstractSparseMatrixCSCInclAdjoi # The maximal number of characters we allow to display the matrix local maxHeight::Int, maxWidth::Int maxHeight = displaysize(io)[1] - 4 # -4 from [Prompt, header, newline after elements, new prompt] - maxWidth = displaysize(io)[2] ÷ 2 + maxWidth = displaysize(io)[2] - 2 # -2 from brackets + + warn = false + try + zero(eltype(S)) + catch + warn = true + maxHeight -= 1 + end # In the process of generating the braille pattern to display the nonzero # structure of `S`, we need to be able to scale the matrix `S` to a @@ -450,6 +466,13 @@ function _show_with_braille_patterns(io::IO, S::AbstractSparseMatrixCSCInclAdjoi rvals = rowvals(parent(S)) rowscale = max(1, scaleHeight - 1) / max(1, m - 1) colscale = max(1, scaleWidth - 1) / max(1, n - 1) + + if rowscale != 1 || colscale != 1 + print(io, " (downscaled to fit on screen)") + end + println(io, ":") + warn && printstyled(stderr, "WARNING: could not find generic zero for given elements. expect errors and wrong results\n", color=:red) + if isa(S, AbstractSparseMatrixCSC) @inbounds for j in axes(S,2) # Scale the column index `j` to the best matching column index @@ -489,6 +512,40 @@ function _show_with_braille_patterns(io::IO, S::AbstractSparseMatrixCSCInclAdjoi foreach(c -> print(io, Char(c)), @view brailleGrid[1:end-1]) end +using Base: alignment +function _show_with_dotted_zeros(io::IO, S::AbstractSparseMatrixCSCInclAdjointAndTranspose) + rows, cols, vals = rowvals(parent(S)), ColumnIndices(parent(S)), nonzeros(parent(S)) + (S isa Adjoint) | (S isa Transpose) && ((rows, cols) = (cols, rows)) + + align = [isassigned(vals, ind) ? alignment(io, vals[ind]) : (3, 3) for ind in eachindex(vals)] + colwidths = [max.((0, 0), align[findall(==(col), cols)]...) for col in axes(S,2)] + displaysize(io)[2] < sum(sum.(colwidths) .+ 2) && return _show_with_braille_patterns(io, S) + + try + zero(eltype(S)) + catch + printstyled(stderr, "WARNING: could not find generic zero for given elements. expect errors and wrong results\n", color=:red) + end + + for row in axes(S,1) + for col in axes(S,2) + index = findall(==(col), cols) + index = index[findall(==(row), rows[index])] + l, r = colwidths[col] + if isempty(index) # no value here, print an aligned dot + l, r = cld(l+r-1, 2) + 1, div(l+r-1, 2) + 1 + print(io, " "^l * "⋅" * (col==axes(S,2)[end] ? "" : " "^r)) + else + l, r = (l+1, r+1) .- align[index[]] + print(io, " "^l) + isassigned(vals, index[]) ? show(io, vals[index[]]) : print(io, "#undef") + col == axes(S,2)[end] || print(io, " "^r) + end + end + row == axes(S,1)[end] || println(io) + end +end + for QT in (:LinAlgLeftQs, :LQPackedQ) @eval (*)(Q::$QT, B::AbstractSparseMatrixCSC) = Q * Matrix(B) @eval (*)(Q::$QT, B::AdjOrTrans{<:Any,<:AbstractSparseMatrixCSC}) = Q * copy(B) From 4243b96289cc41b98716d34bbda1431c696fb4be Mon Sep 17 00:00:00 2001 From: rokke <66498307+rokke-git@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:56:58 -0400 Subject: [PATCH 2/4] fix related issues, add tests --- docs/src/index.md | 7 +++- src/linalg.jl | 12 +++--- src/solvers/spqr.jl | 12 +++--- src/sparsematrix.jl | 43 +++++++++++++++------- test/issues.jl | 28 +++++++++++++- test/sparsematrix_constructors_indexing.jl | 32 ++++++++-------- 6 files changed, 88 insertions(+), 46 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index cb2760c5..27fc0521 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -113,8 +113,11 @@ julia> I = [1, 4, 3, 5]; J = [4, 7, 18, 9]; V = [1, 2, -5, 3]; julia> S = sparse(I,J,V) 5×18 SparseMatrixCSC{Int64, Int64} with 4 stored entries: -⎡⠀⠈⠀⠀⠀⠀⠀⠀⢀⎤ -⎣⠀⠀⠀⠂⡀⠀⠀⠀⠀⎦ + ⋅ ⋅ ⋅ 1 ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ + ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ + ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ -5 + ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ 2 ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ + ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ 3 ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ julia> R = sparsevec(I,V) 5-element SparseVector{Int64, Int64} with 4 stored entries: diff --git a/src/linalg.jl b/src/linalg.jl index 69e2a0d5..2f6f6ab6 100644 --- a/src/linalg.jl +++ b/src/linalg.jl @@ -2094,9 +2094,9 @@ julia> A[4:4:8] .= 1; julia> A 3×3 SparseMatrixCSC{Float64, Int64} with 2 stored entries: - ⋅ 1.0 ⋅ - ⋅ ⋅ 1.0 - ⋅ ⋅ ⋅ + ⋅ 1.0 ⋅ + ⋅ ⋅ 1.0 + ⋅ ⋅ ⋅ julia> C = spzeros(3,3); @@ -2104,9 +2104,9 @@ julia> C[2:4:6] .= 2; julia> C 3×3 SparseMatrixCSC{Float64, Int64} with 2 stored entries: - ⋅ ⋅ ⋅ - 2.0 ⋅ ⋅ - ⋅ 2.0 ⋅ + ⋅ ⋅ ⋅ + 2.0 ⋅ ⋅ + ⋅ 2.0 ⋅ julia> SparseArrays.mergeinds!(C, A) 3×3 SparseMatrixCSC{Float64, Int64} with 4 stored entries: diff --git a/src/solvers/spqr.jl b/src/solvers/spqr.jl index 2f71dd65..9e5f2ebb 100644 --- a/src/solvers/spqr.jl +++ b/src/solvers/spqr.jl @@ -192,8 +192,8 @@ Q factor: 4×4 SparseArrays.SPQR.QRSparseQ{Float64, Int64} R factor: 2×2 SparseMatrixCSC{Float64, Int64} with 2 stored entries: - -1.41421 ⋅ - ⋅ -1.41421 + -1.41421 ⋅ + ⋅ -1.41421 Row permutation: 4-element Vector{Int64}: 1 @@ -471,10 +471,10 @@ when the problem is underdetermined. ```jldoctest julia> A = sparse([1,2,4], [1,1,1], [1.0,1.0,1.0], 4, 2) 4×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries: - 1.0 ⋅ - 1.0 ⋅ - ⋅ ⋅ - 1.0 ⋅ + 1.0 ⋅ + 1.0 ⋅ + ⋅ ⋅ + 1.0 ⋅ julia> qr(A)\\fill(1.0, 4) 2-element Vector{Float64}: diff --git a/src/sparsematrix.jl b/src/sparsematrix.jl index f4c78510..7f144c42 100644 --- a/src/sparsematrix.jl +++ b/src/sparsematrix.jl @@ -352,7 +352,6 @@ function Base.show(io::IO, ::MIME"text/plain", S::AbstractSparseMatrixCSCInclAdj screen = displaysize(io) get(io, :limit, false) && screen[1] <= 4 && return print(io, ": …") - println(io, ":") show_circular(io, S) && return io = IOContext(io, :compact=>true, :typeinfo=>eltype(S), :SHOWN_SET=>S) @@ -433,7 +432,7 @@ function _show_with_braille_patterns(io::IO, S::AbstractSparseMatrixCSCInclAdjoi # `scaleHeight` and `scaleWidth` accordingly. Note that each available # character can contain up to 4 braille dots in its height (⡇) and up to # 2 braille dots in its width (⠉). - if get(io, :limit, true) && (m > 4maxHeight || n > 2maxWidth) + if get(io, :limit, false) && (m > 4maxHeight || n > 2maxWidth) s = min(2maxWidth / n, 4maxHeight / m) scaleHeight = floor(Int, s * m) scaleWidth = floor(Int, s * n) @@ -521,6 +520,8 @@ function _show_with_dotted_zeros(io::IO, S::AbstractSparseMatrixCSCInclAdjointAn colwidths = [max.((0, 0), align[findall(==(col), cols)]...) for col in axes(S,2)] displaysize(io)[2] < sum(sum.(colwidths) .+ 2) && return _show_with_braille_patterns(io, S) + println(io, ":") + try zero(eltype(S)) catch @@ -535,12 +536,26 @@ function _show_with_dotted_zeros(io::IO, S::AbstractSparseMatrixCSCInclAdjointAn if isempty(index) # no value here, print an aligned dot l, r = cld(l+r-1, 2) + 1, div(l+r-1, 2) + 1 print(io, " "^l * "⋅" * (col==axes(S,2)[end] ? "" : " "^r)) - else + else try # print the element with 1 space of buffer on each side l, r = (l+1, r+1) .- align[index[]] print(io, " "^l) isassigned(vals, index[]) ? show(io, vals[index[]]) : print(io, "#undef") col == axes(S,2)[end] || print(io, " "^r) - end + catch # if there's 2+ entries, index[] errors. default to summing + # entries, but print red to warn user that something's wrong + elm = any(!isassigned(vals, ind) for ind in index) ? "#undef" : + try repr(sum(vals[index]); context=:compact=>true) catch e "#NaN" end + + if length(elm) <= l+r # give up on alignment, center item in the column + l, r = cld(l+r-length(elm), 2) + 1, div(l+r-length(elm), 2) + 1 + else # len of new item does not fit in column + elm = "▒"^(l+r) + l, r = 1, 1 + end + + printstyled(io, " "^l, elm, color=:red) + col == axes(S,2)[end] || print(io, " "^r) + end end end row == axes(S,1)[end] || println(io) end @@ -688,7 +703,7 @@ function _sparse_copyto!(dest::AbstractMatrix, src::AbstractSparseMatrixCSC) @inbounds for col in axes(src, 2), ptr in nzrange(src, col) row = rowvals(src)[ptr] val = nonzeros(src)[ptr] - dest[isrc[row, col]] = val + dest[isrc[row, col]] += val end return dest end @@ -712,7 +727,7 @@ function copyto!(dest::AbstractMatrix, Rdest::CartesianIndices{2}, if row in rows val = nonzeros(src′)[ptr] I = Rdest[lin[row, col]] - dest[I] = val + dest[I] += val end end return dest @@ -737,7 +752,7 @@ function Base.copyto!(A::Array{T}, S::SparseMatrixCSC{<:Number}) where {T<:Numbe for i in nzrange(S, col) row = rowval[i] val = nzval[i] - A[linear_index_col0+row] = val + A[linear_index_col0+row] += val end linear_index_col0 += num_rows end @@ -1962,9 +1977,9 @@ julia> A = sparse([1, 2, 3], [1, 2, 3], [1.0, 0.0, 1.0]) julia> dropzeros(A) 3×3 SparseMatrixCSC{Float64, Int64} with 2 stored entries: - 1.0 ⋅ ⋅ - ⋅ ⋅ ⋅ - ⋅ ⋅ 1.0 + 1.0 ⋅ ⋅ + ⋅ ⋅ ⋅ + ⋅ ⋅ 1.0 ``` """ dropzeros(A::AbstractSparseMatrixCSC) = dropzeros!(copy(A)) @@ -2134,7 +2149,7 @@ argument specifies a random number generator, see [Random Numbers](@ref). ```jldoctest; setup = :(using Random; Random.seed!(0)) julia> sprandn(2, 2, 0.75) 2×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries: - -1.20577 ⋅ + -1.20577 ⋅ 0.311817 -0.234641 ``` """ @@ -2161,9 +2176,9 @@ specified. ```jldoctest julia> spzeros(3, 3) 3×3 SparseMatrixCSC{Float64, Int64} with 0 stored entries: - ⋅ ⋅ ⋅ - ⋅ ⋅ ⋅ - ⋅ ⋅ ⋅ + ⋅ ⋅ ⋅ + ⋅ ⋅ ⋅ + ⋅ ⋅ ⋅ julia> spzeros(Float32, 4) 4-element SparseVector{Float32, Int64} with 0 stored entries diff --git a/test/issues.jl b/test/issues.jl index 9f46a639..7667e88a 100644 --- a/test/issues.jl +++ b/test/issues.jl @@ -758,14 +758,14 @@ let B = similar(A, T12960) @test repr(B) == "sparse([1, 2, 3], [1, 2, 3], $T12960[#undef, #undef, #undef], 3, 3)" @test occursin( - "\n #undef ⋅ ⋅ \n ⋅ #undef ⋅ \n ⋅ ⋅ #undef", + " #undef ⋅ ⋅\n ⋅ #undef ⋅\n ⋅ ⋅ #undef", repr(MIME("text/plain"), B), ) B[1,2] = T12960() @test repr(B) == "sparse([1, 1, 2, 3], [1, 2, 2, 3], $T12960[#undef, $T12960(), #undef, #undef], 3, 3)" @test occursin( - "\n #undef T12960() ⋅ \n ⋅ #undef ⋅ \n ⋅ ⋅ #undef", + "\n #undef T12960() ⋅\n ⋅ #undef ⋅\n ⋅ ⋅ #undef", repr(MIME("text/plain"), B), ) end @@ -805,6 +805,15 @@ end 7 16 4]) end +@testset "Issue #512" begin # suppresses but does not fix the error mentioned + x = sparse([1, 1, 3], [1, 4, 3], [20, 0, [2]]) + @test_warn "WARNING: could not find generic zero" repr(MIME("text/plain"), x) + x = sparse([1, 100, 3], [1, 4, 300], [20, 0, [2]]) + @test_warn "WARNING: could not find generic zero" repr(MIME("text/plain"), x) + + @test_broken repr(MIME("text/plain"), transpose(x')) +end + @testset "Issue #574" begin a = spzeros(Float32, Int16, 2, 3) v = spzeros(Float32, Int16, 2) @@ -812,6 +821,21 @@ end @test eltype(rowvals(zero(v))) <: Int16 end +@testset "Issue #618" begin + x = SparseMatrixCSC(3, 3, [1, 3, 4, 5], [1, 1, 2, 3], [1.0, 1.0, 1.0, 1.0]) + @test contains(repr(MIME"text/plain"(), x), "2.0") + x = SparseMatrixCSC(3, 3, [1, 3, 4, 5], [1, 1, 2, 3], [7.0, 7.0, 1.0, 1.0]) + @test contains(repr(MIME"text/plain"(), x), "▒▒▒") + x = SparseMatrixCSC(3, 3, [1, 3, 4, 5], [1, 1, 2, 3], [7.0, 'o', 1.0, 1.0]) + @test contains(repr(MIME"text/plain"(), x), "#NaN") + v = similar(Any[1, 2, 3, 4]); v[2:4] = [1.0, 1.0, 1.0] + x = SparseMatrixCSC(3, 3, [1, 3, 4, 5], [1, 1, 2, 3], v) + @test contains(repr(MIME"text/plain"(), x), "#undef") + + x = SparseMatrixCSC(3, 3, [1, 3, 4, 5], [1, 1, 2, 3], [1, 1, 1, 1]) + @test 2 == Matrix(x)[1,1] +end + end # SparseTestsBase end # module diff --git a/test/sparsematrix_constructors_indexing.jl b/test/sparsematrix_constructors_indexing.jl index 9fd1567c..48141b61 100644 --- a/test/sparsematrix_constructors_indexing.jl +++ b/test/sparsematrix_constructors_indexing.jl @@ -1514,9 +1514,9 @@ end A = spzeros(Float32, Int64, 2, 2) for (transform, showstring) in zip( (identity, adjoint, transpose), ( - "2×2 $SparseMatrixCSC{Float32, Int64} with 0 stored entries:\n ⋅ ⋅ \n ⋅ ⋅ ", - "2×2 $Adjoint{Float32, $SparseMatrixCSC{Float32, Int64}} with 0 stored entries:\n ⋅ ⋅ \n ⋅ ⋅ ", - "2×2 $Transpose{Float32, $SparseMatrixCSC{Float32, Int64}} with 0 stored entries:\n ⋅ ⋅ \n ⋅ ⋅ ", + "2×2 $SparseMatrixCSC{Float32, Int64} with 0 stored entries:\n ⋅ ⋅\n ⋅ ⋅", + "2×2 $Adjoint{Float32, $SparseMatrixCSC{Float32, Int64}} with 0 stored entries:\n ⋅ ⋅\n ⋅ ⋅", + "2×2 $Transpose{Float32, $SparseMatrixCSC{Float32, Int64}} with 0 stored entries:\n ⋅ ⋅\n ⋅ ⋅", )) show(io, MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring @@ -1538,7 +1538,7 @@ end show(io, MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring _show_with_braille_patterns(convert(IOContext, io), transform(A)) - @test String(take!(io)) == braille + @test contains(String(take!(io)), braille) end # every 1-dot braille pattern @@ -1559,7 +1559,7 @@ end for transform in (identity, adjoint, transpose) expected = "⎡" * Char(10240)^2 * "⎤\n⎣" * Char(10240)^2 * "⎦" _show_with_braille_patterns(convert(IOContext, io), transform(A)) - @test String(take!(io)) == expected + @test contains(String(take!(io)), expected) end A = sparse(Int64[1, 2, 4, 2, 3], Int64[1, 1, 1, 2, 2], Int64[1, 1, 1, 1, 1], 4, 2) @@ -1578,7 +1578,7 @@ end show(io, MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring _show_with_braille_patterns(convert(IOContext, io), transform(A)) - @test String(take!(io)) == braille + @test contains(String(take!(io)), braille) end A = sparse(Int64[1, 3, 2, 4], Int64[1, 1, 2, 2], Int64[1, 1, 1, 1], 7, 3) @@ -1597,7 +1597,7 @@ end show(io, MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring _show_with_braille_patterns(convert(IOContext, io), transform(A)) - @test String(take!(io)) == braille + @test contains(String(take!(io)), braille) end A = sparse(Int64[1:10;], Int64[1:10;], fill(Float64(1), 10)) @@ -1606,7 +1606,7 @@ end "⎣⠀⠀⠀⠀⠑⎦" for transform in (identity, adjoint, transpose) _show_with_braille_patterns(convert(IOContext, io), transform(A)) - @test String(take!(io)) == brailleString + @test contains(String(take!(io)), brailleString) end # Issue #30589 @@ -1622,22 +1622,22 @@ end # vertical scaling ioc = IOContext(io, :displaysize => (5, 80), :limit => true) _show_with_braille_patterns(ioc, _filled_sparse(10, 10)) - @test String(take!(io)) == "⎡⣿⣿⎤\n" * - "⎣⣿⣿⎦" + @test contains(String(take!(io)), "⎡⣿⣿⎤\n" * + "⎣⣿⣿⎦") _show_with_braille_patterns(ioc, _filled_sparse(20, 10)) - @test String(take!(io)) == "⎡⣿⣿⎤\n" * - "⎣⣿⣿⎦" + @test contains(String(take!(io)), "⎡⣿⣿⎤\n" * + "⎣⣿⣿⎦") # horizontal scaling ioc = IOContext(io, :displaysize => (80, 4), :limit => true) _show_with_braille_patterns(ioc, _filled_sparse(8, 8)) - @test String(take!(io)) == "⎡⣿⣿⎤\n" * - "⎣⣿⣿⎦" + @test contains(String(take!(io)), "⎡⣿⣿⎤\n" * + "⎣⣿⣿⎦") _show_with_braille_patterns(ioc, _filled_sparse(8, 16)) - @test String(take!(io)) == "⎡⣿⣿⎤\n" * - "⎣⣿⣿⎦" + @test contains(String(take!(io)), "⎡⣿⣿⎤\n" * + "⎣⣿⣿⎦") # respect IOContext while displaying J I, J, V = shuffle(1:50), shuffle(1:50), [1:50;] From 1eb9e694c13e2467c61b22ec56d82368ae459f5a Mon Sep 17 00:00:00 2001 From: rokke <66498307+rokke-git@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:57:14 -0400 Subject: [PATCH 3/4] simplify spy plot + fix aliasing --- src/sparsematrix.jl | 112 +++++---------------- test/sparsematrix_constructors_indexing.jl | 36 ++----- 2 files changed, 36 insertions(+), 112 deletions(-) diff --git a/src/sparsematrix.jl b/src/sparsematrix.jl index 7f144c42..8219f9f8 100644 --- a/src/sparsematrix.jl +++ b/src/sparsematrix.jl @@ -406,106 +406,46 @@ end const brailleBlocks = UInt16['⠁', '⠂', '⠄', '⡀', '⠈', '⠐', '⠠', '⢀'] function _show_with_braille_patterns(io::IO, S::AbstractSparseMatrixCSCInclAdjointAndTranspose) + # The maximum number of characters we allow to display the matrix + h, w = get(io, :limit, false) ? displaysize(io) .- (4, 2) : (typemax(Int)÷4, typemax(Int)÷2) m, n = size(S) - (m == 0 || n == 0) && return show(io, MIME("text/plain"), S) - - # The maximal number of characters we allow to display the matrix - local maxHeight::Int, maxWidth::Int - maxHeight = displaysize(io)[1] - 4 # -4 from [Prompt, header, newline after elements, new prompt] - maxWidth = displaysize(io)[2] - 2 # -2 from brackets warn = false try zero(eltype(S)) catch warn = true - maxHeight -= 1 - end - - # In the process of generating the braille pattern to display the nonzero - # structure of `S`, we need to be able to scale the matrix `S` to a - # smaller matrix with the same aspect ratio as `S`, but fits on the - # available screen space. The size of that smaller matrix is stored - # in the variables `scaleHeight` and `scaleWidth`. If no scaling is needed, - # we can use the size `m × n` of `S` directly. - # We determine if scaling is needed and set the scaling factors - # `scaleHeight` and `scaleWidth` accordingly. Note that each available - # character can contain up to 4 braille dots in its height (⡇) and up to - # 2 braille dots in its width (⠉). - if get(io, :limit, false) && (m > 4maxHeight || n > 2maxWidth) - s = min(2maxWidth / n, 4maxHeight / m) - scaleHeight = floor(Int, s * m) - scaleWidth = floor(Int, s * n) - else - scaleHeight = m - scaleWidth = n - end - - # Make sure that the matrix size is big enough to be able to display all - # the corner border characters - if scaleHeight < 8 - scaleHeight = 8 - end - if scaleWidth < 4 - scaleWidth = 4 + h -= 1 end - # `brailleGrid` is used to store the needed braille characters for - # the matrix `S`. Each row of the braille pattern to print is stored - # in a column of `brailleGrid`. - brailleGrid = fill(UInt16(10240), (scaleWidth - 1) ÷ 2 + 4, (scaleHeight - 1) ÷ 4 + 1) - brailleGrid[1,:] .= '⎢' - brailleGrid[end-1,:] .= '⎥' - brailleGrid[1,1] = '⎡' - brailleGrid[1,end] = '⎣' - brailleGrid[end-1,1] = '⎤' - brailleGrid[end-1,end] = '⎦' - brailleGrid[end, :] .= '\n' - - rvals = rowvals(parent(S)) - rowscale = max(1, scaleHeight - 1) / max(1, m - 1) - colscale = max(1, scaleWidth - 1) / max(1, n - 1) + # In order to prevent aliasing, we only scale down by full integers. + # While 1 to 1/2 is a big jump, it's less noticeable as you get bigger. + # Note each character has 4 dots of height and 2 dots of width. + scale = max(n÷2w, m÷4h) + 1 + char_h, char_w = fld1(m, 4scale), fld1(n, 2scale) - if rowscale != 1 || colscale != 1 - print(io, " (downscaled to fit on screen)") - end + scale != 1 && print(io, " (displaying at 1/$scale scale)") println(io, ":") warn && printstyled(stderr, "WARNING: could not find generic zero for given elements. expect errors and wrong results\n", color=:red) - if isa(S, AbstractSparseMatrixCSC) - @inbounds for j in axes(S,2) - # Scale the column index `j` to the best matching column index - # of a matrix of size `scaleHeight × scaleWidth` - sj = round(Int, (j - 1) * colscale + 1) - for x in nzrange(S, j) - # Scale the row index `i` to the best matching row index - # of a matrix of size `scaleHeight × scaleWidth` - si = round(Int, (rvals[x] - 1) * rowscale + 1) - - # Given the index pair `(si, sj)` of the scaled matrix, - # calculate the corresponding triple `(k, l, p)` such that the - # element at `(si, sj)` can be found at position `(k, l)` in the - # braille grid `brailleGrid` and corresponds to the 1-dot braille - # character `brailleBlocks[p]` - k = (sj - 1) ÷ 2 + 2 - l = (si - 1) ÷ 4 + 1 - p = ((sj - 1) % 2) * 4 + ((si - 1) % 4 + 1) - - brailleGrid[k, l] |= brailleBlocks[p] - end + # Rows of output are cols of `brailleGrid` since julia is column-major + brailleGrid = fill(UInt16(10240), char_w + 3, char_h) + brailleGrid[[1,end-1,end],:] .= ['⎢', '⎥', '\n'] + brailleGrid[[1,end-1],[1,end]] .= ['⎡' '⎣';'⎤' '⎦'] + char_h == 1 && (brailleGrid[[1,end-1],1] .= ['[', ']']) + + rinds = rowvals(parent(S)) + cinds = ColumnIndices(parent(S)) + + if S isa AbstractSparseMatrixCSC + for cords in zip(rinds, cinds) + row, col = fld1.(cords, scale) |>x-> fldmod1.(x, (4, 2)) + brailleGrid[col[1]+1, row[1]] |= brailleBlocks[row[2] + 4(col[2]-1)] end - else - # If `S` is a adjoint or transpose of a sparse matrix we invert the - # roles of the indices `i` and `j` - @inbounds for i = 1:m - si = round(Int, (i - 1) * rowscale + 1) - for x in nzrange(parent(S), i) - sj = round(Int, (rvals[x] - 1) * colscale + 1) - k = (sj - 1) ÷ 2 + 2 - l = (si - 1) ÷ 4 + 1 - p = ((sj - 1) % 2) * 4 + ((si - 1) % 4 + 1) - brailleGrid[k, l] |= brailleBlocks[p] - end + else # swap rows / cols for adj and transpose + for cords in zip(rinds, cinds) + col, row = fld1.(cords, scale) |>x-> fldmod1.(x, (2, 4)) + brailleGrid[col[1]+1, row[1]] |= brailleBlocks[row[2] + 4(col[2]-1)] end end foreach(c -> print(io, Char(c)), @view brailleGrid[1:end-1]) diff --git a/test/sparsematrix_constructors_indexing.jl b/test/sparsematrix_constructors_indexing.jl index 48141b61..f1750c78 100644 --- a/test/sparsematrix_constructors_indexing.jl +++ b/test/sparsematrix_constructors_indexing.jl @@ -1529,12 +1529,7 @@ end "2×1 $Adjoint{Float64, $SparseMatrixCSC{Float64, Int64}} with 2 stored entries:\n 1.0\n 2.0", "2×1 $Transpose{Float64, $SparseMatrixCSC{Float64, Int64}} with 2 stored entries:\n 1.0\n 2.0", ), - ("⎡⠁⠈⎤\n" * - "⎣⠀⠀⎦", - "⎡⠁⠀⎤\n" * - "⎣⡀⠀⎦", - "⎡⠁⠀⎤\n" * - "⎣⡀⠀⎦")) + ("\n[⠉]", "\n[⠃]", "\n[⠃]")) show(io, MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring _show_with_braille_patterns(convert(IOContext, io), transform(A)) @@ -1555,9 +1550,9 @@ end end # empty braille pattern Char(10240) - A = spzeros(Int64, Int64, 4, 2) + A = spzeros(Int64, Int64, 2, 2) for transform in (identity, adjoint, transpose) - expected = "⎡" * Char(10240)^2 * "⎤\n⎣" * Char(10240)^2 * "⎦" + expected = ":\n[" * Char(10240) * "]" _show_with_braille_patterns(convert(IOContext, io), transform(A)) @test contains(String(take!(io)), expected) end @@ -1569,12 +1564,7 @@ end "2×4 $Adjoint{Int64, $SparseMatrixCSC{Int64, Int64}} with 5 stored entries:\n 1 1 ⋅ 1\n ⋅ 1 1 ⋅", "2×4 $Transpose{Int64, $SparseMatrixCSC{Int64, Int64}} with 5 stored entries:\n 1 1 ⋅ 1\n ⋅ 1 1 ⋅", ), - ("⎡⠅⠠⎤\n" * - "⎣⡀⠐⎦", - "⎡⠉⠈⎤\n" * - "⎣⢀⡀⎦", - "⎡⠉⠈⎤\n" * - "⎣⢀⡀⎦")) + ("\n[⡳]", "\n[⠙⠊]", "\n[⠙⠊]")) show(io, MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring _show_with_braille_patterns(convert(IOContext, io), transform(A)) @@ -1590,10 +1580,8 @@ end ), ("⎡⢕⠀⎤\n" * "⎣⠀⠀⎦", - "⎡⢁⢁⠀⠀⎤\n" * - "⎣⠀⠀⠀⠀⎦", - "⎡⢁⢁⠀⠀⎤\n" * - "⎣⠀⠀⠀⠀⎦")) + "[⠑⠑⠀⠀]", + "[⠑⠑⠀⠀]")) show(io, MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring _show_with_braille_patterns(convert(IOContext, io), transform(A)) @@ -1622,22 +1610,18 @@ end # vertical scaling ioc = IOContext(io, :displaysize => (5, 80), :limit => true) _show_with_braille_patterns(ioc, _filled_sparse(10, 10)) - @test contains(String(take!(io)), "⎡⣿⣿⎤\n" * - "⎣⣿⣿⎦") + @test contains(String(take!(io)), "\n[⣿⣿]") _show_with_braille_patterns(ioc, _filled_sparse(20, 10)) - @test contains(String(take!(io)), "⎡⣿⣿⎤\n" * - "⎣⣿⣿⎦") + @test contains(String(take!(io)), "\n[⣿]") # horizontal scaling ioc = IOContext(io, :displaysize => (80, 4), :limit => true) _show_with_braille_patterns(ioc, _filled_sparse(8, 8)) - @test contains(String(take!(io)), "⎡⣿⣿⎤\n" * - "⎣⣿⣿⎦") + @test contains(String(take!(io)), "\n[⠿⠇]") _show_with_braille_patterns(ioc, _filled_sparse(8, 16)) - @test contains(String(take!(io)), "⎡⣿⣿⎤\n" * - "⎣⣿⣿⎦") + @test contains(String(take!(io)), "\n[⠛⠛]") # respect IOContext while displaying J I, J, V = shuffle(1:50), shuffle(1:50), [1:50;] From c44ee6101d1df4496041c9f24b4ae084541748af Mon Sep 17 00:00:00 2001 From: rokke <66498307+rokke-git@users.noreply.github.com> Date: Fri, 24 Jul 2026 02:32:48 -0400 Subject: [PATCH 4/4] =?UTF-8?q?make=20=E2=96=92=E2=96=92=E2=96=92=20much?= =?UTF-8?q?=20harder=20to=20encounter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/sparsematrix.jl | 44 ++++++++++++---------- test/issues.jl | 16 ++++---- test/sparsematrix_constructors_indexing.jl | 17 +++++---- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/sparsematrix.jl b/src/sparsematrix.jl index 8219f9f8..fabd3838 100644 --- a/src/sparsematrix.jl +++ b/src/sparsematrix.jl @@ -350,13 +350,17 @@ function Base.show(io::IO, ::MIME"text/plain", S::AbstractSparseMatrixCSCInclAdj summary(io, S) isempty(S) && return - screen = displaysize(io) - get(io, :limit, false) && screen[1] <= 4 && return print(io, ": …") + if get(io, :limit, false) + screen = get(io, :displaysize, displaysize(io))::Tuple{Int, Int} + screen[1] <= 4 && return print(io, ": …") + else + screen = typemax(Int), typemax(Int) + end show_circular(io, S) && return io = IOContext(io, :compact=>true, :typeinfo=>eltype(S), :SHOWN_SET=>S) - if (screen[1] < size(S, 1) + 4) | (screen[2] < 3size(S, 2)) + if !get(io, :limit, false) || (screen[1] < size(S, 1) + 4) | (screen[2] < 3size(S, 2)) _show_with_braille_patterns(io, S) else _show_with_dotted_zeros(io, S) @@ -407,8 +411,11 @@ end const brailleBlocks = UInt16['⠁', '⠂', '⠄', '⡀', '⠈', '⠐', '⠠', '⢀'] function _show_with_braille_patterns(io::IO, S::AbstractSparseMatrixCSCInclAdjointAndTranspose) # The maximum number of characters we allow to display the matrix - h, w = get(io, :limit, false) ? displaysize(io) .- (4, 2) : (typemax(Int)÷4, typemax(Int)÷2) - m, n = size(S) + h, w = if get(io, :limit, false)::Bool + get(io, :displysize, displaysize(io)) .- (4, 2) + else + typemax(Int)÷4, typemax(Int)÷2 + end::Tuple{Int, Int} warn = false try @@ -421,8 +428,8 @@ function _show_with_braille_patterns(io::IO, S::AbstractSparseMatrixCSCInclAdjoi # In order to prevent aliasing, we only scale down by full integers. # While 1 to 1/2 is a big jump, it's less noticeable as you get bigger. # Note each character has 4 dots of height and 2 dots of width. - scale = max(n÷2w, m÷4h) + 1 - char_h, char_w = fld1(m, 4scale), fld1(n, 2scale) + scale = maximum(cld.(size(S), (4h, 2w))) + char_h, char_w = cld.(size(S), (4scale, 2scale)) scale != 1 && print(io, " (displaying at 1/$scale scale)") println(io, ":") @@ -439,12 +446,12 @@ function _show_with_braille_patterns(io::IO, S::AbstractSparseMatrixCSCInclAdjoi if S isa AbstractSparseMatrixCSC for cords in zip(rinds, cinds) - row, col = fld1.(cords, scale) |>x-> fldmod1.(x, (4, 2)) + row, col = cld.(cords, scale) |>x-> fldmod1.(x, (4, 2)) brailleGrid[col[1]+1, row[1]] |= brailleBlocks[row[2] + 4(col[2]-1)] end else # swap rows / cols for adj and transpose for cords in zip(rinds, cinds) - col, row = fld1.(cords, scale) |>x-> fldmod1.(x, (2, 4)) + col, row = cld.(cords, scale) |>x-> fldmod1.(x, (2, 4)) brailleGrid[col[1]+1, row[1]] |= brailleBlocks[row[2] + 4(col[2]-1)] end end @@ -457,7 +464,7 @@ function _show_with_dotted_zeros(io::IO, S::AbstractSparseMatrixCSCInclAdjointAn (S isa Adjoint) | (S isa Transpose) && ((rows, cols) = (cols, rows)) align = [isassigned(vals, ind) ? alignment(io, vals[ind]) : (3, 3) for ind in eachindex(vals)] - colwidths = [max.((0, 0), align[findall(==(col), cols)]...) for col in axes(S,2)] + colwidths = [maximum.((first,last), Ref(align[findall(==(col), cols)]);init=0) for col in axes(S,2)] displaysize(io)[2] < sum(sum.(colwidths) .+ 2) && return _show_with_braille_patterns(io, S) println(io, ":") @@ -476,26 +483,25 @@ function _show_with_dotted_zeros(io::IO, S::AbstractSparseMatrixCSCInclAdjointAn if isempty(index) # no value here, print an aligned dot l, r = cld(l+r-1, 2) + 1, div(l+r-1, 2) + 1 print(io, " "^l * "⋅" * (col==axes(S,2)[end] ? "" : " "^r)) - else try # print the element with 1 space of buffer on each side + elseif length(index) == 1 # print the element with 1 space of buffer on each side l, r = (l+1, r+1) .- align[index[]] print(io, " "^l) isassigned(vals, index[]) ? show(io, vals[index[]]) : print(io, "#undef") col == axes(S,2)[end] || print(io, " "^r) - catch # if there's 2+ entries, index[] errors. default to summing - # entries, but print red to warn user that something's wrong + else # default to summing entries, but print red to warn user that something's wrong elm = any(!isassigned(vals, ind) for ind in index) ? "#undef" : - try repr(sum(vals[index]); context=:compact=>true) catch e "#NaN" end + try repr(sum(vals[index]); context=io) catch e "#NaN" end - if length(elm) <= l+r # give up on alignment, center item in the column - l, r = cld(l+r-length(elm), 2) + 1, div(l+r-length(elm), 2) + 1 - else # len of new item does not fit in column + l, r = if textwidth(elm) <= l+r+2 # give up on alignment, just center item + cld(l+r-textwidth(elm), 2) + 1, fld(l+r-textwidth(elm), 2) + 1 + else # new item does not fit in column elm = "▒"^(l+r) - l, r = 1, 1 + 1, 1 end printstyled(io, " "^l, elm, color=:red) col == axes(S,2)[end] || print(io, " "^r) - end end + end end row == axes(S,1)[end] || println(io) end diff --git a/test/issues.jl b/test/issues.jl index 7667e88a..74cb5fbd 100644 --- a/test/issues.jl +++ b/test/issues.jl @@ -759,14 +759,14 @@ let @test repr(B) == "sparse([1, 2, 3], [1, 2, 3], $T12960[#undef, #undef, #undef], 3, 3)" @test occursin( " #undef ⋅ ⋅\n ⋅ #undef ⋅\n ⋅ ⋅ #undef", - repr(MIME("text/plain"), B), + sprint(show, MIME("text/plain"), B; context=:limit=>true), ) B[1,2] = T12960() @test repr(B) == "sparse([1, 1, 2, 3], [1, 2, 2, 3], $T12960[#undef, $T12960(), #undef, #undef], 3, 3)" @test occursin( "\n #undef T12960() ⋅\n ⋅ #undef ⋅\n ⋅ ⋅ #undef", - repr(MIME("text/plain"), B), + sprint(show, MIME("text/plain"), B; context=:limit=>true), ) end @@ -823,14 +823,16 @@ end @testset "Issue #618" begin x = SparseMatrixCSC(3, 3, [1, 3, 4, 5], [1, 1, 2, 3], [1.0, 1.0, 1.0, 1.0]) - @test contains(repr(MIME"text/plain"(), x), "2.0") - x = SparseMatrixCSC(3, 3, [1, 3, 4, 5], [1, 1, 2, 3], [7.0, 7.0, 1.0, 1.0]) - @test contains(repr(MIME"text/plain"(), x), "▒▒▒") + @test contains(sprint(show, MIME"text/plain"(), x; context=:limit=>true), "2.0") + x = SparseMatrixCSC(11, 11, [1; 112; 113; [114 for i=1:9]], [[1 for i=1:111]; 2; 3], [[9 for i=1:111]; 1; 1.]) + @test !contains(sprint(show, MIME"text/plain"(), x; context=:limit=>true), "▒▒▒") + x = SparseMatrixCSC(11, 11, [1; 113; 114; [115 for i=1:9]], [[1 for i=1:112]; 2; 3], [[9 for i=1:112]; 1; 1.]) + @test contains(sprint(show, MIME"text/plain"(), x; context=:limit=>true), "▒▒▒") x = SparseMatrixCSC(3, 3, [1, 3, 4, 5], [1, 1, 2, 3], [7.0, 'o', 1.0, 1.0]) - @test contains(repr(MIME"text/plain"(), x), "#NaN") + @test contains(sprint(show, MIME"text/plain"(), x; context=:limit=>true), "#NaN") v = similar(Any[1, 2, 3, 4]); v[2:4] = [1.0, 1.0, 1.0] x = SparseMatrixCSC(3, 3, [1, 3, 4, 5], [1, 1, 2, 3], v) - @test contains(repr(MIME"text/plain"(), x), "#undef") + @test contains(sprint(show, MIME"text/plain"(), x; context=:limit=>true), "#undef") x = SparseMatrixCSC(3, 3, [1, 3, 4, 5], [1, 1, 2, 3], [1, 1, 1, 1]) @test 2 == Matrix(x)[1,1] diff --git a/test/sparsematrix_constructors_indexing.jl b/test/sparsematrix_constructors_indexing.jl index f1750c78..e24971ff 100644 --- a/test/sparsematrix_constructors_indexing.jl +++ b/test/sparsematrix_constructors_indexing.jl @@ -1488,6 +1488,7 @@ end @testset "show" begin io = IOBuffer() + repl = IOContext(io, :limit=>true) A = spzeros(Float64, Int64, 0, 0) for (transform, showstring) in zip( @@ -1496,7 +1497,7 @@ end "0×0 $Adjoint{Float64, $SparseMatrixCSC{Float64, Int64}} with 0 stored entries", "0×0 $Transpose{Float64, $SparseMatrixCSC{Float64, Int64}} with 0 stored entries" )) - show(io, MIME"text/plain"(), transform(A)) + show(repl , MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring end @@ -1507,7 +1508,7 @@ end "1×1 $Adjoint{Float64, $SparseMatrixCSC{Float64, Int64}} with 1 stored entry:\n 1.0", "1×1 $Transpose{Float64, $SparseMatrixCSC{Float64, Int64}} with 1 stored entry:\n 1.0", )) - show(io, MIME"text/plain"(), transform(A)) + show(repl , MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring end @@ -1518,7 +1519,7 @@ end "2×2 $Adjoint{Float32, $SparseMatrixCSC{Float32, Int64}} with 0 stored entries:\n ⋅ ⋅\n ⋅ ⋅", "2×2 $Transpose{Float32, $SparseMatrixCSC{Float32, Int64}} with 0 stored entries:\n ⋅ ⋅\n ⋅ ⋅", )) - show(io, MIME"text/plain"(), transform(A)) + show(repl , MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring end @@ -1530,7 +1531,7 @@ end "2×1 $Transpose{Float64, $SparseMatrixCSC{Float64, Int64}} with 2 stored entries:\n 1.0\n 2.0", ), ("\n[⠉]", "\n[⠃]", "\n[⠃]")) - show(io, MIME"text/plain"(), transform(A)) + show(repl , MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring _show_with_braille_patterns(convert(IOContext, io), transform(A)) @test contains(String(take!(io)), braille) @@ -1565,7 +1566,7 @@ end "2×4 $Transpose{Int64, $SparseMatrixCSC{Int64, Int64}} with 5 stored entries:\n 1 1 ⋅ 1\n ⋅ 1 1 ⋅", ), ("\n[⡳]", "\n[⠙⠊]", "\n[⠙⠊]")) - show(io, MIME"text/plain"(), transform(A)) + show(repl , MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring _show_with_braille_patterns(convert(IOContext, io), transform(A)) @test contains(String(take!(io)), braille) @@ -1582,7 +1583,7 @@ end "⎣⠀⠀⎦", "[⠑⠑⠀⠀]", "[⠑⠑⠀⠀]")) - show(io, MIME"text/plain"(), transform(A)) + show(repl , MIME"text/plain"(), transform(A)) @test String(take!(io)) == showstring _show_with_braille_patterns(convert(IOContext, io), transform(A)) @test contains(String(take!(io)), braille) @@ -1598,7 +1599,7 @@ end end # Issue #30589 - @test repr("text/plain", sparse([true true])) == "1×2 $SparseMatrixCSC{Bool, $Int} with 2 stored entries:\n 1 1" + @test sprint(show, "text/plain", sparse([true true]); context=:limit=>true) == "1×2 $SparseMatrixCSC{Bool, $Int} with 2 stored entries:\n 1 1" function _filled_sparse(m::Integer, n::Integer) C = CartesianIndices((m, n))[:] @@ -1618,7 +1619,7 @@ end # horizontal scaling ioc = IOContext(io, :displaysize => (80, 4), :limit => true) _show_with_braille_patterns(ioc, _filled_sparse(8, 8)) - @test contains(String(take!(io)), "\n[⠿⠇]") + @test contains(String(take!(io)), "\n[⣿⣿]") _show_with_braille_patterns(ioc, _filled_sparse(8, 16)) @test contains(String(take!(io)), "\n[⠛⠛]")