diff --git a/docs/src/index.md b/docs/src/index.md index f76c3c7c..a2d99dbc 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -214,6 +214,11 @@ sparse. A dense argument contributes its nonzeros, and a `Diagonal` or `I` its d [`sparse_hcat`](@ref), [`sparse_vcat`](@ref) and [`sparse_hvcat`](@ref) return a sparse array even when no argument is sparse. +The index type of the result is promoted over the sparse arguments, wherever they are, +and is `Int` when none is sparse. A dense argument, a number or `I` adds none, so with a +dense `D`, `[A32; D]` and `[D; A32]` both have the `Int32` indices of `A32`. A result too +large for that index type throws an `ArgumentError`. + #### [Reductions](@id man-sparse-reductions) `sum(A)` and the other reductions over all entries visit the stored entries only. Along a diff --git a/src/SparseArrays.jl b/src/SparseArrays.jl index 5c8ce1dd..20110d14 100644 --- a/src/SparseArrays.jl +++ b/src/SparseArrays.jl @@ -122,9 +122,13 @@ are sorted, unique and in `1:size(S, 1)`. Everything else, such as [`nnz`](@ref) [`nzrange`](@ref), indexing, `copy`, `similar` and the sparse linear algebra, is derived from these. [`rowvals`](@ref) and [`nonzeros`](@ref) are not defined for such a subtype. -Subtypes written while `rowvals` and `nonzeros` were the only exported accessors implement -those in place of `getrowval` and `getnzval`, which fall back to them. This remains -supported, but is planned to stop being supported in SparseArrays 2.0. +!!! note "Legacy interface" + A subtype that implements `rowvals` and `nonzeros` in place of `getrowval` and + `getnzval` uses the legacy interface, which is deprecated. It keeps working, because + `getrowval` and `getnzval` fall back to `rowvals` and `nonzeros`, with a deprecation + warning when Julia runs with `--depwarn=yes` and an error with `--depwarn=error`. The + fallback will be removed in SparseArrays 2.0. To migrate, rename the two methods to + `SparseArrays.getrowval` and `SparseArrays.getnzval`. """ abstract type AbstractSparseMatrixCSC{Tv,Ti<:Integer} <: AbstractSparseMatrix{Tv,Ti} end @@ -156,9 +160,10 @@ const SparseMatrixCSCSubArray{Tv,Ti} = SubArray{Tv,2,<:AbstractSparseMatrixCSC{T const SparseMatrixCSCOrSubArray{Tv,Ti} = Union{AbstractSparseMatrixCSC{Tv,Ti}, SparseMatrixCSCSubArray{Tv,Ti}} # Whole-column views of sparse matrices and whole views of sparse vectors share the -# sparse vector interface. +# sparse vector interface. The length of a `SparseVector` has its index type, so a whole +# view of one slices a `OneTo{Ti}`. const SparseColumnView{Tv,Ti} = SubArray{Tv,1,<:AbstractSparseMatrixCSC{Tv,Ti},Tuple{Base.Slice{Base.OneTo{Int}},Int},false} -const SparseVectorView{Tv,Ti} = SubArray{Tv,1,<:AbstractSparseVector{Tv,Ti},Tuple{Base.Slice{Base.OneTo{Int}}},false} +const SparseVectorView{Tv,Ti} = SubArray{Tv,1,<:AbstractSparseVector{Tv,Ti},<:Tuple{Base.Slice{<:Base.OneTo}},false} const SparseVectorOrView{Tv,Ti} = Union{AbstractCompressedVector{Tv,Ti}, SparseColumnView{Tv,Ti}, SparseVectorView{Tv,Ti}} const AdjOrTransSparseVectorOrView{Tv,Ti} = AdjOrTrans{Tv, <:SparseVectorOrView{Tv,Ti}} # view of a unit range of a sparse vector's indices @@ -204,7 +209,8 @@ const LinAlgLeftQs = Union{HessenbergQ,QRCompactWYQ,QRPackedQ} # transpose of the operand. Sparse matrices and their views, with or without an adjoint or # transpose, and a transposed sparse vector as a one-row matrix; and sparse vectors and their # views. An adjoint sparse vector is left to LinearAlgebra's generic (Q' * u')', which keeps -# the result an Adjoint as for a dense vector. +# the result an Adjoint as for a dense vector. `spdiagm` reads the vector operands by their +# stored entries. const SparseQMatOperand = Union{SparseMatrixCSCOrView, AdjOrTrans{<:Any,<:SparseMatrixCSCOrView}, Transpose{<:Any,<:SparseVectorOrView}} const SparseQVecOperand = Union{AbstractSparseVector, SparseVectorOrView, SparseVectorPartialView} @@ -376,7 +382,11 @@ julia> collect(SparseArrays.iternz(x)) """ function iternz end -widelength(x::AbstractSparseArray) = prod(Int64.(size(x))) +widelength(x::AbstractSparseArray) = *(map(widen, size(x))...) +# Linear indexing compares an index with `length(A)`, which wraps for 2^63 entries or more +_checklinearindexing(A::AbstractSparseMatrixCSC) = widelength(A) <= typemax(Int) || + throw(OverflowError(string("a ", size(A, 1), "×", size(A, 2), " sparse matrix has more ", + "entries than an `Int` can index linearly; index it by row and column"))) const _restore_scalar_indexing = Expr[] diff --git a/src/concatenation.jl b/src/concatenation.jl index 54c0b409..73d441a6 100644 --- a/src/concatenation.jl +++ b/src/concatenation.jl @@ -24,6 +24,7 @@ function _vcat_csc(::Type{Tv}, ::Type{Ti}, X::AbstractSparseMatrixCSC...) where nnzX = Int[ nnz(x) for x in X ] nnz_res = sum(nnzX) + _checknnzfits(Ti, nnz_res) colptr = Vector{Ti}(undef, n+1) rowval = Vector{Ti}(undef, nnz_res) nzval = Vector{Tv}(undef, nnz_res) @@ -72,6 +73,7 @@ function hcat(X::AbstractSparseMatrixCSC...) colptr = Vector{Ti}(undef, n+1) nnzX = Int[ nnz(x) for x in X ] nnz_res = sum(nnzX) + _checknnzfits(Ti, nnz_res) rowval = Vector{Ti}(undef, nnz_res) nzval = Vector{Tv}(undef, nnz_res) @@ -99,6 +101,7 @@ end function Base.repeat(A::AbstractSparseMatrixCSC, m) nnz_new = nnz(A) * m + _checknnzfits(indtype(A), nnz_new) colptr = similar(getcolptr(A), length(getcolptr(A))) rowval = similar(getrowval(A), nnz_new) nzval = similar(getnzval(A), nnz_new) @@ -115,12 +118,12 @@ function Base.repeat(A::AbstractSparseMatrixCSC, m) end colptr[c + 1] = ptr_res end - @assert colptr[end] == nnz_new + 1 SparseMatrixCSC(size(A, 1) * m, size(A, 2), colptr, rowval, nzval) end function Base.repeat(A::AbstractSparseMatrixCSC, m, n) + _checknnzfits(indtype(A), nnz(A) * m * n) B = repeat(A, m) nB = size(B, 2) nnzB = nnz(B) @@ -221,6 +224,7 @@ function Base._typed_stack(::Colon, ::Type{Tv}, ::Type{S}, A, Aax::Tuple{Any}) w throw(DimensionMismatch("Inconsistent column lengths.")) tnnz += nnz(x) end + _checknnzfits(Ti, tnnz) colptr = Vector{Ti}(undef, n+1) nzrow = Vector{Ti}(undef, tnnz) @@ -288,16 +292,15 @@ end # Concatenations involving un/annotated sparse/special matrices/vectors should yield sparse arrays -# the output array type is determined by the first element of the to be concatenated objects -# if this is a Number, the output would be dense by the fallback abstractarray.jl code (see cat_similar) -# so make sure that if that happens, the "array" is sparse (if more sparse arrays are involved, of course) +# `_sparse` makes the first argument of a concatenation that has to be sparse, with a +# number standing for a one-element vector _sparse(x::Number) = sparsevec([1], [x], 1) _sparse(A) = _makesparse(A) _makesparse(x::Number) = x _makesparse(x::AbstractVector) = convert(SparseVector, x)::SparseVector _makesparse(x::AbstractMatrix) = convert(SparseMatrixCSC, x)::SparseMatrixCSC # a `UniformScaling` has no size of its own: `LinearAlgebra._hcat`/`_vcat`/`_hvcat` size it -# from its neighbours and then call `promote_to_arrays_` below to make it sparse +# from its neighbours and then call `promote_to_arrays_` below to make it a `Diagonal` _makesparse(J::UniformScaling) = J anysparse() = false anysparse(X) = X isa AbstractArray && issparse(X) @@ -317,13 +320,13 @@ _densesparse(x) = anysparse(x) ? Array(x) : x const _SparseVecConcatGroup = Union{Vector, AbstractSparseVector} function hcat(X::_SparseVecConcatGroup...) if _concatsparse(X...) - return cat(map(sparse, X)...; dims=Val(2)) + return _sparse_cat_t(Val(2), promote_eltype(X...), X...) end return cat(map(_densesparse, X)...; dims=Val(2)) end function vcat(X::_SparseVecConcatGroup...) if _concatsparse(X...) - return cat(map(sparse, X)...; dims=Val(1)) + return _sparse_cat_t(Val(1), promote_eltype(X...), X...) end return cat(map(_densesparse, X)...; dims=Val(1)) end @@ -335,22 +338,30 @@ end # ambiguity is introduced for arrays that are not sparse (#431). const _SparseConcatGroup = Union{AbstractVecOrMat,Number} -# Base's `_cat_t` takes the output type from its first argument, so with a leading number -# it would build a dense array. Choose the destination from the first array instead, and -# keep the number as is so that it fills its block the way it does in dense -# concatenation (#383). A leading number still widens the index type to at least `Int`, -# as the one-element sparse vector standing in for it used to. `X` has already been -# through `_makesparse`. -_catleader(X1::AbstractArray, X...) = X1 -_catleader(X1::Number, X...) = _catleader(X...) -_catleader(X1::Number) = _sparse(X1) -_catdest(::Type{T}, shape, X1::AbstractArray, X...) where {T} = similar(X1, T, shape) -function _catdest(::Type{T}, shape, X1::Number, X...) where {T} - A = _catleader(X1, X...) - return similar(A, T, promote_type(Int, indtype(A)), shape) -end -_catidxtype(X1::AbstractArray, X...) = indtype(X1) -_catidxtype(X1::Number, X...) = promote_type(Int, indtype(_catleader(X1, X...))) +# The index type of a concatenation is promoted over its sparse arguments, wherever they +# are, and is `Int` when none is sparse. A dense array, a number, a structured matrix or a +# `UniformScaling` has no index type of its own, so it adds none, and the result does not +# depend on the order of the arguments. The size of the result is checked against it. +function _catidxtype(X::Vararg{Any,N}) where {N} + Ti = _sparseidxtype(X...) + return Ti === Union{} ? Int : Ti +end +_sparseidxtype() = Union{} +_sparseidxtype(x, X::Vararg{Any,N}) where {N} = + promote_type(anysparse(x) ? _wrappedidxtype(x) : Union{}, _sparseidxtype(X...)) +_wrappedidxtype(x::AbstractSparseArray) = indtype(x) +function _wrappedidxtype(x::AbstractArray) + p = parent(x) + return p === x ? indtype(_makesparse(x)) : _wrappedidxtype(p) +end +# The destination of a vector result is made here and not by Base's `_cat_t`, which takes +# its type from the first argument and so would build a dense array after a leading +# number. A number is kept as is, so that it fills its block as in dense concatenation (#383). +function _catvector(::Type{T}, n, Xin::Tuple) where {T} + Ti = _catidxtype(Xin...) + sparse_check_Ti(n, 1, Ti) + return spzeros(T, Ti, n) +end # The compiled method serves every value of `dims`, so `dims2cat(dims)` has an unknown # length, and `similar` of a sparse array cannot be resolved for a shape of unknown length, # which `juliac --trim` rejects. A sparse result has one or two dimensions, so each gets a @@ -367,10 +378,11 @@ end # Base's `__cat` writes each block into the destination with `setindex!`, which does not # store a zero, so a matrix result is assembled from the stored entries of the blocks. It # keeps the stored zeros of a sparse block, as `hcat` and `vcat` of sparse matrices do. -function _sparse_cat_t_shaped(catdims::Tuple{Vararg{Bool}}, ::Type{T}, X::Vararg{Any,N}) where {T,N} +function _sparse_cat_t_shaped(catdims::Tuple{Vararg{Bool}}, ::Type{T}, Xin::Vararg{Any,N}) where {T,N} + X = map(_makesparse, Xin) shape = Base.cat_size_shape(catdims, X...) - length(shape) == 1 && return Base.__cat(_catdest(T, shape, X...), shape, catdims, X...) - Ti = _catidxtype(X...) + length(shape) == 1 && return Base.__cat(_catvector(T, shape[1], Xin), shape, catdims, X...) + Ti = _catidxtype(Xin...) sparse_check_Ti(shape[1], shape[2], Ti) vertical, horizontal = catdims[1], length(catdims) == 2 && catdims[2] # a number fills its block, which spans the result in a dimension not concatenated @@ -393,9 +405,6 @@ end _catdense(x::AbstractArray) = Array(x) _catdense(x) = x _sparse_typed_hcat(::Type{T}, X...) where {T} = _sparse_cat_t(Val(2), T, X...) -# with only vectors the result is a vector, which Base's `typed_vcat` builds by assigning -# whole sparse vectors, which keeps their stored zeros -_sparse_typed_vcat(::Type{T}, X::AbstractVector...) where {T} = Base.typed_vcat(T, X...) _sparse_typed_vcat(::Type{T}, X...) where {T} = _sparse_cat_t(Val(1), T, X...) # `Vararg{_SparseConcatGroup,N}` makes Julia compile `cat_internal` for each argument @@ -405,51 +414,42 @@ _sparse_typed_vcat(::Type{T}, X...) where {T} = _sparse_cat_t(Val(1), T, X...) Base.@constprop :aggressive function cat_internal(dims, X1::_SparseConcatGroup, X::Vararg{_SparseConcatGroup,N}) where {N} T = promote_eltype(X1, X...) if _concatsparse(X1, X...) - return _sparse_cat_t(dims, T, _makesparse(X1), map(_makesparse, X)...) + return _sparse_cat_t(dims, T, X1, X...) end return Base._cat_t(dims, T, _densesparse(X1), map(_densesparse, X)...) end function hcat_internal(X1::_SparseConcatGroup, X::_SparseConcatGroup...) T = promote_eltype(X1, X...) if _concatsparse(X1, X...) - return _sparse_typed_hcat(T, _makesparse(X1), map(_makesparse, X)...) + return _sparse_typed_hcat(T, X1, X...) end return Base.typed_hcat(T, _densesparse(X1), map(_densesparse, X)...) end function vcat_internal(X1::_SparseConcatGroup, X::_SparseConcatGroup...) T = promote_eltype(X1, X...) if _concatsparse(X1, X...) - return _sparse_typed_vcat(T, _makesparse(X1), map(_makesparse, X)...) + return _sparse_typed_vcat(T, X1, X...) end return Base.typed_vcat(T, _densesparse(X1), map(_densesparse, X)...) end # `Vararg{_SparseConcatGroup,N}` for the same reason as in `cat_internal` function hvcat_internal(rows::Tuple{Vararg{Int}}, X1::_SparseConcatGroup, X::Vararg{_SparseConcatGroup,N}) where {N} if _concatsparse(X1, X...) - return _sparse_hvcat(rows, _makesparse(X1), map(_makesparse, X)...) + return _sparse_hvcat(rows, X1, X...) end return Base.typed_hvcat(Base.promote_eltypeof(X1, X...), rows, _densesparse(X1), map(_densesparse, X)...) end # `_hvcat_csc` reads the blocks by index. Splitting them into a tuple per block row would # give tuples whose length depends on the value of `rows`, so their types could not be # inferred and `juliac --trim` could not resolve the calls on them. -function _sparse_hvcat(rows::Tuple{Vararg{Int}}, X::Vararg{AbstractSparseMatrixCSC,N}) where {N} - return _hvcat_csc(promote_eltype(X...), promote_idxtype(X...), rows, X...) -end # A vector is an `n×1` block and a number a `1×1` block, as in dense `hvcat`. A number is # stored unless scalar `setindex!` would leave it implicit, and a sparse vector keeps its -# stored zeros, as sparse matrix blocks do. A leading number widens the index type to at -# least `Int`, as it does for `hcat` and `vcat` (#383). -function _sparse_hvcat(rows::Tuple{Vararg{Int}}, X::Vararg{Any,N}) where {N} - Tv = promote_eltype(X...) - Ti = _hvcat_idxtype(X...) - return _hvcat_csc(Tv, Ti, rows, map(x -> _hvcat_block(Tv, Ti, x), X)...) -end -_hvcat_idxtype(X1::Number, X...) = promote_type(Int, _blocks_idxtype(X...)) -_hvcat_idxtype(X...) = _blocks_idxtype(X...) -_blocks_idxtype() = Union{} -_blocks_idxtype(x::Number, X...) = _blocks_idxtype(X...) -_blocks_idxtype(x::AbstractSparseArray, X...) = promote_type(indtype(x), _blocks_idxtype(X...)) +# stored zeros, as sparse matrix blocks do. +function _sparse_hvcat(rows::Tuple{Vararg{Int}}, Xin::Vararg{Any,N}) where {N} + Tv = promote_eltype(Xin...) + Ti = _catidxtype(Xin...) + return _hvcat_csc(Tv, Ti, rows, map(x -> _hvcat_block(Tv, Ti, _makesparse(x)), Xin)...) +end _hvcat_block(::Type, ::Type, B::AbstractSparseMatrixCSC) = B function _hvcat_block(::Type, ::Type, x::AbstractSparseVector{<:Any,Ti}) where {Ti} return SparseMatrixCSC(length(x), 1, Ti[1, nnz(x) + 1], nonzeroinds(x), nonzeros(x)) @@ -593,9 +593,17 @@ function Base.repeat(v::AbstractSparseVector, m, n) end -# make sure UniformScaling objects are converted to sparse matrices for concatenation +# A `UniformScaling` in a sparse concatenation becomes a `Diagonal`, which the concatenation +# makes sparse. As a structured block, not a sparse one, it does not widen the index type. promote_to_array_type(A::Tuple{Vararg{Union{_SparseConcatGroup,UniformScaling}}}) = _concatsparse(A...) ? SparseMatrixCSC : Matrix -promote_to_arrays_(n::Int, ::Type{SparseMatrixCSC}, J::UniformScaling) = sparse(J, n, n) +promote_to_arrays_(n::Int, ::Type{SparseMatrixCSC}, J::UniformScaling) = Diagonal(fill(J.λ, n)) +# `LinearAlgebra` sizes the `UniformScaling`s and concatenates the result, which is sparse +# only if an argument is. When none is, the first array or number is made sparse, with the +# `Int` indices that the index type of the result has then anyway. +_withsparsefirst(X::Vararg{Any,N}) where {N} = anysparse(X...) ? X : _sparsefirst(X...) +_sparsefirst() = () +_sparsefirst(J::UniformScaling, X::Vararg{Any,N}) where {N} = (J, _sparsefirst(X...)...) +_sparsefirst(x, X::Vararg{Any,N}) where {N} = (_sparse(x), X...) """ sparse_hcat(A...) @@ -607,9 +615,9 @@ Concatenate along dimension 2. Return a SparseMatrixCSC object. the concatenation with specialized "sparse" matrix types from LinearAlgebra.jl automatically yielded sparse output even in the absence of any SparseArray argument. """ -sparse_hcat(Xin::Union{AbstractVecOrMat,Number}...) = _sparse_cat_t(Val(2), promote_eltype(Xin...), map(_makesparse, Xin)...) +sparse_hcat(Xin::Union{AbstractVecOrMat,Number}...) = _sparse_cat_t(Val(2), promote_eltype(Xin...), Xin...) function sparse_hcat(X::Union{AbstractVecOrMat,UniformScaling,Number}...) - LinearAlgebra._hcat(_sparse(first(X)), map(_makesparse, Base.tail(X))...; array_type = SparseMatrixCSC) + LinearAlgebra._hcat(_withsparsefirst(X...)...; array_type = SparseMatrixCSC) end """ @@ -622,9 +630,9 @@ Concatenate along dimension 1. Return a SparseMatrixCSC object. the concatenation with specialized "sparse" matrix types from LinearAlgebra.jl automatically yielded sparse output even in the absence of any SparseArray argument. """ -sparse_vcat(Xin::Union{AbstractVecOrMat,Number}...) = _sparse_cat_t(Val(1), promote_eltype(Xin...), map(_makesparse, Xin)...) +sparse_vcat(Xin::Union{AbstractVecOrMat,Number}...) = _sparse_cat_t(Val(1), promote_eltype(Xin...), Xin...) function sparse_vcat(X::Union{AbstractVecOrMat,UniformScaling,Number}...) - LinearAlgebra._vcat(_sparse(first(X)), map(_makesparse, Base.tail(X))...; array_type = SparseMatrixCSC) + LinearAlgebra._vcat(_withsparsefirst(X...)...; array_type = SparseMatrixCSC) end """ @@ -639,9 +647,7 @@ arguments to concatenate in each block row. the concatenation with specialized "sparse" matrix types from LinearAlgebra.jl automatically yielded sparse output even in the absence of any SparseArray argument. """ -function sparse_hvcat(rows::Tuple{Vararg{Int}}, Xin::Union{AbstractVecOrMat,Number}...) - hvcat(rows, _sparse(first(Xin)), map(_makesparse, Base.tail(Xin))...) -end +sparse_hvcat(rows::Tuple{Vararg{Int}}, Xin::Union{AbstractVecOrMat,Number}...) = _sparse_hvcat(rows, Xin...) function sparse_hvcat(rows::Tuple{Vararg{Int}}, X::Union{AbstractVecOrMat,UniformScaling,Number}...) - LinearAlgebra._hvcat(rows, _sparse(first(X)), map(_makesparse, Base.tail(X))...; array_type = SparseMatrixCSC) + LinearAlgebra._hvcat(rows, _withsparsefirst(X...)...; array_type = SparseMatrixCSC) end diff --git a/src/constructors.jl b/src/constructors.jl index ca8db3dd..9eebafe4 100644 --- a/src/constructors.jl +++ b/src/constructors.jl @@ -429,7 +429,7 @@ preceding CSR form into a fully-sorted CSC form with no repeated entries. Input arrays `csrrowptr`, `csrcolval`, and `csrnzval` constitute storage for the intermediate CSR forms and require `length(csrrowptr) >= m + 1`, -`length(csrcolval) >= length(I)`, and `length(csrnzval >= length(I))`. Input +`length(csrcolval) >= length(I)`, and `length(csrnzval) >= length(I)`. Input array `klasttouch`, workspace for the second stage, requires `length(klasttouch) >= n`. Optional input arrays `csccolptr`, `cscrowval`, and `cscnzval` constitute storage for the returned CSC form `S`. If necessary, these are resized automatically to satisfy @@ -447,8 +447,8 @@ You may reuse the input arrays' storage (`I`, `J`, `V`) for the output arrays Note that they will be resized to satisfy the conditions above. For the sake of efficiency, this method performs no argument checking beyond -`1 <= I[k] <= m` and `1 <= J[k] <= n`. Use with care. Testing with `--check-bounds=yes` -is wise. +`1 <= I[k] <= m`, `1 <= J[k] <= n` and the lengths of the input arrays. Use with care. +Testing with `--check-bounds=yes` is wise. This method runs in `O(m, n, length(I))` time. The HALFPERM algorithm described in F. Gustavson, "Two fast algorithms for sparse matrices: multiplication and permuted @@ -470,11 +470,16 @@ function sparse!(I::AbstractVector{Ti}, J::AbstractVector{Ti}, V::AbstractVector # alias cscnzval, which will be resized and filled with zero(Tv). only_sparsity_pattern = combine === nothing - # Compute the CSR form's row counts and store them shifted forward by one in csrrowptr - fill!(csrrowptr, Tj(0)) coolen = length(I) length(J) >= coolen || throw(ArgumentError("J need length >= length(I) = $coolen")) only_sparsity_pattern || length(V) >= coolen || throw(ArgumentError("V need length >= length(I) = $coolen")) + sparse_check_length("klasttouch", klasttouch, n, String) + sparse_check_length("csrrowptr", csrrowptr, m + 1, String) + sparse_check_length("csrcolval", csrcolval, coolen, String) + only_sparsity_pattern || sparse_check_length("csrnzval", csrnzval, coolen, String) + + # Compute the CSR form's row counts and store them shifted forward by one in csrrowptr + fill!(csrrowptr, Tj(0)) @inbounds for k in 1:coolen Ik = I[k] @@ -692,6 +697,7 @@ You can sample nonzero values from a different distribution by passing a custom `rfn` function instead of `rand`. This should be a function `rfn(k)` that returns an array of `k` random numbers sampled from the desired distribution; alternatively, if `rng` is supplied, it should instead be a function `rfn(rng, k)`. +Passing `T` after `rfn` converts the values that `rfn` returns to `T`. # Examples ```jldoctest; setup = :(using Random; Random.seed!(1234)) @@ -706,16 +712,30 @@ julia> sprand(Float64, 3, 0.75) [2] = 0.49425 ``` """ -function sprand(r::AbstractRNG, m::Integer, n::Integer, density::AbstractFloat, rfn::Function, ::Type{T}=eltype(rfn(r, 1))) where T +function sprand(r::AbstractRNG, m::Integer, n::Integer, density::AbstractFloat, rfn::Function, ::Type{T}) where T + m, n = Int(m), Int(n) + I, V = _sprandvalues(r, m, n, density, rfn) + return sparse_sortedlinearindices!(I, convert(Vector{T}, V), m, n) +end +function sprand(r::AbstractRNG, m::Integer, n::Integer, density::AbstractFloat, rfn::Function) m, n = Int(m), Int(n) + I, V = _sprandvalues(r, m, n, density, rfn) + return sparse_sortedlinearindices!(I, convert(Vector{eltype(V)}, V), m, n) +end +function _sprandvalues(r::AbstractRNG, m::Int, n::Int, density::AbstractFloat, rfn::F) where {F} (m < 0 || n < 0) && throw(ArgumentError("invalid Array dimensions")) 0 <= density <= 1 || throw(ArgumentError("$density not in [0,1]")) + # the entries are drawn as linear indices, which wrap for 2^63 entries or more + Base.mul_with_overflow(m, n)[2] && throw(OverflowError(string("a ", m, "×", n, + " sparse matrix has more entries than `sprand` can draw from"))) I = randsubseq(r, 1:(m*n), density) - return sparse_sortedlinearindices!(I, convert(Vector{T}, rfn(r,length(I))), m, n) + return I, rfn(r, length(I)) end -sprand(m::Integer, n::Integer, density::AbstractFloat, rfn::Function, ::Type{T} = eltype(rfn(1))) where {T} = +sprand(m::Integer, n::Integer, density::AbstractFloat, rfn::Function) = sprand(default_rng(), m, n, density, (r, i) -> rfn(i)) +sprand(m::Integer, n::Integer, density::AbstractFloat, rfn::Function, ::Type{T}) where {T} = + sprand(default_rng(), m, n, density, (r, i) -> rfn(i), T) truebools(r::AbstractRNG, n::Integer) = fill(true, n) @@ -908,10 +928,10 @@ sparse(s::UniformScaling, m::Integer, n::Integer) = sparse(s, Dims((m, n))) sparse(::Type{Tv}, s::UniformScaling, m::Integer, n::Integer) where {Tv} = SparseMatrixCSC{Tv}(s, Dims((m, n))) sparse(::Type{Tv}, ::Type{Ti}, s::UniformScaling, m::Integer, n::Integer) where {Tv, Ti} = SparseMatrixCSC{Tv, Ti}(s, Dims((m, n))) -_nnz(v::AbstractSparseVector) = nnz(v) +_nnz(v::SparseQVecOperand) = nnz(v) _nnz(v::AbstractVector) = length(v) -function _indices(v::AbstractSparseVector, row, col) +function _indices(v::SparseQVecOperand, row, col) ix = nonzeroinds(v) return (row .+ ix, col .+ ix) end @@ -920,7 +940,7 @@ function _indices(v::AbstractVector, row, col) return (row+1:row+veclen, col+1:col+veclen) end -_nzvals(v::AbstractSparseVector) = nonzeros(v) +_nzvals(v::SparseQVecOperand) = nonzeros(v) _nzvals(v::AbstractVector) = v # Promoted element type of the diagonals, mirroring `Base.promote_eltypeof` diff --git a/src/higherorderfns.jl b/src/higherorderfns.jl index 3f47cdaa..be693c63 100644 --- a/src/higherorderfns.jl +++ b/src/higherorderfns.jl @@ -1216,7 +1216,7 @@ end # broadcast entry points for combinations of sparse arrays and other (scalar) types @inline function copy(bc::Broadcasted{<:SparseVecOrMatStyle}) - _isdensesum(bc) && return _densebroadcast(bc) + _isdensesum(bc) && return _densesum(bc) bcf = flatten(bc) _asksthezeros(bc) && _fillszeros(bcf.f, bcf.args...) && return _densenonzero(bc, bcf.f, bcf.args...) return _copy(bcf.f, bcf.args...) @@ -1360,7 +1360,7 @@ broadcast(f::Tf, A::AbstractSparseMatrixCSC, ::Type{T}) where {Tf,T} = broadcast # which is sparse, and inferable, for every element type. function copy(bc::Broadcasted{PromoteToSparse}) - _isdensesum(bc) && return _densebroadcast(bc) + _isdensesum(bc) && return _densesum(bc) bcf = flatten(bc) _asksthezeros(bc) && _fillszeros(bcf.f, bcf.args...) && return _densenonzero(bc, bcf.f, bcf.args...) if can_skip_sparsification(bcf.f, bcf.args...) @@ -1382,6 +1382,78 @@ end end _isdensesum(bc::Broadcasted) = _issumtree(bc) && _anydensifying(bc.args...) + +# A sum of sparse arrays and scalars with a dense result, such as `A .+ 1`, skips the dense +# copy of each sparse argument. It broadcasts once with every sparse argument replaced by its +# implicit zero, which is already right wherever no stored entry reaches, then recomputes the +# entries that a stored entry reaches. The function is made of `+` and `-`, so computing an +# entry twice is harmless. The rest goes to `_densebroadcast`: sums with a dense array, where +# the copy costs little, non-`Number` elements, whose zero may be of another type, and results +# whose element type depends on the values. +function _densesum(bc::Broadcasted) + ax = axes(bc) + (any(isempty, ax) || !_sparseandscalars(bc)) && return _densebroadcast(bc) + # recomputing an entry looks up every sparse argument, which costs about as much as copying + # 32 entries, so beyond that many reached entries the dense copy is cheaper + len = prod(map(length, ax)) + nsparse, reached = _sparsereach(len, bc.args...) + 32 * nsparse * reached <= len || return _densebroadcast(bc) + zbc = Broadcasted{Broadcast.DefaultArrayStyle{length(ax)}}(bc.f, map(_zerosparse, bc.args), ax) + # the zeros alone must give the element type that dense broadcast infers from all the values + T = Broadcast.combine_eltypes(zbc.f, zbc.args) + isconcretetype(T) && T === Broadcast.combine_eltypes(bc.f, bc.args) || return _densebroadcast(bc) + C = copy(Broadcast.instantiate(zbc)) + return _storedentries!(C, bc, bc.args...) +end +_zerosparse(bc::Broadcasted) = Broadcasted(bc.f, map(_zerosparse, bc.args)) +_zerosparse(S::SparseVecOrMat) = _densezero(S) +_zerosparse(x) = x +# the number of sparse arguments, and of the result entries their stored entries reach +_sparsereach(len) = (0, 0) +function _sparsereach(len, x, rest::Vararg{Any,N}) where {N} + n, r = _sparsereach(len, x) + n2, r2 = _sparsereach(len, rest...) + return (n + n2, r + r2) +end +_sparsereach(len, x::Broadcasted) = _sparsereach(len, x.args...) +_sparsereach(len, S::SparseVecOrMat) = (1, nnz(S) * (len ÷ length(S))) +_sparsereach(len, x) = (0, 0) +_sparseandscalars(bc::Broadcasted) = all(_sparseandscalars, bc.args) +_sparseandscalars(S::SparseVecOrMat) = eltype(S) <: Number +_sparseandscalars(x::Union{AbstractArray,Tuple}) = false +_sparseandscalars(x) = true + +# recompute the entries of `C` that the stored entries of the sparse arguments reach +_storedentries!(C, bc) = C +function _storedentries!(C, bc, x, rest::Vararg{Any,N}) where {N} + _storedentries!(C, bc, x) + return _storedentries!(C, bc, rest...) +end +_storedentries!(C, bc, x::Broadcasted) = _storedentries!(C, bc, x.args...) +_storedentries!(C, bc, x) = C +function _storedentries!(C, bc, S::AbstractCompressedVector) + for i in nonzeroinds(S) + _reevaluate!(C, bc, S, (Int(i),)) + end + return C +end +function _storedentries!(C, bc, S::AbstractSparseMatrixCSC) + rv = getrowval(S) + for j in axes(S, 2), k in nzrange(S, j) + _reevaluate!(C, bc, S, (Int(rv[k]), j)) + end + return C +end +# the stored entry at `I` of `S` reaches all of `C` along the dimensions where `S` has size 1, +# and index `I[d]` along each other dimension `d` +function _reevaluate!(C, bc, S, I::Tuple{Vararg{Int}}) + ranges = ntuple(d -> size(S, d) == 1 ? UnitRange(axes(C, d)) : (I[d]:I[d]), Val(ndims(C))) + for J in CartesianIndices(ranges) + C[J] = bc[J] + end + return C +end + function _densebroadcast(bc::Broadcasted) # an empty result reads no entry, so the sparse arguments are left as they are dbc = any(isempty, axes(bc)) ? bc : Broadcast.instantiate(_densifysparse(bc)) diff --git a/src/indexing.jl b/src/indexing.jl index 0838852c..c0cda6d3 100644 --- a/src/indexing.jl +++ b/src/indexing.jl @@ -35,8 +35,10 @@ _integer_indices(I::AbstractArray{CartesianIndex{1}}) = map(first ∘ Tuple, I) # Index types no method below matches (`CartesianIndex`, custom indices) are lowered and # dispatched again; indices that are already lowered fall back to Base. -@propagate_inbounds getindex(A::AbstractSparseMatrixCSC{Tv,Ti}, I...) where {Tv,Ti} = - _getindex_lowered(A, I, _lower_indices(A, I...)) +@propagate_inbounds function getindex(A::AbstractSparseMatrixCSC{Tv,Ti}, I...) where {Tv,Ti} + I isa Tuple{Integer} && _checklinearindexing(A) + return _getindex_lowered(A, I, _lower_indices(A, I...)) +end @propagate_inbounds getindex(x::AbstractSparseVector{Tv,Ti}, I...) where {Tv,Ti} = _getindex_lowered(x, I, _lower_indices(x, I...)) @propagate_inbounds _getindex_lowered(A, I, L) = A[L...] @@ -503,6 +505,7 @@ end function getindex(A::AbstractSparseMatrixCSC{Tv,Ti}, I::AbstractArray) where {Tv,Ti} require_one_based_indexing(A, I) + eltype(I) <: Integer && _checklinearindexing(A) ndims(I) == 2 || return _getindex_dense(A, I) szA = size(A) nA = szA[1]*szA[2] @@ -596,6 +599,13 @@ _checkscalarindex(::Integer) = nothing # dispatch helper for #29034 @RCI setindex!(A::AbstractSparseMatrixCSC, _v, _i::Integer, _j::Integer) = _setindex_scalar!(A, _v, _i, _j) +@RCI function setindex!(A::AbstractSparseMatrixCSC, _v, _i::Integer) + _checkscalarindex(_i) + _checklinearindexing(A) + checkbounds(A, _i) + col, row = divrem(Int(_i) - 1, size(A, 1)) + return _setindex_scalar!(A, _v, row + 1, col + 1) +end function _setindex_scalar!(A::AbstractSparseMatrixCSC{Tv,Ti}, _v, _i::Integer, _j::Integer) where {Tv,Ti<:Integer} _checkscalarindex(_i); _checkscalarindex(_j) @@ -1124,6 +1134,7 @@ end function setindex!(A::AbstractSparseMatrixCSC, x::AbstractArray, Ix::AbstractVector{<:Integer}) require_one_based_indexing(A, x, Ix) + _checklinearindexing(A) # to_indices does not compare a Bool mask's length with A's Ix isa AbstractVector{Bool} && checkbounds(A, Ix) (I,) = Base.ensure_indexable(to_indices(A, (Ix,))) @@ -1564,6 +1575,7 @@ getindex(A::AbstractSparseMatrixCSC, ::Colon) = A[1:end] function getindex(A::AbstractSparseMatrixCSC{Tv}, I::AbstractUnitRange) where Tv require_one_based_indexing(A, I) + _checklinearindexing(A) checkbounds(A, I) szA = size(A) nA = szA[1]*szA[2] @@ -1606,6 +1618,7 @@ end function getindex(A::AbstractSparseMatrixCSC{Tv,Ti}, I::AbstractVector) where {Tv,Ti} I isa Base.LogicalIndex && return _logical_index(A, I.mask) require_one_based_indexing(A, I) + eltype(I) <: Integer && _checklinearindexing(A) @boundscheck checkbounds(A, I) szA = size(A) nA = szA[1]*szA[2] diff --git a/src/linalg.jl b/src/linalg.jl index efd664e5..4417f927 100644 --- a/src/linalg.jl +++ b/src/linalg.jl @@ -554,46 +554,50 @@ function _dot(x::AbstractSparseVector, A::SparseMatrixCSCOrColumnSubset, y::Abst end ## end of symmetric/Hermitian -\(A::Transpose{<:Complex,<:Hermitian{<:Complex,<:AbstractSparseMatrixCSC}}, B::Vector) = copy(A) \ B +\(A::Transpose{<:Complex,<:Hermitian{<:Complex,<:AbstractSparseMatrixCSC}}, B::VecOrMat) = copy(A) \ B ## diagonal scaling # Scale the stored entries of `A` into `C`, which either is `A` or takes its pattern: # `C[i, j] = op(A[i, j], d[j])` columnwise, `C[i, j] = op(d[i], A[i, j])` rowwise. The # argument order of `op` is kept for non-commutative eltypes. The callers check that -# `length(d)` matches the scaled dimension and that `size(C) == size(A)`. +# `length(d)` matches the scaled dimension and that `size(C) == size(A)`. When a value might +# not convert to `eltype(C)`, the values go to a new vector that `C` takes once all are written. function _scalecols!(C::AbstractSparseMatrixCSC, A::AbstractSparseMatrixCSC, d::AbstractVector, op::F) where {F} - copyinds!(C, A) - Cnzval = getnzval(C) Anzval = getnzval(A) - resize!(Cnzval, length(Anzval)) + direct = _storesafely(eltype(C), promote_op(op, eltype(A), eltype(d))) + Cnzval = direct ? _takepattern!(C, A) : Vector{eltype(C)}(undef, nnz(A)) @inbounds for col in axes(A, 2) dcol = d[col] for p in nzrange(A, col) Cnzval[p] = op(Anzval[p], dcol) end end + direct || _storescaled!(C, A, Cnzval) return C end function _scalerows!(C::AbstractSparseMatrixCSC, A::AbstractSparseMatrixCSC, d::AbstractVector, op::F) where {F} - copyinds!(C, A) - Cnzval = getnzval(C) Anzval = getnzval(A) Arowval = getrowval(A) - resize!(Cnzval, length(Anzval)) + direct = _storesafely(eltype(C), promote_op(op, eltype(d), eltype(A))) + Cnzval = direct ? _takepattern!(C, A) : Vector{eltype(C)}(undef, nnz(A)) @inbounds for col in axes(A, 2), p in nzrange(A, col) Cnzval[p] = op(d[Arowval[p]], Anzval[p]) end + direct || _storescaled!(C, A, Cnzval) return C end function _scalerows!(x::AbstractSparseVector, d::AbstractVector, op::F) where {F} xnzval = nonzeros(x) xnzind = nonzeroinds(x) + direct = _storesafely(eltype(x), promote_op(op, eltype(d), eltype(x))) + vals = direct ? xnzval : similar(xnzval) @inbounds for p in eachindex(xnzind, xnzval) - xnzval[p] = op(d[xnzind[p]], xnzval[p]) + vals[p] = op(d[xnzind[p]], xnzval[p]) end + direct || copyto!(xnzval, vals) return x end @@ -725,7 +729,7 @@ function _sparse_diff1_column!(rowval, nzval, numnz, rowval_a, nzval_a, rng, m) return numnz end -function sparse_diff1(S::AbstractSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} +function sparse_diff1(S::SparseMatrixCSCOrColumnSubset{Tv,Ti}) where {Tv,Ti} m,n = size(S) m > 1 || return SparseMatrixCSC(0, n, fill(one(Ti),n+1), Ti[], Tv[]) colptr = Vector{Ti}(undef, n+1) @@ -735,7 +739,7 @@ function sparse_diff1(S::AbstractSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} numnz = 0 @inbounds colptr[1] = 1 @inbounds for col = 1 : n - numnz = _sparse_diff1_column!(rowval, nzval, numnz, getrowval(S), getnzval(S), nzrange(S, col), m) + numnz = _sparse_diff1_column!(rowval, nzval, numnz, getrowval(S), getnzval(S), getnzrange(S, col), m) colptr[col+1] = numnz+1 end deleteat!(rowval, numnz+1:length(rowval)) @@ -743,7 +747,7 @@ function sparse_diff1(S::AbstractSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} return SparseMatrixCSC(m-1, n, colptr, rowval, nzval) end -function sparse_diff2(a::AbstractSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} +function sparse_diff2(a::SparseMatrixCSCOrColumnSubset{Tv,Ti}) where {Tv,Ti} m,n = size(a) colptr = Vector{Ti}(undef, max(n,1)) numnz = 2 * nnz(a) # upper bound; will shrink later @@ -752,7 +756,6 @@ function sparse_diff2(a::AbstractSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} z = zero(Tv) - colptr_a = getcolptr(a) rowval_a = getrowval(a) nzval_a = getnzval(a) @@ -762,28 +765,21 @@ function sparse_diff2(a::AbstractSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} n == 0 && return SparseMatrixCSC(m, n, colptr, rowval, nzval) - startA = colptr_a[1] - stopA = colptr_a[2] - - rA = startA : stopA - 1 + rA = getnzrange(a, 1) rowvalA = rowval_a[rA] nzvalA = nzval_a[rA] - lA = stopA - startA + lA = length(rA) end @inbounds for col = 1:n-1 - startB, stopB = startA, stopA - startA = colptr_a[col+1] - stopA = colptr_a[col+2] - rowvalB = rowvalA nzvalB = nzvalA lB = lA - rA = startA : stopA - 1 + rA = getnzrange(a, col+1) rowvalA = rowval_a[rA] nzvalA = nzval_a[rA] - lA = stopA - startA + lA = length(rA) ptrB = 1 ptrA = 1 @@ -834,10 +830,15 @@ function sparse_diff2(a::AbstractSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} return SparseMatrixCSC(m, n-1, colptr, rowval, nzval) end -function diff(a::AbstractSparseMatrixCSC; dims::Integer) +function diff(a::SparseMatrixCSCOrSubArray; dims::Integer) 1 <= dims <= 2 || throw(ArgumentError("dimension $dims out of range (1:2)")) + a = _compressed(a) return dims == 1 ? sparse_diff1(a) : sparse_diff2(a) end +function diff(a::AdjOrTrans{<:Any,<:SparseMatrixCSCOrSubArray}; dims::Integer) + 1 <= dims <= 2 || throw(ArgumentError("dimension $dims out of range (1:2)")) + return copy(LinearAlgebra.wrapperop(a)(diff(parent(a); dims = 3 - dims))) +end function _sparse_diff(x, dims) dims == 1 || throw(ArgumentError("dimension $dims out of range (1:1)")) @@ -985,18 +986,28 @@ estimator of Higham and Tisseur, from a factorization of `A` by `factorize` and forming the inverse, so the result can fall below the exact condition number. `p = 2`, the default, throws an `ArgumentError`, so `cond(A)` does too: use `cond(Array(A))` for a matrix small enough to store dense. + +A singular matrix gives `Inf` when its factorization meets an exactly zero pivot. Rounding +can hide that zero, and the result is then finite but very large. The sparse LU pivots in +another order than the dense one, so `cond(A, p)` can be finite where `cond(Array(A), p)` +is `Inf`. """ function cond(A::AbstractSparseMatrixCSC, p::Real=2) + # the type of dense `cond`, whichever precision the factorization of `A` works in + T = float(real(eltype(A))) # as for a dense matrix; the estimator of the inverse's norm needs at least one column - (p == 1 || p == Inf) && checksquare(A) == 0 && return zero(real(float(eltype(A)))) - if p == 1 - normAinv = opnormestinv(A) - normA = opnorm(A, 1) - return normA * normAinv - elseif p == Inf - normAinv = opnormestinv(copy(A')) - normA = opnorm(A, Inf) - return normA * normAinv + (p == 1 || p == Inf) && checksquare(A) == 0 && return zero(T) + if p == 1 || p == Inf + B = p == 1 ? A : copy(A') + # a singular matrix has an infinite condition number, as for a dense matrix + F = try + factorize(B) + catch e + e isa SingularException || rethrow() + return convert(T, Inf) + end + _issingularfactor(F) && return convert(T, Inf) + return convert(T, opnorm(A, p) * _opnormestinv(B, F, min(2, size(B, 1)))) elseif p == 2 throw(ArgumentError("only 1- and Inf-norm condition numbers are implemented for sparse matrices, for 2-norm try cond(Array(A), 2) instead")) else @@ -1004,11 +1015,22 @@ function cond(A::AbstractSparseMatrixCSC, p::Real=2) end end -function opnormestinv(A::AbstractSparseMatrixCSC{T}, t::Integer = min(2,maximum(size(A)))) where T +# `factorize` throws for a singular LU or Hermitian factorization, but leaves a triangular +# or diagonal one to the solve +_issingularfactor(F) = false +_issingularfactor(D::Diagonal) = any(iszero, D.diag) +_issingularfactor(L::LowerTriangular{<:Any,<:AbstractSparseMatrixCSC}) = !iszero(_trisingular(false, parent(L))) +_issingularfactor(U::UpperTriangular{<:Any,<:AbstractSparseMatrixCSC}) = !iszero(_trisingular(true, parent(U))) + +function opnormestinv(A::AbstractSparseMatrixCSC, t::Integer = min(2,maximum(size(A)))) + checksquare(A) + return _opnormestinv(A, factorize(A), t) +end + +# the estimate for the factorization `F` of `A` +function _opnormestinv(A::AbstractSparseMatrixCSC{T}, F, t::Integer) where T maxiter = 5 - # Check the input - n = checksquare(A) - F = factorize(A) + n = size(A, 1) if t <= 0 throw(ArgumentError("number of blocks must be a positive integer")) end @@ -1174,13 +1196,13 @@ end # Hermitian or triangular wrapper of a sparse vector or matrix or its adjoint or transpose. # Views stay out of the wrappers: each such wrapped view in these signatures makes loading # the package much slower, as Julia compares it with LinearAlgebra's triangular methods. -const _SparseKronGroup = Union{SparseVectorOrView, SparseMatrixCSCSubArray, - AdjOrTrans{<:Any,<:Union{SparseVectorOrView, SparseMatrixCSCSubArray}}, +const _SparseKronGroup = Union{SparseVectorOrView, SparseVectorPartialView, SparseMatrixCSCSubArray, + AdjOrTrans{<:Any,<:Union{SparseVectorOrView, SparseVectorPartialView, SparseMatrixCSCSubArray}}, SparseVecOrMatMaybeAdjOrTrans, HermOrSym{<:Any,<:SparseVecOrMatMaybeAdjOrTrans}, UpperOrLowerTriangular{<:Any,<:SparseVecOrMatMaybeAdjOrTrans}} _kronmatrix(A) = convert(SparseMatrixCSC, A) -_kronmatrix(x::Union{SparseColumnView,SparseVectorView}) = convert(SparseMatrixCSC, SparseVector(x)) +_kronmatrix(x::Union{SparseColumnView,SparseVectorView,SparseVectorPartialView}) = convert(SparseMatrixCSC, SparseVector(x)) # A number scales a view of a sparse matrix into a dense matrix unless the view selects # whole columns. Scaling the sparse matrix it converts to gives what the number gives with # a sparse matrix that is not a view. @@ -1318,8 +1340,10 @@ kron(A::_SparseKronGroup, B::_SparseKronGroup) = kron(_kronmatrix(A), _kronmatrix(B)) kron(A::_SparseKronGroup, B::_DenseKronGroup) = kron(A, sparse(B)) kron(A::_DenseKronGroup, B::_SparseKronGroup) = kron(sparse(A), B) -kron(x::SparseVectorOrView, y::SparseVectorOrView) = kron(SparseVector(x), SparseVector(y)) -kron(A::SparseVectorOrView, B::AdjOrTrans{<:Any,<:SparseVectorOrView}) = A .* B +kron(x::Union{SparseVectorOrView,SparseVectorPartialView}, y::Union{SparseVectorOrView,SparseVectorPartialView}) = + kron(SparseVector(x), SparseVector(y)) +kron(A::Union{SparseVectorOrView,SparseVectorPartialView}, + B::AdjOrTrans{<:Any,<:Union{SparseVectorOrView,SparseVectorPartialView}}) = A .* B # disambiguation kron(A::AbstractCompressedVector, B::AdjOrTrans{<:Any,<:AbstractCompressedVector}) = A .* B kron(a::Number, b::_SparseKronGroup) = a * _kronscaled(b) @@ -1353,6 +1377,19 @@ function copyinds!(C::AbstractSparseMatrixCSC, A::AbstractSparseMatrixCSC; copy_ end end +# Give `C` the pattern of `A`, and return the values of `C`, of the length of those of `A`. +function _takepattern!(C::AbstractSparseMatrixCSC, A::AbstractSparseMatrixCSC) + copyinds!(C, A) + return resize!(getnzval(C), length(getnzval(A))) +end +# Store `nzval` in `C` with the pattern of `A`, converting it first so that a value that does +# not convert to `eltype(C)` throws with `C` unchanged. +function _storescaled!(C::AbstractSparseMatrixCSC, A::AbstractSparseMatrixCSC, nzval::AbstractVector) + vals = convert(Vector{eltype(C)}, nzval) + copyto!(_takepattern!(C, A), vals) + return C +end + """ rowcheck_index(A::AbstractSparseMatrixCSC, row::Integer, col::Integer) @@ -1439,10 +1476,11 @@ end _hermitian_solve(A::AbstractSparseMatrixCSC) = !(eltype(A) <: Union{Integer, Complex{<:Integer}}) && ishermitian(A) -# `\` and `factorize` choose the same method from the structure of `A`; `f` (or `flu`, for -# the LU factorization) is applied in each branch so that the result stays inferrable. A -# Hermitian matrix is only wrapped, because `\` and `factorize` finish it differently. -function _sparse_factorize(f, A::AbstractSparseMatrixCSC, flu = f) +# `\` and `factorize` choose the same method from the structure of `A`; `f` (or `fsolver`, +# for the LU, QR and LQ factorizations) is applied in each branch so that the result stays +# inferrable, and the QR and LQ factorizations are of `fqr(A)`. A Hermitian matrix is only +# wrapped, because `\` and `factorize` finish it differently. +function _sparse_factorize(f, A::AbstractSparseMatrixCSC, fsolver = f, fqr = identity) m, n = size(A) if m == n if istril(A) @@ -1452,12 +1490,12 @@ function _sparse_factorize(f, A::AbstractSparseMatrixCSC, flu = f) elseif _hermitian_solve(A) return f(Hermitian(A)) end - return flu(lu(A)) + return fsolver(lu(A)) elseif m > n - return f(qr(A)) + return fsolver(qr(fqr(A))) else # A is wide, so the LQ factorization gives the minimum-norm solution - return f(lq(A)) + return fsolver(lq(fqr(A))) end end @@ -1465,15 +1503,24 @@ end _solve_factor(F) = F _solve_factor(D::Diagonal) = Diagonal(Vector(diag(D))) _sparse_solve(op, F, B) = \(op(_solve_factor(F)), B) -# the sparse LU works in double precision, so its solution is converted to the eltype that -# dense `\` gives -_sparse_lusolve(op, F, A, B) = - convert(AbstractArray{typeof(one(eltype(A)) \ one(eltype(B)))}, _sparse_solve(op, F, B)) +# The eltype that dense `\` gives. It is `nothing` for an abstract eltype, which has no `one` +# and whose solution is returned as the solver gives it. +_ldiv_eltype(A, B) = isconcretetype(eltype(A)) && isconcretetype(eltype(B)) ? + typeof(one(eltype(A)) \ one(eltype(B))) : nothing +# The sparse LU works in double precision and the QR and LQ in the precision of the matrix, +# so their solution is converted to the eltype that dense `\` gives, and for the QR and LQ a +# matrix of a narrower eltype than that is converted before it is factorized. +_sparse_solversolve(op, F, A, B) = _convert_solution(_ldiv_eltype(A, B), _sparse_solve(op, F, B)) +_convert_solution(T, X) = convert(AbstractArray{T}, X) +_convert_solution(::Nothing, X) = X +_qr_matrix(A, B) = _qr_matrix(A, _ldiv_eltype(A, B)) +_qr_matrix(A, T::Type) = convert(AbstractMatrix{promote_type(float(eltype(A)), real(T))}, A) +_qr_matrix(A, ::Nothing) = A function \(A::AbstractSparseMatrixCSC, B::AbstractVecOrMat) require_one_based_indexing(A, B) return _sparse_factorize(F -> _sparse_solve(identity, F, B), A, - F -> _sparse_lusolve(identity, F, A, B)) + F -> _sparse_solversolve(identity, F, A, B), A -> _qr_matrix(A, B)) end for (xformtype, xformop) in ((:Adjoint, :adjoint), (:Transpose, :transpose)) @eval function \(xformA::($xformtype){<:Any,<:AbstractSparseMatrixCSC}, B::AbstractVecOrMat) @@ -1482,12 +1529,12 @@ for (xformtype, xformop) in ((:Adjoint, :adjoint), (:Transpose, :transpose)) m, n = size(A) if m < n # A' is tall, so the least squares solve needs a factorization of A' itself - return _sparse_solve(identity, qr($xformop(A)), B) + return _sparse_solversolve(identity, qr($xformop(_qr_matrix(A, B))), A, B) end # Otherwise the transformed choice for A serves. For a wide A' that is the # factorization of A, which gives the minimum-norm solution. return _sparse_factorize(F -> _sparse_solve($xformop, F, B), A, - F -> _sparse_lusolve($xformop, F, A, B)) + F -> _sparse_solversolve($xformop, F, A, B), A -> _qr_matrix(A, B)) end end @@ -1497,12 +1544,11 @@ factorize(A::AbstractSparseMatrixCSC) = _sparse_factorize(_factorize_choice, A) function factorize(A::RealHermSymComplexHerm{<:Union{Float32,Float64},<:AbstractSparseMatrixCSC}) F = cholesky(A; check = false) - if LinearAlgebra.issuccess(F) - return F - else - ldlt!(F, A) - return F - end + LinearAlgebra.issuccess(F) && return F + ldlt!(F, A; check = false) + LinearAlgebra.issuccess(F) && return F + # an indefinite matrix may need pivoting, which only the LU does, as in `\` + return lu(A) end eigen(A::AbstractSparseMatrixCSC) = diff --git a/src/matmul.jl b/src/matmul.jl index 50de6da2..8c8ba86c 100644 --- a/src/matmul.jl +++ b/src/matmul.jl @@ -362,12 +362,15 @@ end # Sparse matrix multiplication as described in [Gustavson, 1978]: # http://dl.acm.org/citation.cfm?id=355796 -# spmatmul handles compressed vectors and whole-column/whole-vector views; other -# AbstractSparseVectors take the generic product. Plain CSC times a compressed vector is -# defined with the sparse vector products below. -*(A::SparseTriangular, B::SparseVectorOrView) = spmatmulv(A, B) -*(A::SparseMatrixCSCView, B::SparseVectorOrView) = spmatmulv(A, B) -*(A::AbstractSparseMatrixCSC, B::Union{SparseColumnView,SparseVectorView}) = spmatmulv(A, B) +# spmatmul handles compressed vectors and whole-column/whole-vector views, and a range view +# through the sparse vector it reads as; other AbstractSparseVectors take the generic +# product. Plain CSC times a compressed vector is defined with the sparse vector products +# below. +_spmatmulv_operand(x::Union{AbstractCompressedVector,SparseColumnView,SparseVectorView}) = x +_spmatmulv_operand(x::SparseVectorPartialView) = _sparsevector(x) +*(A::SparseTriangular, B::Union{SparseVectorOrView,SparseVectorPartialView}) = spmatmulv(A, _spmatmulv_operand(B)) +*(A::SparseMatrixCSCView, B::Union{SparseVectorOrView,SparseVectorPartialView}) = spmatmulv(A, _spmatmulv_operand(B)) +*(A::AbstractSparseMatrixCSC, B::Union{SparseColumnView,SparseVectorPartialView}) = spmatmulv(A, _spmatmulv_operand(B)) """ *(A::AbstractSparseMatrixCSC, B::AbstractSparseMatrixCSC) @@ -405,7 +408,7 @@ julia> A * B *(A::SparseMatrixCSCSymmHerm, B::Union{SparseOrTri,AdjOrTrans{<:Any,<:AbstractSparseMatrixCSC}}) = sparse(A) * B *(A::Union{SparseOrTri,AdjOrTrans{<:Any,<:AbstractSparseMatrixCSC}}, B::SparseMatrixCSCSymmHerm) = A * sparse(B) *(A::SparseMatrixCSCSymmHerm, B::SparseMatrixCSCSymmHerm) = sparse(A) * sparse(B) -*(A::SparseMatrixCSCSymmHerm, x::SparseVectorOrView) = sparse(A) * x +*(A::SparseMatrixCSCSymmHerm, x::Union{SparseVectorOrView,SparseVectorPartialView}) = sparse(A) * x # so is the eagerly made adjoint/transpose of a sparse triangular matrix for W in (:UpperTriangular, :LowerTriangular, :UnitUpperTriangular, :UnitLowerTriangular) @eval _sptriangular(A::$W) = $W(copy(parent(A))) @@ -415,7 +418,7 @@ end *(A::Union{SparseOrTri,AdjOrTrans{<:Any,<:AbstractSparseMatrixCSC},SparseMatrixCSCSymmHerm}, B::SparseAdjOrTransTriangular) = A * _sptriangular(B) *(A::SparseAdjOrTransTriangular, B::SparseAdjOrTransTriangular) = _sptriangular(A) * _sptriangular(B) -*(A::SparseAdjOrTransTriangular, x::SparseVectorOrView) = _sptriangular(A) * x +*(A::SparseAdjOrTransTriangular, x::Union{SparseVectorOrView,SparseVectorPartialView}) = _sptriangular(A) * x (*)(Da::Diagonal, A::Union{SparseMatrixCSCOrView, AdjOrTrans{<:Any,<:AbstractSparseMatrixCSC}}, Db::Diagonal) = Da * (A * Db) function (*)(Da::Diagonal, A::SparseMatrixCSC, Db::Diagonal) @@ -1022,18 +1025,18 @@ _lmulkeepszeros(b::Number, A) = (z = _implicit_zero(A); z isa UnknownZero || _is function mul!(C::AbstractSparseMatrixCSC, A::AbstractSparseMatrixCSC, b::Number) size(A)==size(C) || throw(DimensionMismatch("A has size $(size(A)) but C has size $(size(C))")) (_is_fixed(C) || !_rmulkeepszeros(A, b)) && return broadcast!(*, C, A, b) - copyinds!(C, A) - resize!(getnzval(C), length(getnzval(A))) - mul!(getnzval(C), getnzval(A), b) + _storesafely(eltype(C), promote_op(*, eltype(A), typeof(b))) || + return _storescaled!(C, A, view(getnzval(A), 1:nnz(A)) * b) + mul!(_takepattern!(C, A), getnzval(A), b) C end function mul!(C::AbstractSparseMatrixCSC, b::Number, A::AbstractSparseMatrixCSC) size(A)==size(C) || throw(DimensionMismatch("A has size $(size(A)) but C has size $(size(C))")) (_is_fixed(C) || !_lmulkeepszeros(b, A)) && return broadcast!(*, C, b, A) - copyinds!(C, A) - resize!(getnzval(C), length(getnzval(A))) - mul!(getnzval(C), b, getnzval(A)) + _storesafely(eltype(C), promote_op(*, typeof(b), eltype(A))) || + return _storescaled!(C, A, b * view(getnzval(A), 1:nnz(A))) + mul!(_takepattern!(C, A), b, getnzval(A)) C end diff --git a/src/reductions.jl b/src/reductions.jl index 1db9702c..8f0d86a6 100644 --- a/src/reductions.jl +++ b/src/reductions.jl @@ -95,13 +95,13 @@ function _mapreduce_ordered(f::F, op::G, A::SparseMatrixCSCOrColumnSubset{T}) wh v, dropout = _foldstored(f, op, z, nzvalview(A)) dropout && return v v = Base._InitialValue() - unstored = Int64(0) # spans columns, so it can exceed typemax(Int32) + unstored = widen(0) # spans columns, so it can exceed typemax(Int) @inbounds for col in axes(A, 2) prev = 0 for k in getnzrange(A, col) row = Int(rowval[k]) v = rf(v, _afterunstored(op, z, idem, unstored + (row - prev - 1), f(nzval[k]))) - unstored, prev = Int64(0), row + unstored, prev = widen(0), row end unstored += m - prev end @@ -187,14 +187,30 @@ end _mapreducezeros(f::F, op::Union{typeof(Base.add_sum),typeof(+)}, ::Type{T}, nzeros::Integer, v0) where {F,T} = nzeros == 0 ? Base.reduce_first(op, v0) : op(_addzeros(_unstored(op, f, T, v0), nzeros), v0) _mapreducezeros(f::F, op::Union{typeof(Base.mul_prod),typeof(*)},::Type{T}, nzeros::Integer, v0) where {F,T} = - nzeros == 0 ? Base.reduce_first(op, v0) : op(_unstored(op, f, T, v0)^nzeros, v0) + nzeros == 0 ? Base.reduce_first(op, v0) : op(_powzeros(_unstored(op, f, T, v0), nzeros), v0) +# `z` to the power of `nzeros`. An eltype may define `^` for an `Int` power only, so a count +# from `widelength` is split into `Int` powers, as `_addzeros` splits it into factors. +_powzeros(z, nzeros) = z^nzeros +function _powzeros(z, nzeros::Int128) + nzeros <= typemax(Int) && return z^Int(nzeros) + q, r = divrem(nzeros, typemax(Int)) + return (z^typemax(Int))^Int(q) * z^Int(r) +end _unstored(op, f, ::Type{T}, v0) where T = _widen(Base.reduce_first(op, f(zero(T))), v0) _widen(z, v0) = _widen(z, typeof(v0)) _widen(z, ::Type{S}) where S = convert(promote_type(typeof(z), S), z) # `nzeros` copies of `z` added up, in the type `+` keeps for them: a small integer wraps -# rather than widening to the count's `Int`, as when dense input is summed with `+` +# rather than widening to the count's `Int`, as when dense input is summed with `+`. A count +# from `widelength` is an `Int128`, which would widen `Complex{Int}` and the like, so it is +# split into `Int` factors. _addzeros(z, nzeros) = z * nzeros +function _addzeros(z, nzeros::Int128) + nzeros <= typemax(Int) && return z * Int(nzeros) + q, r = divrem(nzeros - 1, typemax(Int)) + return z * Int(q) * typemax(Int) + z * Int(r + 1) +end _addzeros(z::Base.BitInteger, nzeros) = z * (nzeros % typeof(z)) +_addzeros(z::Base.BitInteger, nzeros::Int128) = z * (nzeros % typeof(z)) _mapreducezeros(f::F, op::Union{typeof(min),typeof(max)}, ::Type{T}, nzeros::Integer, v0) where {F,T} = nzeros == 0 ? v0 : op(v0, f(zero(T))) _mapreducezeros(f::Base.ExtremaMap, op::typeof(Base._extrema_rf), ::Type{T}, nzeros::Integer, v0) where {T} = @@ -220,7 +236,7 @@ function Base._mapreduce(f::F, op::Union{typeof(Base.mul_prod),typeof(*)}, ::Bas elseif !_commutes(f, op, T) _mapreduce_ordered(f, op, A) else - v = Base.reduce_first(op, f(zero(T)))^nzeros + v = _powzeros(Base.reduce_first(op, f(zero(T))), nzeros) (nnzA == 0 || (_iszero(v) && Base.promote_op(f, T) <: Integer)) && return v r = v*Base._mapreduce(f, op, nzvalview(A)) # A zero ends the product unless a stored `Inf` or `NaN` makes it `NaN`. The product @@ -266,7 +282,7 @@ end # A reduction along a dimension, an empty array and a shape mismatch are left to Base's method. for T in (:SparseVectorOrView, :SparseMatrixCSCOrColumnSubset) @eval function Base.mapreduce(f::F, op::G, A::$T, B::$T; dims=:, init=Base._InitialValue(), kw...) where {F,G} - (dims === (:) && isempty(kw) && size(A) == size(B) && !isempty(A)) || + (dims === (:) && isempty(kw) && size(A) == size(B) && !iszero(widelength(A))) || return reduce(op, map(f, A, B); dims, init, kw...) return _commutes(f, op, eltype(A), eltype(B)) ? _mapreduce_unordered(f, op, init, A, B) : _mapreduce_ordered(f, op, init, A, B) @@ -280,7 +296,7 @@ function _mapreduce_ordered(f::F, op::G, init, A::AbstractArray{Ta}, B::Abstract Ai, Av, Bi, Bv = getrowval(A), getnzval(A), getrowval(B), getnzval(B) m = size(A, 1) v = init - unstored = Int64(0) # positions that neither array stores, since the last one folded + unstored = widen(0) # positions that neither array stores, since the last one folded @inbounds for col in axes(A, 2) ra, rb = getnzrange(A, col), getnzrange(B, col) ia, ib = first(ra), first(rb) @@ -290,18 +306,18 @@ function _mapreduce_ordered(f::F, op::G, init, A::AbstractArray{Ta}, B::Abstract row = Int(min(Ai[ia], Bi[ib])) x, ia, ib = _merge_step(f, Ai, Av, ia, Bi, Bv, ib) v = rf(_foldunstored(f, rf, v, Ta, Tb, unstored + (row - prev - 1)), x) - unstored, prev = Int64(0), row + unstored, prev = widen(0), row end while ia <= ea row = Int(Ai[ia]) v = rf(_foldunstored(f, rf, v, Ta, Tb, unstored + (row - prev - 1)), f(Av[ia], zero(Tb))) - unstored, prev = Int64(0), row + unstored, prev = widen(0), row ia += 1 end while ib <= eb row = Int(Bi[ib]) v = rf(_foldunstored(f, rf, v, Ta, Tb, unstored + (row - prev - 1)), f(zero(Ta), Bv[ib])) - unstored, prev = Int64(0), row + unstored, prev = widen(0), row ib += 1 end unstored += m - prev @@ -365,7 +381,7 @@ function _mapreduce_unordered(f::F, op::G, init, A::AbstractArray{Ta}, B::Abstra ib == eb && (v = rf(v, f(zero(Ta), Bv[ib]))) end w isa Base._InitialValue || (v = v isa Base._InitialValue ? w : op(v, w)) - nzeros = prod(Int64.(size(A))) - stored + nzeros = prod(widen, size(A)) - stored if v isa Base._InitialValue # nothing stored and no `init`: seed with one unstored position v = rf(v, f(zero(Ta), zero(Tb))) nzeros -= 1 @@ -627,7 +643,7 @@ end # The kernels above fold each slice's stored entries first and its unstored ones last. That # matches element order only when `op` commutes or every entry is stored; otherwise the # kernels below fold the unstored entries where they lie. -_inorder(f, op, A::AbstractArray{T}) where {T} = _commutes(f, op, T) || nnz(A) == length(A) +_inorder(f, op, A::AbstractArray{T}) where {T} = _commutes(f, op, T) || nnz(A) == widelength(A) function _mapreducerows_ordered!(f::F, op::G, R::AbstractArray, A::SparseMatrixCSCOrColumnSubset{T}) where {F,G,T} require_one_based_indexing(A, R) rowval, nzval = getrowval(A), getnzval(A) diff --git a/src/solvers/cholmod.jl b/src/solvers/cholmod.jl index e0d03046..faae83ab 100644 --- a/src/solvers/cholmod.jl +++ b/src/solvers/cholmod.jl @@ -1166,8 +1166,19 @@ function Sparse{Tv, Ti}(A::SparseMatrixCSC{<:Any}, stype::Integer) where {Tv<:VT check_sparse(o) return o end + +# CHOLMOD takes Int32 or Int64 indices; other index types are promoted with Int +function cholmod_itype(A, ::Type{Ti}) where {Ti} + Ti <: ITypes && return Ti + T = promote_type(Ti, Int) + T <: ITypes || throw(ArgumentError(string("matrix type ", typeof(A), " not supported: ", + "CHOLMOD needs an index type of ", join(Base.uniontypes(ITypes), " or "), ". ", + "Try converting its index type to Int first."))) + return T +end + Sparse{Tv}(A::SparseMatrixCSC{<:Any, Ti}, stype::Integer) where {Tv<:VTypes, Ti} = - Sparse{Tv, Ti <: ITypes ? Ti : promote_type(Ti, Int)}(A, stype) + Sparse{Tv, cholmod_itype(A, Ti)}(A, stype) # handle promotion function Sparse(A::SparseMatrixCSC{Tv}, stype::Integer) where {Tv} T = promote_type(Tv, Float64) @@ -1191,7 +1202,7 @@ function Sparse{Tv, Ti}(A::SparseMatrixCSC) where {Tv<:VTypes, Ti<:ITypes} o end Sparse{Tv}(A::SparseMatrixCSC{<:Any, Ti}) where {Tv<:VTypes, Ti} = - Sparse{Tv, Ti <: ITypes ? Ti : promote_type(Ti, Int)}(A) + Sparse{Tv, cholmod_itype(A, Ti)}(A) function Sparse(A::SparseMatrixCSC{Tv}) where {Tv} T = promote_type(Tv, Float64) return Sparse{T}(A) @@ -1206,19 +1217,19 @@ Sparse{Tv, Ti}(A::Hermitian{<:Any, <:SparseMatrixCSC}) where {Tv<:VTypes, Ti<:IT Sparse{Tv, Ti}(A.data, A.uplo == 'L' ? -1 : 1) Sparse(A::Symmetric{Tv, SparseMatrixCSC{Tv,Ti}}) where {Tv<:Real, Ti} = - Sparse{promote_type(Tv, Float64), Ti <: ITypes ? Ti : promote_type(Ti, Int)}( + Sparse{promote_type(Tv, Float64), cholmod_itype(A, Ti)}( A.data, A.uplo == 'L' ? -1 : 1 ) Sparse(A::Symmetric{Tv, SparseMatrixCSC{Tv,Ti}}) where {Tv<:Union{Float16, Float32}, Ti} = - Sparse{Float32, Ti <: ITypes ? Ti : promote_type(Ti, Int)}(A.data, A.uplo == 'L' ? -1 : 1) + Sparse{Float32, cholmod_itype(A, Ti)}(A.data, A.uplo == 'L' ? -1 : 1) Sparse(A::Hermitian{Tv, SparseMatrixCSC{Tv,Ti}}) where {Tv, Ti} = - Sparse{promote_type(Tv, Float64), Ti <: ITypes ? Ti : promote_type(Ti, Int)}( + Sparse{promote_type(Tv, Float64), cholmod_itype(A, Ti)}( A.data, A.uplo == 'L' ? -1 : 1 ) Sparse(A::Hermitian{Tv, SparseMatrixCSC{Tv,Ti}}) where {Tv<:Union{Float16, Float32, ComplexF32, ComplexF16}, Ti} = - Sparse{promote_type(Float32, Tv), Ti <: ITypes ? Ti : promote_type(Ti, Int)}( + Sparse{promote_type(Float32, Tv), cholmod_itype(A, Ti)}( A.data, A.uplo == 'L' ? -1 : 1 ) @@ -1333,6 +1344,10 @@ function SparseVector{Tv, Ti}(A::Sparse{Tv, Ti}) where {Tv, Ti<:ITypes} throw(ArgumentError("matrix has stype != 0. Convert to matrix " * "with stype == 0 before converting to SparseVector")) end + if s.ncol != 1 + throw(DimensionMismatch("cannot convert a CHOLMOD.Sparse with $(Int(s.ncol)) columns " * + "to SparseVector: it must have exactly one column")) + end args = _extract_args(A, Tv) s.sorted == 0 && _sort_buffers!(args...); _trim_nz_builder!(args...) @@ -1384,11 +1399,7 @@ function Hermitian{Tv,SparseMatrixCSC{Tv, Ti}}(A::Sparse{Tv, Ti}) where {Tv<:VTy end convert(T::Type{Hermitian{Tv,SparseMatrixCSC{Tv,Ti}}}, A::Sparse{Tv, Ti}) where {Tv<:VTypes, Ti<:ITypes} = T(A) -function sparsevec(A::Sparse{Tv, Ti}) where {Tv, Ti} - s = unsafe_load(pointer(A)) - @assert s.stype == 0 - return SparseVector{Tv, Ti}(A) -end +sparsevec(A::Sparse{Tv, Ti}) where {Tv, Ti} = SparseVector{Tv, Ti}(A) function sparse(A::Sparse{Tv, Ti}) where {Tv<:VRealTypes, Ti} # Notice! Cannot be type stable because of stype s = unsafe_load(pointer(A)) @@ -1660,6 +1671,11 @@ function check_hermitian_stype(A::Sparse) throw(ArgumentError("sparse matrix is not symmetric/Hermitian")) return nothing end +function check_refactor_size(F::Factor, A::Sparse) + size(A) == size(F) || throw(DimensionMismatch( + "matrix has size $(size(A)), but the factorization has size $(size(F))")) + return nothing +end ## Compute that symbolic factorization only function symbolic(A::Sparse{<:VTypes, Ti}; @@ -1699,6 +1715,7 @@ function cholesky!(F::Factor{Tv}, A::Sparse{Tv}; shift::Real=0.0, check::Bool = true) where Tv check_hermitian_stype(A) @lock F.lock begin + check_refactor_size(F, A) @cholmod_param final_ll = true begin factorize_p!(A, shift, F) end @@ -1738,13 +1755,13 @@ true Input matrices not of those element types will be converted to these types as appropriate. """ -cholesky!(F::Factor, A::Union{SparseMatrixCSC{T, Ti}, - SparseMatrixCSC{Complex{T}, Ti}, - Symmetric{T,<:SparseMatrixCSC{T, Ti}}, - Hermitian{Complex{T},<:SparseMatrixCSC{Complex{T}, Ti}}, - Hermitian{T,<:SparseMatrixCSC{T, Ti}}}; - shift = 0.0, check::Bool = true) where {T<:Real, Ti} = - cholesky!(F, Sparse{eltype(F), Ti}(A); shift = shift, check = check) +cholesky!(F::Factor{Tv, Ti}, A::Union{SparseMatrixCSC{T}, + SparseMatrixCSC{Complex{T}}, + Symmetric{T,<:SparseMatrixCSC{T}}, + Hermitian{Complex{T},<:SparseMatrixCSC{Complex{T}}}, + Hermitian{T,<:SparseMatrixCSC{T}}}; + shift = 0.0, check::Bool = true) where {Tv, Ti, T<:Real} = + cholesky!(F, Sparse{Tv, Ti}(A); shift = shift, check = check) function cholesky(A::Sparse; shift::Real=0.0, check::Bool = true, perm::Union{Nothing,AbstractVector{<:Integer}}=nothing) @@ -1890,6 +1907,7 @@ function ldlt!(F::Factor{Tv}, A::Sparse{Tv}; shift::Real=0.0, check::Bool = true) where Tv check_hermitian_stype(A) @lock F.lock begin + check_refactor_size(F, A) change_factor!(F, false, false, true, false) factorize_p!(A, shift, F) check && (issuccess(F) || throw(factorization_exception(F))) @@ -1928,13 +1946,13 @@ true Input matrices not of those element types will be converted to these types as appropriate. """ -ldlt!(F::Factor, A::Union{SparseMatrixCSC{T, Ti}, - SparseMatrixCSC{Complex{T}, Ti}, - Symmetric{T, <:SparseMatrixCSC{T, Ti}}, - Hermitian{Complex{T}, <:SparseMatrixCSC{Complex{T}, Ti}}, - Hermitian{T, <:SparseMatrixCSC{T, Ti}}}; - shift = 0.0, check::Bool = true) where {T<:Real, Ti} = - ldlt!(F, Sparse{eltype(F), Ti}(A), shift = shift, check = check) +ldlt!(F::Factor{Tv, Ti}, A::Union{SparseMatrixCSC{T}, + SparseMatrixCSC{Complex{T}}, + Symmetric{T, <:SparseMatrixCSC{T}}, + Hermitian{Complex{T}, <:SparseMatrixCSC{Complex{T}}}, + Hermitian{T, <:SparseMatrixCSC{T}}}; + shift = 0.0, check::Bool = true) where {Tv, Ti, T<:Real} = + ldlt!(F, Sparse{Tv, Ti}(A), shift = shift, check = check) function ldlt(A::Sparse; shift::Real=0.0, check::Bool = true, perm::Union{Nothing,AbstractVector{<:Integer}}=nothing) diff --git a/src/solvers/spqr.jl b/src/solvers/spqr.jl index cbfd69ef..6c565685 100644 --- a/src/solvers/spqr.jl +++ b/src/solvers/spqr.jl @@ -654,6 +654,15 @@ LinearAlgebra.rank(F::AdjointQRSparse) = rank(parent(F)) _rhs_eltype(::Type{T}, ::Type{<:Complex}) where {T<:LinearAlgebra.BlasReal} = Complex{T} _rhs_eltype(::Type{T}, ::Type) where {T} = T _rhs_array(F, B::AbstractVecOrMat) = convert(Array{_rhs_eltype(eltype(F), eltype(B))}, B) +# Rather than round the right-hand side to the precision of the factorization, a solve whose +# solution dense `\` gives a wider eltype converts the factorization to that precision, as +# dense `\` does. A real factorization stays real for a complex right-hand side. +_solve_eltype(F, B) = isconcretetype(eltype(B)) ? + promote_type(eltype(F), real(typeof(zero(eltype(B)) / oneunit(eltype(F))))) : eltype(F) +_promoted_factor(F::QRSparse, B) = _promoted_factor(F, _solve_eltype(F, B)) +_promoted_factor(F::QRSparse{T}, ::Type{T}) where {T} = F +_promoted_factor(F::QRSparse, ::Type{T}) where {T<:LinearAlgebra.BlasFloat} = QRSparse{T}(F) +_promoted_factor(F::QRSparse, ::Type) = F ## Two helper methods _ret_size(F::Union{QRSparse,AdjointQRSparse}, b::AbstractVector) = (size(F, 2),) @@ -733,7 +742,7 @@ julia> qr(A)\\fill(1.0, 4) 0.0 ``` """ -(\)(F::QRSparse, B::AbstractVecOrMat) = F\_rhs_array(F, B) +(\)(F::QRSparse, B::AbstractVecOrMat) = (G = _promoted_factor(F, B); G\_rhs_array(G, B)) function LinearAlgebra.ldiv!(X::StridedVecOrMat{T}, F::QRSparse{T}, B::StridedVecOrMat{T}; workspace::Union{Nothing,SpqrWS{T}} = nothing) where {T} @@ -842,7 +851,8 @@ julia> A'x 0.0 ``` """ -(\)(Fadj::AdjointQRSparse, B::AbstractVecOrMat) = Fadj\_rhs_array(Fadj, B) +(\)(Fadj::AdjointQRSparse, B::AbstractVecOrMat) = + (G = adjoint(_promoted_factor(parent(Fadj), B)); G\_rhs_array(G, B)) function LinearAlgebra.ldiv!(X::StridedVecOrMat{T}, Fadj::AdjointQRSparse{T}, B::StridedVecOrMat{T}; workspace::Union{Nothing,SpqrWS{T}} = nothing) where {T} diff --git a/src/solvers/umfpack.jl b/src/solvers/umfpack.jl index 68ffb597..ed9fce7f 100644 --- a/src/solvers/umfpack.jl +++ b/src/solvers/umfpack.jl @@ -13,7 +13,7 @@ import LinearAlgebra: Factorization, AdjointFactorization, TransposeFactorizatio checksquare, det, logabsdet, lu, lu!, ldiv! using SparseArrays -using SparseArrays: getcolptr, getrowval, getnzval, AbstractSparseMatrixCSC +using SparseArrays: getcolptr, getrowval, getnzval, AbstractSparseMatrixCSC, _checkconvertible import SparseArrays: nnz import Serialization: AbstractSerializer, deserialize, serialize @@ -531,6 +531,7 @@ function lu!(F::UmfpackLU{Tv, Ti}, S::AbstractSparseMatrixCSC; if Tv <: Real && !(eltype(S) <: Real) throw(ArgumentError("cannot refactorize the real $(typeof(F)) with a matrix of eltype $(eltype(S)); use lu(S) instead")) end + _checkconvertible(Tv, getnzval(S)) @lock F.lock begin # the symbolic factors are those of a matrix of another size @@ -1163,7 +1164,8 @@ function _AqldivB_kernel!(x::StridedVector{Tb}, lu::UmfpackLU{Float64}, b::StridedVector{Tb}, transposeoptype, workspace) where Tb<:Complex r = similar(b, Float64) i = similar(b, Float64) - c = real.(b) + c = similar(b, Float64) + c .= real.(b) solve!(r, lu, c, transposeoptype; workspace) c .= imag.(b) solve!(i, lu, c, transposeoptype; workspace) diff --git a/src/sparseconvert.jl b/src/sparseconvert.jl index 3f05adcd..2678d4bf 100644 --- a/src/sparseconvert.jl +++ b/src/sparseconvert.jl @@ -11,6 +11,9 @@ for wr in (Symmetric, Hermitian, Transpose, Adjoint, @eval SparseMatrixCSC{Tv}(A::$wr) where Tv = SparseMatrixCSC{Tv}(_sparsem(A)) @eval SparseMatrixCSC{Tv,Ti}(A::$wr) where {Tv,Ti} = SparseMatrixCSC{Tv,Ti}(_sparsem(A)) end +# a vector view converts to an `n x 1` matrix, as a sparse vector does +SparseMatrixCSC(A::SubArray{<:Any,1}) = SparseMatrixCSC(_sparsem(A)) +SparseMatrixCSC{Tv}(A::SubArray{Tv,1}) where Tv = SparseMatrixCSC(_sparsem(A)) """ iswrsparse(::S) @@ -66,6 +69,8 @@ function _sparsem(@nospecialize A::AbstractArray{Tv}) where Tv A end else + # a dense vector has no matrix constructor, as `Matrix(v)` has none + A isa AbstractMatrix || throw(MethodError(SparseMatrixCSC, (A,))) # explicitly call abstract matrix fallback using getindex(A,...) invoke(SparseMatrixCSC{Tv,Int}, Tuple{AbstractMatrix}, A) end diff --git a/src/sparsematrix.jl b/src/sparsematrix.jl index 584fda58..02a782af 100644 --- a/src/sparsematrix.jl +++ b/src/sparsematrix.jl @@ -56,13 +56,19 @@ end function SparseMatrixCSC(m::Integer, n::Integer, colptr::Vector, rowval::Vector, nzval::Vector) Tv = eltype(nzval) Ti = promote_type(eltype(colptr), eltype(rowval)) + eltype(colptr) === eltype(rowval) || + throw(ArgumentError("colptr and rowval must have the same element type, got $(eltype(colptr)) and $(eltype(rowval))")) sparse_check_Ti(m, n, Ti) sparse_check(n, colptr, rowval, nzval) # silently shorten rowval and nzval to usable index positions. maxlen = abs(widemul(m, n)) isbitstype(Ti) && (maxlen = min(maxlen, typemax(Ti) - 1)) - length(rowval) > maxlen && resize!(rowval, maxlen) - length(nzval) > maxlen && resize!(nzval, maxlen) + if length(rowval) > maxlen || length(nzval) > maxlen + length(colptr) == n + 1 && colptr[end] - 1 == min(length(rowval), maxlen) == min(length(nzval), maxlen) || + throw(ArgumentError("Invalid buffers for SparseMatrixCSC construction n=$n, colptr=$(summary(colptr)), rowval=$(summary(rowval)), nzval=$(summary(nzval))")) + length(rowval) > maxlen && resize!(rowval, maxlen) + length(nzval) > maxlen && resize!(nzval, maxlen) + end SparseMatrixCSC{Tv,Ti}(m, n, colptr, rowval, nzval) end @@ -187,6 +193,29 @@ const _SparseTriOrSymHerm{Tv,Ti} = Union{UpperTriangular{Tv,<:SparseMatrixCSCOrV SparseMatrixCSCSymmHerm{Tv,Ti}} _storageparent(S) = S _storageparent(S::_SparseTriOrSymHerm) = parent(S) + +# The legacy interface of `AbstractSparseMatrixCSC`: a subtype that implements `rowvals` and +# `nonzeros` in place of `getrowval` and `getnzval`. Kernels reach these fallbacks once per +# entry and `Base.depwarn` walks the backtrace on every call, so it is skipped when warnings +# are off and made once per type and accessor when they are on. +const _legacy_accessor_warned = Set{Tuple{Core.TypeName,Symbol}}() +const _legacy_accessor_lock = ReentrantLock() +@inline _depwarn_legacy_accessor(S, get::Symbol, legacy::Symbol) = + Base.JLOptions().depwarn == 0 ? nothing : _depwarn_legacy_accessor_on(S, get, legacy) +@noinline function _depwarn_legacy_accessor_on(S, get::Symbol, legacy::Symbol) + if Base.JLOptions().depwarn == 1 + key = (Base.typename(typeof(S)), get) + seen = @lock _legacy_accessor_lock (key in _legacy_accessor_warned || + (push!(_legacy_accessor_warned, key); false)) + seen && return nothing + end + T = nameof(typeof(S)) + Base.depwarn("implementing `$legacy(::$T)` as a storage accessor of a subtype of " * + "`AbstractSparseMatrixCSC` is deprecated, implement `SparseArrays.$get(::$T)` instead.", + get) + return nothing +end + """ getrowval(A) @@ -203,6 +232,9 @@ the parent's vector, of which `rowvals(A)` is the part belonging to `A`. It is n defined for a triangular, `Symmetric` or `Hermitian` wrapper of a sparse matrix; use `getrowval(parent(A))`, which also holds the entries outside the wrapper's triangle. +A subtype of [`AbstractSparseMatrixCSC`](@ref) implements `getrowval`. Calling it on a subtype +that implements only `rowvals` is deprecated. + # Examples ```jldoctest julia> A = sparse(2I, 3, 3) @@ -225,8 +257,7 @@ julia> getrowval(sparsevec([2, 5], [3.0, 4.0])) """ getrowval(S::SparseMatrixCSC) = getfield(S, :rowval) getrowval(S::FixedSparseCSC) = getfield(S, :rowval) -# a subtype may implement the longer-exported `rowvals` instead -getrowval(S::AbstractSparseMatrixCSC) = rowvals(S) +getrowval(S::AbstractSparseMatrixCSC) = (_depwarn_legacy_accessor(S, :getrowval, :rowvals); rowvals(S)) getrowval(S::SparseMatrixCSCColumnSubset) = getrowval(parent(S)) """ @@ -244,6 +275,9 @@ the parent's vector, of which `nonzeros(A)` is the part belonging to `A`. It is defined for a triangular, `Symmetric` or `Hermitian` wrapper of a sparse matrix; use `getnzval(parent(A))`, which also holds the entries outside the wrapper's triangle. +A subtype of [`AbstractSparseMatrixCSC`](@ref) implements `getnzval`. Calling it on a subtype +that implements only `nonzeros` is deprecated. + # Examples ```jldoctest julia> A = sparse(2I, 3, 3) @@ -266,8 +300,7 @@ julia> getnzval(sparsevec([2, 5], [3.0, 4.0])) """ getnzval(S::SparseMatrixCSC) = getfield(S, :nzval) getnzval(S::FixedSparseCSC) = getfield(S, :nzval) -# a subtype may implement the longer-exported `nonzeros` instead -getnzval(S::AbstractSparseMatrixCSC) = nonzeros(S) +getnzval(S::AbstractSparseMatrixCSC) = (_depwarn_legacy_accessor(S, :getnzval, :nonzeros); nonzeros(S)) getnzval(S::SparseMatrixCSCColumnSubset) = getnzval(parent(S)) nzvalview(S::AbstractSparseMatrixCSC) = view(getnzval(S), 1:nnz(S)) nzvalview(S::SparseMatrixCSCColumnSubset) = nonzeros(S) @@ -286,7 +319,8 @@ function _storedinds(S::SparseMatrixCSCColumnSubset) end return inds end -widelength(S::SparseMatrixCSCColumnSubset) = prod(Int64.(size(S))) +widelength(S::SparseMatrixCSCColumnSubset) = *(map(widen, size(S))...) +widelength(x::Union{SparseColumnView,SparseVectorView}) = length(x) # `A` itself where it exposes compressed storage through `nzrange`, `rowvals` and `nonzeros`, # otherwise its sparse copy _compressed(A) = A @@ -320,7 +354,7 @@ nnz(S::SparseMatrixCSCView) = length(_storedinds(S)) nnz1(S) = @inbounds sum(length.(getnzrange.(Ref(S), axes(S, 2)))) function Base._simple_count(pred, S::SparseMatrixCSCOrColumnSubset, init::T) where T - init + T(count(pred, nzvalview(S)) + pred(zero(eltype(S)))*(prod(size(S)) - nnz(S))) + init + T(count(pred, nzvalview(S)) + pred(zero(eltype(S)))*(widelength(S) - nnz(S))) end # the entries of a lazy adjoint or transpose are those of its parent, each under the wrapper Base._simple_count(pred, S::AdjOrTrans{<:Any,<:SparseMatrixCSCOrColumnSubset}, init) = @@ -950,7 +984,7 @@ similar(S::AbstractSparseMatrixCSC, ::Type{TvNew}, ::Type{TiNew}, m::Integer, n: _sparsesimilar(S, TvNew, TiNew, (m, n)) function Base.sizehint!(S::SparseMatrixCSC, n::Integer) - nhint = min(n, widelength(S)) + nhint = Int(min(n, widelength(S))) sizehint!(getrowval(S), nhint) sizehint!(getnzval(S), nhint) return S @@ -1511,8 +1545,8 @@ Base.sort(A::AbstractSparseMatrixCSC; kws...) = ## fkeep! and children tril!, triu!, droptol!, dropzeros[!] -# The predicates of `tril!`, `droptol!` and the other children do not throw, and call this -# with no `progress`. `fkeep!` passes one for a caller's `f`: see `_fkeep_guarded!`. +# The predicates of `tril!` and `triu!` do not throw, and call this with no `progress`. Those +# of `fkeep!`, `droptol!` and `dropzeros!` can, for some eltypes: see `_fkeep_guarded!`. function _fkeep!(f::F, A::AbstractSparseMatrixCSC, progress::P=nothing) where {F<:Function,P} An = size(A, 2) Acolptr = getcolptr(A) @@ -1656,7 +1690,7 @@ julia> droptol!(A, 1e-6) ``` """ droptol!(A::AbstractSparseMatrixCSC, tol) = - (_checkpatternwritable(A, :droptol!); _fkeep!((i, j, x) -> abs(x) > tol, A)) + (_checkpatternwritable(A, :droptol!); _fkeep_guarded!((i, j, x) -> abs(x) > tol, A)) """ dropzeros!(A::AbstractSparseMatrixCSC) @@ -1665,7 +1699,7 @@ Remove the stored zeros from `A`, in place, and return `A`. See also [`dropzeros`](@ref), which returns a copy, and [`droptol!`](@ref). """ -dropzeros!(A::AbstractSparseMatrixCSC) = (_checkpatternwritable(A, :dropzeros!); _fkeep!((i, j, x) -> _isnotzero(x), A)) +dropzeros!(A::AbstractSparseMatrixCSC) = (_checkpatternwritable(A, :dropzeros!); _fkeep_guarded!((i, j, x) -> _isnotzero(x), A)) """ dropzeros(A::AbstractSparseMatrixCSC;) @@ -2265,6 +2299,8 @@ function circshift!(O::AbstractSparseMatrixCSC, X::AbstractSparseMatrixCSC, (r,c Base.mightalias(getnzval(O), getnzval(X))) && throw(ArgumentError(LazyString("cannot shift the columns of a ", nameof(typeof(X)), " into a destination that shares its buffers; use `circshift` or a separate destination"))) + _checkconvertible(eltype(O), view(getnzval(X), 1:nz)) + _checknnzfits(indtype(O), nz) ##### readjust output resize!(getcolptr(O), size(X, 2) + 1) resize!(getrowval(O), nz) diff --git a/src/sparsevector.jl b/src/sparsevector.jl index 9c10b931..378c2d59 100644 --- a/src/sparsevector.jl +++ b/src/sparsevector.jl @@ -63,8 +63,8 @@ length(x::FixedSparseVector) = getfield(x, :n) size(x::SparseVector) = (getfield(x, :n),) size(x::FixedSparseVector) = (getfield(x, :n),) -function Base._simple_count(f, x::AbstractCompressedVector, init::T) where T - init + T(count(f, nonzeros(x)) + f(zero(eltype(x)))*(length(x) - nnz(x))) +function Base._simple_count(f, x::Union{SparseVectorOrView,SparseVectorPartialView}, init::T) where T + init + T(count(f, nonzeros(x)) + f(_densezero(x))*(length(x) - nnz(x))) end # implement the nnz - nzrange - nonzeros - rowvals interface for sparse vectors @@ -163,11 +163,9 @@ rowvals(x::SparseVectorOrView) = nonzeroinds(x) indtype(x::SparseColumnView) = indtype(parent(x)) indtype(x::Union{SparseVectorView, SparseVectorPartialView}) = indtype(parent(x)) -# the sparse vector a view reads as, built from its stored entries: `copy` of a column view -# widens the index type to `Int` and `SparseVector(view)` scans every element +# the sparse vector a view reads as _sparsevector(x::AbstractCompressedVector) = x -_sparsevector(x::Union{SparseColumnView,SparseVectorView,SparseVectorPartialView}) = - SparseVector(length(x), convert(Vector{indtype(x)}, nonzeroinds(x)), convert(Vector{eltype(x)}, nonzeros(x))) +_sparsevector(x::Union{SparseColumnView,SparseVectorView,SparseVectorPartialView}) = SparseVector(x) function Base.sizehint!(v::SparseVector, newlen::Integer) @@ -555,6 +553,7 @@ sparsevec(a::AbstractVector{T}) where {T} = SparseVector{T, Int}(a) sparsevec(a::AbstractArray) = sparsevec(vec(a)) sparsevec(a::AbstractSparseMatrixCSC) = a[:] sparsevec(a::AbstractSparseVector) = vec(a) +sparsevec(a::Union{SparseColumnView,SparseVectorView,SparseVectorPartialView}) = SparseVector(a) sparse(a::AbstractVector) = sparsevec(a) function _dense2indval!(nzind::Vector{Ti}, nzval::Vector{Tv}, s::AbstractArray) where {Tv,Ti} @@ -595,9 +594,9 @@ SparseVector{Tv}(s::AbstractVector) where {Tv} = SparseVector{Tv,Int}(s) SparseVector(s::AbstractVector{Tv}) where {Tv} = SparseVector{Tv,Int}(s) # copy-constructors -SparseVector(s::AbstractCompressedVector{Tv,Ti}) where {Tv,Ti} = SparseVector{Tv,Ti}(s) -SparseVector{Tv}(s::AbstractCompressedVector{<:Any,Ti}) where {Tv,Ti} = SparseVector{Tv,Ti}(s) -function SparseVector{Tv,Ti}(s::AbstractCompressedVector) where {Tv,Ti} +SparseVector(s::Union{SparseVectorOrView{Tv,Ti},SparseVectorPartialView{Tv,Ti}}) where {Tv,Ti} = SparseVector{Tv,Ti}(s) +SparseVector{Tv}(s::Union{SparseVectorOrView{<:Any,Ti},SparseVectorPartialView{<:Any,Ti}}) where {Tv,Ti} = SparseVector{Tv,Ti}(s) +function SparseVector{Tv,Ti}(s::Union{SparseVectorOrView,SparseVectorPartialView}) where {Tv,Ti} copyind = Vector{Ti}(nonzeroinds(s)) copynz = Vector{Tv}(nonzeros(s)) SparseVector{Tv,Ti}(length(s), copyind, copynz) @@ -669,11 +668,12 @@ end function copyto!(A::AbstractCompressedVector, B::AbstractSparseMatrixCSC) _is_fixed(A) && return _copyto_fixed!(A, copyto!(spzeros(eltype(A), indtype(A), length(B)), B)) + maximum(getcolptr(B))-1 <= length(getrowval(B)) || throw(BoundsError()) + _checkconvertible(eltype(A), getnzval(B)) prep_sparsevec_copy_dest!(A, length(B), nnz(B)) ptr = 1 @assert length(nonzeroinds(A)) >= length(getrowval(B)) - maximum(getcolptr(B))-1 <= length(getrowval(B)) || throw(BoundsError()) @inbounds for col=1:length(getcolptr(B))-1 offsetA = (col - 1) * size(B, 1) while ptr <= getcolptr(B)[col+1]-1 @@ -767,11 +767,16 @@ end ### Rand Construction -sprand(n::Integer, p::AbstractFloat, rfn::Function, ::Type{T}) where {T} = sprand(default_rng(), n, p, rfn, T) +# `rfn(rng, T, k)` predates the documented `rfn(k)` and `rfn(rng, k)` and is still accepted +function sprand(n::Integer, p::AbstractFloat, rfn::Function, ::Type{T}) where {T} + r = default_rng() + applicable(rfn, r, T, 0) && return sprand(r, n, p, rfn, T) + return sprand(r, n, p, (r, i) -> rfn(i), T) +end function sprand(r::AbstractRNG, n::Integer, p::AbstractFloat, rfn::Function, ::Type{T}) where T I = randsubseq(r, 1:convert(Int, n), p) - V = rfn(r, T, length(I)) - SparseVector(n, I, V) + V = applicable(rfn, r, T, length(I)) ? rfn(r, T, length(I)) : rfn(r, length(I)) + SparseVector(n, I, convert(Vector{T}, V)) end sprand(n::Integer, p::AbstractFloat, rfn::Function) = sprand(default_rng(), n, p, (r, i) -> rfn(i)) @@ -1789,11 +1794,15 @@ function _checktrisolve(xA::UpperOrLowerTriangular, b::AbstractVector) return nothing end -function \(xA::StridedTriangularMaybeAdjOrTrans{TA}, b::AbstractCompressedVector{Tb}) where {TA<:Number,Tb<:Number} - _checktrisolve(xA, b) - TAb = xA isa UnitUpperOrUnitLowerTriangular ? +# the eltype of the solution of a triangular solve: a unit triangle does not divide +_trisolve_eltype(xA::UpperOrLowerTriangular{TA}, ::AbstractVector{Tb}) where {TA<:Number,Tb<:Number} = + xA isa UnitUpperOrUnitLowerTriangular ? typeof(zero(TA)*zero(Tb) + zero(TA)*zero(Tb)) : typeof((zero(TA)*zero(Tb) + zero(TA)*zero(Tb))/one(TA)) + +function \(xA::StridedTriangularMaybeAdjOrTrans{TA}, b::AbstractCompressedVector{Tb}) where {TA<:Number,Tb<:Number} + _checktrisolve(xA, b) + TAb = _trisolve_eltype(xA, b) r = convert(Array{TAb}, b) if nnz(b) != 0 active = _activerange(xA, b) @@ -1806,6 +1815,14 @@ function ldiv!(xA::StridedTriangularMaybeAdjOrTrans, b::AbstractCompressedVector _checktrisolve(xA, b) nnz(b) == 0 && return b active = _activerange(xA, b) + # a solution that may not convert to the eltype of `b` is solved out of place and checked, + # so that a failed conversion throws before `b` takes the dense pattern + if eltype(xA) <: Number && eltype(b) <: Number && !_storesafely(eltype(b), _trisolve_eltype(xA, b)) + x = view(xA \ b, active) + _checkconvertible(eltype(b), x) + copyto!(view(nonzeros(b), _densify!(b, active)), x) + return b + end # store the whole active section up front rather than entry by entry during the solve, # which also lets the solve run on a dense vector and reach tuned BLAS stored = _densify!(b, active) @@ -1888,8 +1905,7 @@ end Base.sort(x::AbstractCompressedVector; kws...) = sort!(_writablecopy(x); kws...) -function fkeep!(f, x::AbstractCompressedVector{Tv}) where Tv - _checkpatternwritable(x, :fkeep!) +function _fkeep!(f::F, x::AbstractCompressedVector, progress::Base.RefValue{Tuple{Int,Int}}) where F nzind = nonzeroinds(x) nzval = nonzeros(x) @@ -1897,6 +1913,7 @@ function fkeep!(f, x::AbstractCompressedVector{Tv}) where Tv @inbounds for xk in 1:nnz(x) xi = nzind[xk] xv = nzval[xk] + progress[] = (xk, x_writepos) # If this element should be kept, rewrite in new position if f(xi, xv) if x_writepos != xk @@ -1914,7 +1931,28 @@ function fkeep!(f, x::AbstractCompressedVector{Tv}) where Tv return x end +# As for a matrix: when `f` throws at entry `xk`, keep it and every later entry, and rethrow. +function _fkeep_guarded!(f::F, x::AbstractCompressedVector) where F + progress = Ref((1, 1)) + try + _fkeep!(f, x, progress) + catch + xk, x_writepos = progress[] + nzind, nzval = nonzeroinds(x), nonzeros(x) + shift = xk - x_writepos + if shift != 0 + x_nnz = length(nzind) + copyto!(nzind, x_writepos, nzind, xk, x_nnz - xk + 1) + copyto!(nzval, x_writepos, nzval, xk, x_nnz - xk + 1) + resize!(nzind, x_nnz - shift) + resize!(nzval, x_nnz - shift) + end + rethrow() + end + return x +end +fkeep!(f::F, x::AbstractCompressedVector) where {F} = (_checkpatternwritable(x, :fkeep!); _fkeep_guarded!(f, x)) """ droptol!(x::AbstractCompressedVector, tol) diff --git a/test/concatenation.jl b/test/concatenation.jl index 726cd3fb..ddcdeef8 100644 --- a/test/concatenation.jl +++ b/test/concatenation.jl @@ -36,6 +36,10 @@ include("testhelpers.jl") se33_32bit = convert(SparseMatrixCSC{Float32,Int32}, se33) @test [se33; se33_32bit] == [Array(se33); Array(se33_32bit)] @test length(nonzeros([sp33; 0I])) == 3 + # the 128 stored entries of the result do not fit `Int8` + A8 = SparseMatrixCSC{Float64,Int8}(sparse(ones(8, 8))) + @test_throws ArgumentError vcat(A8, A8) + @test_throws ArgumentError repeat(A8, 2) end end @@ -87,7 +91,7 @@ include("testhelpers.jl") A32 = SparseMatrixCSC{Float64,Int32}(A) v32 = SparseVector{Float64,Int32}(vz) @test [A32 v32; v32' 1.0] isa SparseMatrixCSC{Float64,Int32} - @test [1.0 v32'; v32 A32] isa SparseMatrixCSC{Float64,Int} # a leading number widens + @test [1.0 v32'; v32 A32] isa SparseMatrixCSC{Float64,Int32} end @test_throws DimensionMismatch [A vz; 1 2] @static if COMPREHENSIVE @@ -138,6 +142,16 @@ include("testhelpers.jl") sz33 = spzeros(3, 3) de33 = Matrix(1.0I, 3, 3) @test [se33 de33; sz33 se33] == Array([se33 se33; sz33 se33 ]) + # the index type is promoted over the sparse blocks, not taken from the first + A8, A = spzeros(Float64, Int8, 1, 2), spzeros(200, 2) + @test mismatch(vcat(A8, A, ones(1, 2)), [zeros(201, 2); ones(1, 2)], Ti=Int) === nothing + v8, v = spzeros(Float64, Int8, 1), spzeros(200) + @test mismatch(vcat(v8, v, ones(1)), [zeros(201); 1], Ti=Int) === nothing + # a dense block or `I` adds no index type, wherever it is + @test mismatch(hcat(ones(1), v8), [1.0 0.0], Ti=Int8) === nothing + @test mismatch(hcat(I, A8'), [Matrix(1.0I, 2, 2) zeros(2)], Ti=Int8) === nothing + @test mismatch([A8' ones(2); A8' ones(2)], [zeros(2) ones(2); zeros(2) ones(2)], Ti=Int8) === nothing + @test mismatch(sparse_hcat(ones(2), A8', I), [ones(2) zeros(2) Matrix(1.0I, 2, 2)], Ti=Int8) === nothing end end @@ -389,9 +403,9 @@ end @static if COMPREHENSIVE B = sparse(I, 4, 4) @test mismatch(@inferred(hvcat(rows, S, B, B, S)), [dS I; I dS]; Ti=Int) === nothing - # a dense block converts to `Int` indices + # a dense block adds no index type S32 = SparseMatrixCSC{Float64,Int32}(S) - @test mismatch(@inferred(lit22(S32, A)), [dS A; A A]; Ti=Int) === nothing + @test mismatch(@inferred(lit22(S32, A)), [dS A; A A]; Ti=Int32) === nothing end end @@ -580,11 +594,12 @@ end @test mismatch(sparse_vcat(1, 2), [1, 2]) === nothing @static if COMPREHENSIVE @test sparse_hcat(1, 2)::SparseMatrixCSC == [1 2] - # a leading number widens a narrow index type as it did before - v8 = sparsevec(Int8[127], [2], 127); A8 = sparse(Int8[127], Int8[1], [2], 127, 2) - @test vcat(1, v8)::SparseVector{Int,Int} == vcat(1, Array(v8)) - @test sparse_vcat(1, v8)::SparseVector{Int,Int} == vcat(1, Array(v8)) - @test vcat(1, A8)::SparseMatrixCSC{Int,Int} == vcat(1, Array(A8)) + # a leading number adds no index type, and the size of the result is checked against it + v8 = sparsevec(Int8[126], [2], 126); A8 = sparse(Int8[126], Int8[1], [2], 126, 2) + @test vcat(1, v8)::SparseVector{Int,Int8} == vcat(1, Array(v8)) + @test sparse_vcat(1, v8)::SparseVector{Int,Int8} == vcat(1, Array(v8)) + @test vcat(1, A8)::SparseMatrixCSC{Int,Int8} == vcat(1, Array(A8)) + @test_throws ArgumentError vcat(1, 2, v8) # a vector whose length is not an `Int` stacked on a matrix w8 = sparsevec(Int8[1], [2.0], 2) @test mismatch(vcat(w8, ones(1, 1)), vcat(Array(w8), ones(1, 1)), Ti=Int8) === nothing diff --git a/test/constructors.jl b/test/constructors.jl index 91be8dad..9f02a728 100644 --- a/test/constructors.jl +++ b/test/constructors.jl @@ -30,6 +30,9 @@ end @static if COMPREHENSIVE @test SparseMatrixCSC{ComplexF64}(Array(a)')::SparseMatrixCSC{ComplexF64,Int} == a' @test Array(SparseMatrixCSC{eltype(a), Int8}(a)) == Array(a) + @test mismatch(SparseMatrixCSC(view(a, :, 1)), Array(a)[:, 1:1]) === nothing + # a view of a dense vector has no matrix constructor, as for `Matrix` + @test_throws MethodError SparseMatrixCSC(view([1.0, 2.0], :)) end @test collect(a) == a @test SparseMatrixCSC(a)::typeof(a) == a @@ -80,6 +83,11 @@ end @test isequal(sparse(big.([1,1,1,2,2,3,4,5]),big.([1,2,3,2,3,3,4,5]),big.([1,2,4,3,5,6,7,8]), 6, 6), SparseMatrixCSC(6, 6, big.([1,2,4,7,8,9,9]), big.([1,1,2,1,2,3,4,5]), big.([1,2,3,4,5,6,7,8]))) @test sparse(Any[1,2,3], Any[1,2,3], Any[1,1,1]) == sparse([1,2,3], [1,2,3], [1,1,1]) + # an invalid call does not shorten the caller's vectors + rv, nz = [1, 1], [1.0, 2.0] + @test_throws ArgumentError SparseMatrixCSC(1, 1, [1, 3], rv, nz) + @test_throws ArgumentError SparseMatrixCSC(1, 1, Int32[1, 2], rv, nz) + @test rv == [1, 1] && nz == [1.0, 2.0] end # with combine @test sparse([1, 1, 2, 2, 2], [1, 2, 1, 2, 2], 1.0, 2, 2, +) == sparse([1, 1, 2, 2], [1, 2, 1, 2], [1.0, 1.0, 1.0, 2.0], 2, 2) @@ -290,6 +298,7 @@ end @static if COMPREHENSIVE m = sprand(Float32, 10, 10, 0.1) @test eltype(m) == Float32 + @test eltype(sprand(10, 10, 0.5, k -> rand(Int8, k), Float64)) == Float64 end m = sprand(Float64, 10, 10, 0.1) @test eltype(m) == Float64 @@ -494,6 +503,14 @@ end end end end + + @static if COMPREHENSIVE + # a short workspace throws before anything is written + csrrowptr = [7, 7] + @test_throws ArgumentError sparse!([1, 2], [1, 2], [1.0, 2.0], 2, 2, +, + [0, 0], csrrowptr, [0, 0], [0.0, 0.0]) + @test csrrowptr == [7, 7] + end end end # module diff --git a/test/higherorderfns.jl b/test/higherorderfns.jl index 7ef4578d..c8ad4bdd 100644 --- a/test/higherorderfns.jl +++ b/test/higherorderfns.jl @@ -808,6 +808,10 @@ densesum(x, y) = x .+ y @test @inferred(broadcast(+, s, d))::Vector{Float64} == fs + d @test @inferred(broadcast(-, D, S))::Matrix{Float64} == D - fS @test @inferred(broadcast(+, s, 1))::Vector{Float64} == fs .+ 1 + # a sum of a sparse array with few stored entries and a scalar makes no dense copy of it + L = sparse([2, 400], [1, 3], [-0.0, NaN], 400, 3) + @test isequal(@inferred(densesum(L, 1))::Matrix{Float64}, Array(L) .+ 1) + @test @allocated(densesum(L, 1)) < 2 * sizeof(Array(L)) # the other broadcasts with a dense array or a scalar, and sums without one, stay sparse @test (S .* D)::SparseMatrixCSC == fS .* D @test (S .- S)::SparseMatrixCSC == fS .- fS @@ -830,6 +834,10 @@ densesum(x, y) = x .+ y @test @inferred(broadcast(-, 1, S))::Matrix{Float64} == 1 .- fS @test (s .+ fill(1.0))::Vector{Float64} == fs .+ 1 @test (S .+ s .- Ref(2))::Matrix{Float64} == fS .+ fs .- 2 + l = sparsevec([5], [2.0], 400) + @test isequal((L .+ l .- 2)::Matrix{Float64}, Array(L) .+ Array(l) .- 2) + R = SparseMatrixCSC{Real,Int}(400, 3, [1, 2, 2, 2], [7], Real[1.5]) + @test (R .+ 1)::Matrix{Real} == Array(R) .+ 1 @test (.-s)::SparseVector == .-fs # a sparse destination keeps the result sparse x = copy(s); x .+= d; x .-= 1 diff --git a/test/issues.jl b/test/issues.jl index eb1972d7..b765ce0f 100644 --- a/test/issues.jl +++ b/test/issues.jl @@ -968,6 +968,20 @@ end @inferred hcat(fixture(Float64, 2, 1), Matrix(1.0I, 2, 2)) end +@testset "a matrix of 2^64 entries does not overflow its length" begin + B = spzeros(2^62, 4) + @test any(iszero, B) && !all(isone, B) + @test sum(x -> 1.0, B) == 2.0^64 && prod(x -> 2.0, B) == Inf + @test count(iszero, B; init = big(0)) == big(2)^64 + @test findmax(B) == (0.0, CartesianIndex(1, 1)) + @test mapreduce(*, +, B, B) == 0.0 + @test_throws OverflowError B[[1]] + @test_throws OverflowError B[[1]] = [1.0] + @test_throws OverflowError B[1] = 1.0 + @test nnz(B) == 0 + @test_throws OverflowError sprand(Xoshiro(1), 2^62, 4, 1e-17) +end + if isdefined(Docs, :undocumented_names) # new in Julia 1.11 @testset "docstrings (issue julia#52725)" begin @test isempty(Docs.undocumented_names(SparseArrays)) diff --git a/test/linalg.jl b/test/linalg.jl index d39ca267..24bb3f90 100644 --- a/test/linalg.jl +++ b/test/linalg.jl @@ -59,6 +59,13 @@ include("testhelpers.jl") @test mismatch(O, [1 0 2; 0 3 0; 4 0 5.0]) === nothing end @test mismatch(circshift!(S, S, (1, 0)), [4 0 5.0; 1 0 2; 0 3 0]) === nothing + # a value or a count that the destination cannot hold is rejected before it is written to + O = sparse([1 0 0; 0 2 0; 0 0 3]) + @test_throws InexactError circshift!(O, sparse([1.5 0 0; 0 0 0; 0 0 0]), (0, 1)) + @test mismatch(O, [1 0 0; 0 2 0; 0 0 3]) === nothing + O8 = SparseMatrixCSC{Float64,Int8}(sparse(1.0I, 12, 12)) + @test_throws ArgumentError circshift!(O8, sparse(ones(12, 12)), (0, 1)) + @test mismatch(O8, Matrix(1.0I, 12, 12); Ti=Int8) === nothing end end @@ -344,6 +351,15 @@ end C = spzeros(ComplexF64, 3, 3) @test_throws DimensionMismatch ldiv!(C, Dl, A) @test nnz(C) == 0 + @static if COMPREHENSIVE + # a quotient that does not convert to the eltype of the destination throws with it unchanged + A0 = sparse([2 0; 0 3]); x0 = sparsevec([1, 2], [2, 3]); D2 = Diagonal([2, 2]) + for (f!, X0) in ((C -> ldiv!(C, D2, A0), sparse([1 0; 0 0])), (B -> rdiv!(B, D2), A0), (y -> ldiv!(D2, y), x0)) + X = copy(X0) + @test_throws InexactError f!(X) + @test mismatch(X, X0) === nothing + end + end end end @@ -548,6 +564,8 @@ end @test (dims == 1 ? 3 : 4) ∉ view(rowvals(D), nzrange(D, dims == 1 ? 2 : 1)) @static if COMPREHENSIVE @test diff(sparse(Int32.(1:7), Int32.(1:7), one(T)); dims) isa SparseMatrixCSC{T,Int32} + @test mismatch(diff(view(A, :, [1, 2, 4]); dims), diff(M[:, [1, 2, 4]]; dims)) === nothing + @test mismatch(diff(transpose(A); dims), diff(transpose(M); dims)) === nothing end end for dims in (0, 3, 7) @@ -700,6 +718,13 @@ end x = fixturevec(Float64, 10); y = ones(Int, 9) @test spdiagm(0 => x, 1 => y)::SparseMatrixCSC{Float64,Int} == Bidiagonal(Vector(x), ones(9), :U) @test nnz(spdiagm(0 => x, 1 => y)) == length(y) + nnz(x) + @static if COMPREHENSIVE + # so do views of a sparse vector or column + A = fixture(Float64, 10, 3) + for v in (view(x, :), view(x, 2:9), view(A, :, 1)) + @test exact_equal(spdiagm(-1 => v), spdiagm(-1 => copy(v))) + end + end end @static if COMPREHENSIVE diff --git a/test/matmul.jl b/test/matmul.jl index 4d458f9d..5721a67a 100644 --- a/test/matmul.jl +++ b/test/matmul.jl @@ -573,6 +573,15 @@ end @test mismatch(mul!(sC, sA, 0.5), 0.5 * dA) === nothing @test mismatch(lmul!(0.5, copy(sA)), 0.5 * dA) === nothing @test mul!(sC, 0.5, sA) == mul!(sC, sA, 0.5) + @static if COMPREHENSIVE + # a product that does not convert to the eltype of `C` throws before `C` takes the pattern of `A` + A0 = sparse([1 0; 0 3]); C0 = sparse([2 0; 0 0]) + for f! in (C -> mul!(C, A0, 0.5), C -> mul!(C, 0.5, A0)) + C = copy(C0) + @test_throws InexactError f!(C) + @test mismatch(C, C0) === nothing + end + end @testset "inverse scaling with mul!" begin bi = inv.(b) @@ -963,6 +972,15 @@ end @test all(nonzeros(y) .!= 0.0) @test mismatch(y, Af * xf; Ti=Int, approx=true) === nothing @test mismatch((A * view(x, :))::SparseVector{Float64,Int}, Af * xf; approx=true) === nothing + @static if COMPREHENSIVE + # a range view multiplies as a whole view of the sparse vector it reads as + w = view(x, 3:11) + S = A[:, 3:11] + for M in (S, view(A, :, 3:11), UpperTriangular(S), Symmetric(S), UpperTriangular(S)') + @test which(*, (typeof(M), typeof(w))).module === SparseArrays + @test exact_equal(M * w, M * view(x[3:11], :)) + end + end y = *(transpose(A), x2) @test isa(y, SparseVector{Float64,Int}) diff --git a/test/solvers/issues.jl b/test/solvers/issues.jl index 13bb94e5..2eff6dfa 100644 --- a/test/solvers/issues.jl +++ b/test/solvers/issues.jl @@ -195,6 +195,33 @@ end @test all(logabsdet(F) .≈ logabsdet(Matrix(A))) end +@testset "lu!(F, S) leaves F unchanged when the values of S do not convert" begin + A = sparse(ComplexF64[4 1 0 0; 1 4 1 0; 0 1 4 1; 0 0 1 4]) + F = lu(A) + colptr, rowval, nzval = copy(F.colptr), copy(F.rowval), copy(F.nzval) + S = SparseMatrixCSC{Any,Int}(2, 2, [1, 2, 4], [1, 1, 2], Any[1.0, "x", 1.0]) + @test_throws MethodError lu!(F, S) + @test size(F) == (4, 4) + @test (F.colptr, F.rowval, F.nzval) == (colptr, rowval, nzval) + b = ComplexF64[1, 2, 3, 4] + @test F \ b ≈ Matrix(A) \ b +end + +@testset "cholesky and cholesky! with an index type other than CHOLMOD's" begin + A = sparse([4.0 1 0; 1 4 1; 0 1 4]) + @test_throws ArgumentError cholesky(SparseMatrixCSC{Float64,UInt64}(A)) + F = cholesky(A) + @test cholesky!(F, SparseMatrixCSC{Float64,Int16}(2A)) \ [1.0, 2.0, 3.0] ≈ (2A) \ [1.0, 2.0, 3.0] +end + +@testset "ldlt! with a matrix of another size leaves an LLt factor unchanged" begin + A = sparse([4.0 1 0; 1 4 1; 0 1 4]) + F = cholesky(A) + @test_throws DimensionMismatch ldlt!(F, sparse(1.0I, 4, 4)) + @test :D ∉ propertynames(F) + @test F \ ones(3) ≈ A \ ones(3) +end + @testset "Issues #26367 and #26368" begin A = sparse([0.0 1 0 0; 0 0 0 0]) F = qr(A) @@ -212,4 +239,30 @@ end @test eltype(F.Q) == eltype(F.R) == eltyA end +@testset "sparsevec of a CHOLMOD.Sparse checks its shape and stype" begin + @test_throws DimensionMismatch sparsevec(CHOLMOD.Sparse(sparse([1.0 2; 0 3]))) + @test_throws ArgumentError sparsevec(CHOLMOD.Sparse(Symmetric(sparse([2.0 1; 1 2])))) +end + +@testset "ldiv! of a real UmfpackLU with a ComplexF32 vector" begin + A = sparse([2.0 1; 1 3]) + @test ldiv!(lu(A), ComplexF32[1, 2]) ≈ ComplexF32.(Matrix(A) \ [1, 2]) +end + +@testset "factorize of an indefinite Hermitian matrix whose LDLt needs pivoting" begin + b = [1.0, 2.0] + @test factorize(sparse([0.0 1; 1 0])) \ b ≈ [0.0 1; 1 0] \ b +end + +@testset "cond of a singular matrix is Inf" begin + @test cond(sparse([1.0 2; 3 6]), 1) === Inf + # `factorize` leaves the zero on the diagonal of a triangular or diagonal factor + @test cond(sparse([1.0 0; 2 0]), Inf) === Inf + @test cond(sparse([1.0 0; 0 0]), 1) === Inf + # Hermitian, so `factorize` tries Cholesky and LDLt before the LU finds it singular + @test cond(sparse([1.0 1; 1 1]), 1) === Inf + # the LU works in double precision, but the result is of the matrix's precision + @test cond(sparse(Float32[1 2; 3 4]), 1) isa Float32 +end + end # module diff --git a/test/solvers/solvers.jl b/test/solvers/solvers.jl index 7f763e38..21cbb8d1 100644 --- a/test/solvers/solvers.jl +++ b/test/solvers/solvers.jl @@ -129,7 +129,14 @@ end # the adjoint solve converts its result to the eltype the plain solve gives x = square' \ Float32.(b) @test x isa Vector{Float32} && x ≈ Matrix(square') \ b - # only the LU solve converts its result, so a least squares solve takes any right-hand side + # the least squares solve gives the eltype of dense `\`, and keeps the precision of `b` + x = @inferred Float32.(tall) \ b + @test x isa Vector{Float64} && x ≈ Matrix(tall) \ b + # so does the solve of a transposed complex Hermitian matrix with a matrix right-hand side + Ht = transpose(sparse(ComplexF32[4 im; -im 3])) + @test @inferred(Ht \ Float32[1 2; 3 4]) isa Matrix{ComplexF32} + # the solution for an abstract eltype is not converted, so a least squares solve takes + # any right-hand side @test tall \ Any[1.0, 2, 3] ≈ Matrix(tall) \ b end # when the Cholesky factorization of a Hermitian matrix fails, `factorize` returns diff --git a/test/solvers/spqr.jl b/test/solvers/spqr.jl index 2417d4a4..8adbd6ca 100644 --- a/test/solvers/spqr.jl +++ b/test/solvers/spqr.jl @@ -254,6 +254,8 @@ end b, B = collect(1.0:m), reshape(collect(1.0:3m), 3, m) eltyA <: Real && @test F.Q * b ≈ F.Q * eltyA.(b) eltyA <: Real && @test B * F.Q' ≈ eltyA.(B) * F.Q' + # a solve converts F to the precision of the solution, as dense `\` does + @test @inferred(F \ b) isa Vector{widen(eltyA)} end end diff --git a/test/sparsematrix.jl b/test/sparsematrix.jl index 3e6b169a..0e470d02 100644 --- a/test/sparsematrix.jl +++ b/test/sparsematrix.jl @@ -208,18 +208,24 @@ end @test isone(SparseMatrixCSC(2, 2, [1, 3, 4], [1, 2, 2], [1, 0, 1])) # stored zero off-diagonal is fine end -@testset "a subtype implementing rowvals and nonzeros but not the get* accessors" begin +@testset "the legacy interface: a subtype implementing rowvals and nonzeros but not the get* accessors" begin S = sparse([1.0 0 2; 0 3 0; 4 0 5]) A = LegacyCSC(S) - @test SparseArrays.getrowval(A) === rowvals(S) && SparseArrays.getnzval(A) === nonzeros(S) - @test nonzeros(view(A, :, 2:3)) == nonzeros(view(S, :, 2:3)) - @test A[2, 2] == 3 - @test copy(A) == S - x = [1.0, 2, 3] - @test A * x == S * x - @test A' * x == S' * x - @test A * S == S * A == A * A == S * S - @test A * Matrix(S) == S * Matrix(S) + # the warning is given once per type and accessor, so these come before any other use + @test_deprecated r"getrowval" SparseArrays.getrowval(A) + @test_deprecated r"getnzval" SparseArrays.getnzval(A) + # under --depwarn=error, which Julia's own CI uses, every use of the type throws + if Base.JLOptions().depwarn != 2 + @test SparseArrays.getrowval(A) === rowvals(S) && SparseArrays.getnzval(A) === nonzeros(S) + @test nonzeros(view(A, :, 2:3)) == nonzeros(view(S, :, 2:3)) + @test A[2, 2] == 3 + @test copy(A) == S + x = [1.0, 2, 3] + @test A * x == S * x + @test A' * x == S' * x + @test A * S == S * A == A * A == S * S + @test A * Matrix(S) == S * Matrix(S) + end end @testset "indtype" begin @@ -1277,6 +1283,9 @@ end A = sparse(D) @test_throws ErrorException SparseArrays.fkeep!((i, j, x) -> error("stop"), A) @test mismatch(A, D) === nothing + A = sparse([1, 2], [1, 2], Union{Missing,Float64}[0.1, missing], 2, 2) + @test_throws TypeError droptol!(A, 0.5) + @test isequal(A, sparse([2], [2], Union{Missing,Float64}[missing], 2, 2)) && nnz(A) == 1 end @testset "tril! and triu! with a diagonal out of range" begin diff --git a/test/sparsevector.jl b/test/sparsevector.jl index 07312934..7c1be97e 100644 --- a/test/sparsevector.jl +++ b/test/sparsevector.jl @@ -242,6 +242,14 @@ end @test xv == xr @test sprand(20, 1, 0.5, rfn1) isa SparseMatrixCSC{Int8,Int} @test sprand(MersenneTwister(5), 20, 1, 0.5, rfn2) isa SparseMatrixCSC{Int8,Int} + # and with `T`, which the older `rfn(rng, T, k)` still accepts + @test sprand(20, 0.5, rfn1, Float64) isa SparseVector{Float64,Int} + @test sprand(MersenneTwister(5), 20, 0.5, rfn2, Float64) isa SparseVector{Float64,Int} + @test sprand(MersenneTwister(5), 20, 0.5, (r, T, k) -> rand(r, T, k), Float64) isa SparseVector{Float64,Int} + # without `T`, `rfn` is called once, for the values + calls = Ref(0) + sprand(MersenneTwister(5), 4, 4, 0.5, (r, k) -> (calls[] += 1; rand(r, Int8, k))) + @test calls[] == 1 end end end @@ -679,6 +687,8 @@ end @test mismatch(dst, [1, 0, 0, 0, 0]) === nothing && nnz(dst) == 1 @test_throws InexactError copyto!(dst, src) @test mismatch(dst, [1, 0, 0, 0, 0]) === nothing && nnz(dst) == 1 + @test_throws InexactError copyto!(dst, sparse([1, 2], [1, 2], [0.5, 1.0])) + @test mismatch(dst, [1, 0, 0, 0, 0]) === nothing && nnz(dst) == 1 @test_throws InexactError copy!(dst, [1.0, 2.5, 0.0, 0.0, 0.0]) @test mismatch(dst, [1, 0, 0, 0, 0]) === nothing && nnz(dst) == 1 @test mismatch(copy!(dst, sparsevec([2, 4], [3.0, 4.0], 5)), [0, 3, 0, 4, 0]) === nothing @@ -1428,6 +1438,12 @@ end SparseArrays.fkeep!(f_drop, xdrop) @test exact_equal(xdrop, SparseVector(7, [1, 3, 4, 7], [3., -1., 1., 3.])) @test_deprecated SparseArrays.fkeep!(xdrop, f_drop) + @static if COMPREHENSIVE + # the entries visited before the throw are filtered and the others are kept + x = sparsevec([1, 2, 3], [-1.0, 1.0, 2.0]) + @test_throws ErrorException SparseArrays.fkeep!((i, v) -> i == 3 ? error("stop") : v > 0, x) + @test mismatch(x, [0.0, 1.0, 2.0]) === nothing && nnz(x) == 2 + end end @static if COMPREHENSIVE @@ -1789,9 +1805,9 @@ end @test ((1 + im) * v)::T == (1 + im) * d @test copy(v)::T == d end - # the full-column and full-vector views scale in place through the parent's storage - # (`view(x, :)` is a `SparseVectorView` only when the parent's axes are `Int`-sized) - for v in (view(A, :, 1), (Ti === Int ? (view(x, :),) : ())...), a in (1.0 + im,) + # the full-column and full-vector views scale in place through the parent's storage, + # also when the parent's axes are not `Int` + for v in (view(A, :, 1), view(x, :), view(SparseVector{ComplexF64,Int128}(x), :)), a in (1.0 + im,) @test which(*, (typeof(v), typeof(a))).module === SparseArrays @test which(*, (typeof(a), typeof(v))).module === SparseArrays @test which(/, (typeof(v), typeof(a))).module === SparseArrays @@ -1815,6 +1831,16 @@ end @test @allocated(f(v)) < 10^4 end end + # `count` calls `f` on the stored entries and on one zero + calls = Ref(0) + @test count(z -> (calls[] += 1; !iszero(z)), view(SparseVector{ComplexF64,Int32}(y), :)) == 2 && calls[] == 3 + # a view converts with the parent's index type and stored zeros + z = SparseVector{ComplexF64,Int32}(x) + C = SparseMatrixCSC{ComplexF64,Int32}(A) + for (v, u) in ((view(z, :), z), (view(C, :, 1), C[:, 1]), (view(z, 2:6), z[2:6])), + f in (SparseVector, sparse, w -> vcat(w, w), w -> kron(w, z), w -> kron(w, C), w -> kron(w, w')) + @test exact_equal(f(v), f(u)) + end end end diff --git a/test/triangular.jl b/test/triangular.jl index 7a93b7c4..de29ab7c 100644 --- a/test/triangular.jl +++ b/test/triangular.jl @@ -730,6 +730,10 @@ end b = SparseVector(3, [1, 3], Any[1.0, 2.0]) x = ldiv!(L, copy(b)) @test nonzeroinds(x) == 1:3 && x ≈ L \ [1.0, 0.0, 2.0] + # a solution that does not convert to the eltype of `b` throws before `b` is densified + b = sparsevec([2], [1], 2) + @test_throws InexactError ldiv!(UpperTriangular([2.0 1; 0 2]), b) + @test nonzeroinds(b) == [2] && nonzeros(b) == [1] end @testset "scalar promotion" for eltypemat in eltypes diff --git a/test/trim/src/SparseTrim.jl b/test/trim/src/SparseTrim.jl index 7d305668..18dd884f 100644 --- a/test/trim/src/SparseTrim.jl +++ b/test/trim/src/SparseTrim.jl @@ -91,6 +91,11 @@ function indexing() B[:, 2] .= 1.0 bad += check(B[:, 2] == [1.0, 1.0, 1.0]) bad += check(Array(view(A, :, 3)) == [0.0, 0.0, 5.0]) + bad += check(A[3] == 4.0 && A[[3, 5]] == sparsevec([1, 2], [4.0, 3.0], 2) && A[2:3] == sparsevec([2], [4.0], 2)) + C = copy(A) + C[2] = 1.0 + C[[4]] = [7.0] + bad += check(C[2, 1] == 1.0 && C[1, 2] == 7.0 && nnz(C) == 6) return bad end @@ -113,6 +118,8 @@ function algebra() bad += check(A - B == sparse([1, 2, 3, 3, 1], [1, 2, 1, 3, 3], [2.0, 3.0, 5.0, 5.0, -1.0], 3, 3)) bad += check(A * B == sparse([1, 3, 3], [3, 1, 3], [2.0, -5.0, 4.0], 3, 3)) bad += check(Matrix(A * D) == [4.0 0.0 0.0; 0.0 9.0 0.0; 28.0 0.0 25.0]) + bad += check(mapreduce(*, +, A, A) == 54.0 && prod(x -> x + 1.0, A) == 360.0) + bad += check(mapreduce(identity, (a, b) -> a + b, A) == 14.0) bad += check(sum(A) == 14.0 && sum(A; dims = 1) == [6.0 3.0 5.0]) bad += check(sum(A; dims = 2) == reshape([2.0, 3.0, 9.0], 3, 1)) bad += check(maximum(A) == 5.0 && minimum(B) == -1.0 && prod(A) == 0.0)