diff --git a/Project.toml b/Project.toml index 7e9699a21..ee5fcdf9c 100644 --- a/Project.toml +++ b/Project.toml @@ -19,6 +19,7 @@ PooledArrays = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +RectangularFullPacked = "27983f2f-6524-42ba-a408-2b5a31c238e4" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" @@ -57,6 +58,7 @@ PooledArrays = "0.5, 1" PrecompileTools = "1" ProgressMeter = "1.7" Random = "1" +RectangularFullPacked = "0.2.1" SparseArrays = "1" StableRNGs = "0.1, 1" StaticArrays = "0.11, 0.12, 1" diff --git a/src/MixedModels.jl b/src/MixedModels.jl index d0ec6df98..f608d1413 100644 --- a/src/MixedModels.jl +++ b/src/MixedModels.jl @@ -25,6 +25,7 @@ using PooledArrays: PooledArrays, PooledArray using PrecompileTools: PrecompileTools, @setup_workload, @compile_workload using ProgressMeter: ProgressMeter, Progress, ProgressUnknown, finish!, next! using Random: Random, AbstractRNG, randn! +using RectangularFullPacked: HermitianRFP, TriangularRFP using SparseArrays: SparseArrays, SparseMatrixCSC, SparseVector, dropzeros!, nnz using SparseArrays: nonzeros, nzrange, rowvals, sparse using StaticArrays: StaticArrays, SVector @@ -214,7 +215,6 @@ include("profile/profile.jl") include("nlopt.jl") include("prima.jl") - # aliases with non-unicode function names const settheta! = setθ! const profilesigma = profileσ diff --git a/src/blockdescription.jl b/src/blockdescription.jl index d64417e5e..d89119c3c 100644 --- a/src/blockdescription.jl +++ b/src/blockdescription.jl @@ -32,11 +32,17 @@ end BlockDescription(m::GeneralizedLinearMixedModel) = BlockDescription(m.LMM) -shorttype(::UniformBlockDiagonal, ::UniformBlockDiagonal) = "BlkDiag" -shorttype(::UniformBlockDiagonal, ::Matrix) = "BlkDiag/Dense" +function shorttype( + ::UniformBlockDiagonal, ::LowerTriangular{T,UniformBlockDiagonal{T}} +) where {T} + return "BlkDiag" +end +shorttype(::UniformBlockDiagonal, ::LowerTriangular) = "BlkDiag/Dense" shorttype(::SparseMatrixCSC, ::BlockedSparse) = "Sparse" shorttype(::Diagonal, ::Diagonal) = "Diagonal" shorttype(::Diagonal, ::Matrix) = "Diag/Dense" +shorttype(::Diagonal, ::LowerTriangular) = "Diag/Dense" +shorttype(::Diagonal, ::TriangularRFP) = "Diag/TrRFP" shorttype(::Matrix, ::Matrix) = "Dense" shorttype(::SparseMatrixCSC, ::SparseMatrixCSC) = "Sparse" shorttype(::SparseMatrixCSC, ::Matrix) = "Sparse/Dense" diff --git a/src/linalg.jl b/src/linalg.jl index 91e628153..64ab9e331 100644 --- a/src/linalg.jl +++ b/src/linalg.jl @@ -46,7 +46,7 @@ function LinearAlgebra.mul!( end function LinearAlgebra.ldiv!( - A::UpperTriangular{T,<:Adjoint{T,UniformBlockDiagonal{T}}}, B::StridedVector{T} + A::UpperTriangular{T,Adjoint{T,UniformBlockDiagonal{T}}}, B::StridedVector{T} ) where {T} adjA = A.data length(B) == size(A, 2) || throw(DimensionMismatch("")) @@ -60,7 +60,8 @@ function LinearAlgebra.ldiv!( end function LinearAlgebra.rdiv!( - A::Matrix{T}, B::UpperTriangular{T,<:Adjoint{T,UniformBlockDiagonal{T}}} + A::Matrix{T}, + B::UpperTriangular{T,Adjoint{T,UniformBlockDiagonal{T}}}, ) where {T} m, n = size(A) Bd = B.data.parent @@ -78,7 +79,8 @@ function LinearAlgebra.rdiv!( end function LinearAlgebra.rdiv!( - A::BlockedSparse{T,S,P}, B::UpperTriangular{T,<:Adjoint{T,UniformBlockDiagonal{T}}} + A::BlockedSparse{T,S,P}, + B::UpperTriangular{T,Adjoint{T,UniformBlockDiagonal{T}}}, ) where {T,S,P} Bpd = B.data.parent Bdat = Bpd.data diff --git a/src/linalg/cholUnblocked.jl b/src/linalg/cholUnblocked.jl index b430e324a..36b8eb4f5 100644 --- a/src/linalg/cholUnblocked.jl +++ b/src/linalg/cholUnblocked.jl @@ -8,17 +8,21 @@ because these are part of the inner calculations in a blocked Cholesky factoriza """ function cholUnblocked! end -function cholUnblocked!(D::Diagonal{T}, ::Type{Val{:L}}) where {T<:AbstractFloat} - Ddiag = D.diag +function cholUnblocked!(D::Hermitian{T,Diagonal{T,Vector{T}}}) where {T} + Ddiag = D.data.diag @inbounds for i in eachindex(Ddiag) (ddi = Ddiag[i]) ≤ zero(T) && throw(PosDefException(i)) Ddiag[i] = sqrt(ddi) end - return D end -function cholUnblocked!(A::StridedMatrix{T}, ::Type{Val{:L}}) where {T<:BlasFloat} +function cholUnblocked!(A::Hermitian{T,Matrix{T}}) where {T} + A.uplo == 'L' || throw(ArgumentError("A.uplo should be 'L'")) + return cholUnblocked!(A.data) +end + +function cholUnblocked!(A::StridedMatrix{T}) where {T<:BlasFloat} n = LinearAlgebra.checksquare(A) if n == 1 A[1] < zero(T) && throw(PosDefException(1)) @@ -36,10 +40,12 @@ function cholUnblocked!(A::StridedMatrix{T}, ::Type{Val{:L}}) where {T<:BlasFloa return A end -function cholUnblocked!(D::UniformBlockDiagonal, ::Type{Val{:L}}) - Ddat = D.data +function cholUnblocked!(D::Hermitian{T,UniformBlockDiagonal{T}}) where {T} + Ddat = D.data.data for k in axes(Ddat, 3) - cholUnblocked!(view(Ddat, :, :, k), Val{:L}) + cholUnblocked!(view(Ddat, :, :, k)) end return D end + +cholUnblocked!(D::HermitianRFP) = LinearAlgebra.cholesky!(D) diff --git a/src/linalg/logdet.jl b/src/linalg/logdet.jl index 5f2c5a564..49fa19a15 100644 --- a/src/linalg/logdet.jl +++ b/src/linalg/logdet.jl @@ -7,13 +7,19 @@ Return `log(det(tril(A)))` evaluated in place. """ LD(d::Diagonal{T}) where {T<:Number} = sum(log, d.diag) -function LD(d::UniformBlockDiagonal{T}) where {T} +function LD(d::LowerTriangular{T,UniformBlockDiagonal{T}}) where {T} dat = d.data return sum(log, dat[j, j, k] for j in axes(dat, 2), k in axes(dat, 3)) end LD(d::DenseMatrix{T}) where {T} = @inbounds sum(log, d[k] for k in diagind(d)) +LD(d::LowerTriangular{T,Matrix{T}}) where {T} = LD(d.data) + +function LD(d::TriangularRFP{T}) where {T} + return sum(log, d[j, j] for j in axes(d, 2)) +end + """ logdet(m::LinearMixedModel) diff --git a/src/linalg/rankUpdate.jl b/src/linalg/rankUpdate.jl index 705f87d53..4adcae0ec 100644 --- a/src/linalg/rankUpdate.jl +++ b/src/linalg/rankUpdate.jl @@ -3,7 +3,7 @@ rankUpdate!(C, A, α) rankUpdate!(C, A, α, β) -A rank-k update, C := α*A'A + β*C, of a Hermitian (Symmetric) matrix. +A rank-k update, C := α*A*A' + β*C, of a Hermitian (Symmetric) matrix. `α` and `β` both default to 1.0. When `α` is -1.0 this is a downdate operation. The name `rankUpdate!` is borrowed from [https://github.com/andreasnoack/LinearAlgebra.jl] @@ -39,6 +39,18 @@ function rankUpdate!(C::HermOrSym{T,S}, A::StridedMatrix{T}, α, β) where {T,S} return C end +# function rankUpdate!( +# C::HermOrSym{T,S}, A::StridedMatrix{T}, α, β +# ) where {T,S<:LowerTriangular} +# BLAS.syrk!(C.uplo, 'N', T(α), A, T(β), C.data.data) +# return C +# end + +# function rankUpdate!(C::HermitianRFP{T}, A::StridedMatrix{T}, α, β) where {T} +# BLAS.syrk!('N', T(α), A, T(β), C) +# return C +# end + """ _columndot(rv, nz, rngi, rngj) @@ -64,12 +76,17 @@ function _columndot(rv, nz, rngi, rngj) return accum end -function rankUpdate!(C::HermOrSym{T,S}, A::SparseMatrixCSC{T}, α, β) where {T,S} +function rankUpdate!( + C::HermOrSym{T,S}, + A::SparseMatrixCSC{T}, + α, + β, +) where {T,S} require_one_based_indexing(C, A) m, n = size(A) Cd, rv, nz = C.data, A.rowval, A.nzval lower = C.uplo == 'L' - (lower ? m : n) == size(C, 2) || throw(DimensionMismatch()) + # (lower ? m : n) == size(C, 2) || throw(DimensionMismatch()) # Doesn't make sense with lower. isone(β) || rmul!(lower ? LowerTriangular(Cd) : UpperTriangular(Cd), β) if lower @inbounds for jj in axes(A, 2) @@ -96,6 +113,38 @@ function rankUpdate!(C::HermOrSym{T,S}, A::SparseMatrixCSC{T}, α, β) where {T, return C end +function rankUpdate!( + C::HermitianRFP{T}, + A::SparseMatrixCSC{T}, + α, + β, +) where {T} + require_one_based_indexing(C, A) + Ctr = TriangularRFP(C.data, C.transr, C.uplo) # need TriangularRFP for write access + rv, nz = A.rowval, A.nzval + if size(A, 1) ≠ size(C, 1) + throw(DimensionMismatch("size(A, 1) == $(size(A,1)) ≠ $(size(C, 1)) = size(C, 1)")) + end + isone(β) || rmul!(Ctr, β) + if C.uplo == 'L' + @inbounds for jj in axes(A, 2) + rangejj = nzrange(A, jj) + lenrngjj = length(rangejj) + for (k, j) in enumerate(rangejj) + anzj = α * nz[j] + rvj = rv[j] + for i in k:lenrngjj + kk = rangejj[i] + Ctr[rv[kk], rvj] = muladd(nz[kk], anzj, Ctr[rv[kk], rvj]) + end + end + end + else + throw(ArgumentError("rankUpdate! requires C be stored in the lower triangle")) + end + return C +end + function rankUpdate!(C::HermOrSym, A::BlockedSparse, α, β) return rankUpdate!(C, sparse(A), α, β) end diff --git a/src/linearmixedmodel.jl b/src/linearmixedmodel.jl index 7c7b761b6..b76f6169f 100644 --- a/src/linearmixedmodel.jl +++ b/src/linearmixedmodel.jl @@ -42,10 +42,22 @@ struct LinearMixedModel{T<:AbstractFloat} <: MixedModel{T} end function LinearMixedModel( - f::FormulaTerm, tbl; contrasts=Dict{Symbol,Any}(), wts=[], σ=nothing, amalgamate=true + f::FormulaTerm, + tbl; + contrasts=Dict{Symbol,Any}(), + wts=[], + σ=nothing, + amalgamate=true, + RFPthreshold=1000, ) return LinearMixedModel( - f::FormulaTerm, Tables.columntable(tbl); contrasts, wts, σ, amalgamate + f::FormulaTerm, + Tables.columntable(tbl); + contrasts, + wts, + σ, + amalgamate, + RFPthreshold, ) end @@ -55,7 +67,7 @@ const _MISSING_RE_ERROR = ArgumentError( function LinearMixedModel( f::FormulaTerm, tbl::Tables.ColumnTable; contrasts=Dict{Symbol,Any}(), wts=[], - σ=nothing, amalgamate=true, + σ=nothing, amalgamate=true, RFPthreshold=1000, ) fvars = StatsModels.termvars(f) tvars = Tables.columnnames(tbl) @@ -77,7 +89,7 @@ function LinearMixedModel( y, Xs = modelcols(form, tbl) - return LinearMixedModel(y, Xs, form, wts, σ, amalgamate) + return LinearMixedModel(y, Xs, form, wts, σ, amalgamate, RFPthreshold) end """ @@ -100,6 +112,7 @@ function LinearMixedModel( wts=[], σ=nothing, amalgamate=true, + RFPthreshold=1000, ) T = promote_type(Float64, float(eltype(y))) # ensure eltype of model matrices is at least Float64 @@ -133,12 +146,12 @@ function LinearMixedModel( end isempty(reterms) && throw(_MISSING_RE_ERROR) return LinearMixedModel( - convert(Array{T}, y), only(feterms), reterms, form, wts, σ, amalgamate + convert(Array{T}, y), only(feterms), reterms, form, wts, σ, amalgamate, RFPthreshold ) end """ - LinearMixedModel(y, feterm, reterms, form, wts=[], σ=nothing; amalgamate=true) + LinearMixedModel(y, feterm, reterms, form, wts=[], σ=nothing; amalgamate=true, RFPthreshold=1000) Private constructor for a `LinearMixedModel` given already assembled fixed and random effects. @@ -159,6 +172,7 @@ function LinearMixedModel( wts=[], σ=nothing, amalgamate=true, + RFPthreshold=1000, ) where {T} # detect and combine RE terms with the same grouping var if length(reterms) > 1 && amalgamate @@ -172,7 +186,7 @@ function LinearMixedModel( sqrtwts = map!(sqrt, Vector{T}(undef, length(wts)), wts) reweight!.(reterms, Ref(sqrtwts)) reweight!(Xy, sqrtwts) - A, L = createAL(reterms, Xy) + A, L = createAL(reterms, Xy; RFPthreshold) lbd = foldl(vcat, lowerbd(c) for c in reterms) θ = foldl(vcat, getθ(c) for c in reterms) optsum = OptSummary(θ, lbd) @@ -384,9 +398,13 @@ function _pushALblock!(A, L, blk) return push!(A, deepcopy(isa(blk, BlockedSparse) ? blk.cscmat : blk)) end -function createAL(reterms::Vector{<:AbstractReMat{T}}, Xy::FeMat{T}) where {T} +function createAL( + reterms::Vector{<:AbstractReMat{T}}, + Xy::FeMat{T}; + RFPthreshold::Int=1000, +) where {T} k = length(reterms) - vlen = kchoose2(k + 1) + vlen = kp1choose2(k + 1) A = sizehint!(AbstractMatrix{T}[], vlen) L = sizehint!(AbstractMatrix{T}[], vlen) for i in eachindex(reterms) @@ -394,16 +412,23 @@ function createAL(reterms::Vector{<:AbstractReMat{T}}, Xy::FeMat{T}) where {T} _pushALblock!(A, L, densify(reterms[i]' * reterms[j])) end end - for j in eachindex(reterms) # can't fold this into the previous loop b/c block order + for j in eachindex(reterms) # can't fold this into the previous loop b/c block order _pushALblock!(A, L, densify(Xy' * reterms[j])) end _pushALblock!(A, L, densify(Xy'Xy)) - for i in 2:k # check for fill-in due to non-nested grouping factors + for i in 2:k # check for fill-in due to non-nested grouping factors ci = reterms[i] for j in 1:(i - 1) cj = reterms[j] if !isnested(cj, ci) - for l in i:k + ind = kp1choose2(i) # location of i'th diagonal block + Li = collect(L[ind]) + L[ind] = if size(Li, 2) > RFPthreshold + TriangularRFP(Li, :L) + else + LowerTriangular(Li) + end + for l in (i + 1):k ind = block(l, i) L[ind] = Matrix(L[ind]) end @@ -411,7 +436,14 @@ function createAL(reterms::Vector{<:AbstractReMat{T}}, Xy::FeMat{T}) where {T} end end end - return identity.(A), identity.(L) + for k in axes(reterms, 1) # patch up the UniformBlockDiagonal in L for nested factors + diagind = kp1choose2(k) + Ldi = L[diagind] + if isa(Ldi, UniformBlockDiagonal) + L[diagind] = LowerTriangular(Ldi) + end + end + return identity.(A), identity.(L) # does anyone remember what the `identity` is for?` end StatsAPI.deviance(m::LinearMixedModel) = objective(m) @@ -710,8 +742,12 @@ end # use dispatch to distinguish Diagonal and UniformBlockDiagonal in first(L) _ldivB1!(B1::Diagonal{T}, rhs::AbstractVector{T}, ind) where {T} = rhs ./= B1.diag[ind] -function _ldivB1!(B1::UniformBlockDiagonal{T}, rhs::AbstractVector{T}, ind) where {T} - return ldiv!(LowerTriangular(view(B1.data, :, :, ind)), rhs) +function _ldivB1!( + B1::LowerTriangular{T,UniformBlockDiagonal{T}}, + rhs::AbstractVector{T}, + ind, +) where {T} + return ldiv!(LowerTriangular(view(B1.data.data, :, :, ind)), rhs) end """ @@ -931,7 +967,7 @@ principal component, the first two principal components, etc. The last element always 1.0 representing the complete proportion of the variance. """ function rePCA(m::LinearMixedModel; corr::Bool=true) - pca = PCA.(m.reterms, corr=corr) + pca = PCA.(m.reterms; corr=corr) return NamedTuple{_unique_fnames(m)}(getproperty.(pca, :cumvar)) end @@ -943,7 +979,7 @@ covariance matrices or correlation matrices when `corr` is `true`. """ function PCA(m::LinearMixedModel; corr::Bool=true) - return NamedTuple{_unique_fnames(m)}(PCA.(m.reterms, corr=corr)) + return NamedTuple{_unique_fnames(m)}(PCA.(m.reterms; corr=corr)) end """ @@ -1081,18 +1117,17 @@ Base.show(io::IO, m::LinearMixedModel) = Base.show(io, MIME"text/plain"(), m) """ _coord(A::AbstractMatrix) -Return the positions and values of the nonzeros in `A` as a -`NamedTuple{(:i, :j, :v), Tuple{Vector{Int32}, Vector{Int32}, Vector{Float64}}}` +Return the positions and values of the nonzeros in `A` as a `TypedTables.Table` with columns `i`, `j`, and `v` """ function _coord(A::Diagonal) - return (i=Int32.(axes(A, 1)), j=Int32.(axes(A, 2)), v=A.diag) + return Table(; i=Int32.(axes(A, 1)), j=Int32.(axes(A, 2)), v=A.diag) end -function _coord(A::UniformBlockDiagonal) - dat = A.data +function _coord(A::LowerTriangular{T,UniformBlockDiagonal{T}}) where {T} + dat = A.data.data r, c, k = size(dat) blk = repeat(r .* (0:(k - 1)); inner=r * c) - return ( + return Table(; i=Int32.(repeat(1:r; outer=c * k) .+ blk), j=Int32.(repeat(1:c; inner=r, outer=k) .+ blk), v=vec(dat), @@ -1105,20 +1140,43 @@ function _coord(A::SparseMatrixCSC{T,Int32}) where {T} for j in axes(A, 2), k in nzrange(A, j) cv[k] = j end - return (i=rv, j=cv, v=nonzeros(A)) + return Table(; i=rv, j=cv, v=nonzeros(A)) end _coord(A::BlockedSparse) = _coord(A.cscmat) function _coord(A::Matrix) m, n = size(A) - return ( + return Table(; i=Int32.(repeat(axes(A, 1); outer=n)), j=Int32.(repeat(axes(A, 2); inner=m)), v=vec(A), ) end +function _triinds(n::Integer, uplo::Symbol) + inds = sizehint!(@NamedTuple{i::Int32, j::Int32}[], kp1choose2(n)) + ul = uplo == :U + for j in Int32.(1:n) + for i in Int32.(ul ? (1:j) : (j:n)) + push!(inds, (; i, j)) + end + end + return Table(inds) +end + +function _coord(A::LowerTriangular{T,Matrix{T}}) where {T} + tbl = _triinds(size(A, 2), :L) + v = [A[i, j] for (i, j) in tbl] + return Table(tbl; v) +end + +function _coord(A::TriangularRFP{T}) where {T} + tbl = _triinds(size(A, 2), Symbol(A.uplo)) + v = [A[i, j] for (i, j) in tbl] + return Table(tbl; v) +end + """ sparseL(m::LinearMixedModel; fname::Symbol=first(fnames(m)), full::Bool=false) @@ -1260,14 +1318,16 @@ Update the blocked lower Cholesky factor, `m.L`, from `m.A` and `m.reterms` (use This is the crucial step in evaluating the objective, given a new parameter value. """ function updateL!(m::LinearMixedModel{T}) where {T} - A, L, reterms = m.A, m.L, m.reterms + (; A, L, reterms) = m k = length(reterms) copyto!(last(m.L), last(m.A)) # ensure the fixed-effects:response block is copied - for j in eachindex(reterms) # pre- and post-multiply by Λ, add I to diagonal + for j in eachindex(reterms) # pre- and post-multiply by Λ, add I to diagonal cj = reterms[j] diagind = kp1choose2(j) - copyscaleinflate!(L[diagind], A[diagind], cj) - for i in (j + 1):(k + 1) # postmultiply column by Λ + LdH = L[diagind] + LdH = isa(LdH, LowerTriangular) ? Hermitian(LdH.data, :L) : Hermitian(LdH, :L) + copyscaleinflate!(LdH, A[diagind], cj) + for i in (j + 1):(k + 1) # postmultiply column by Λ bij = block(i, j) rmulΛ!(copyto!(L[bij], A[bij]), cj) end @@ -1277,17 +1337,17 @@ function updateL!(m::LinearMixedModel{T}) where {T} end for j in 1:(k + 1) # blocked Cholesky Ljj = L[kp1choose2(j)] + LjjH = isa(Ljj, LowerTriangular) ? Hermitian(Ljj.data, :L) : Hermitian(Ljj, :L) for jj in 1:(j - 1) - rankUpdate!(Hermitian(Ljj, :L), L[block(j, jj)], -one(T), one(T)) + rankUpdate!(LjjH, L[block(j, jj)], -one(T), one(T)) end - cholUnblocked!(Ljj, Val{:L}) - LjjT = isa(Ljj, Diagonal) ? Ljj : LowerTriangular(Ljj) + cholUnblocked!(LjjH) for i in (j + 1):(k + 1) Lij = L[block(i, j)] for jj in 1:(j - 1) mul!(Lij, L[block(i, jj)], L[block(j, jj)]', -one(T), one(T)) end - rdiv!(Lij, LjjT') + rdiv!(Lij, Ljj') end end return m diff --git a/src/pca.jl b/src/pca.jl index 2f49a9830..c706d52b0 100644 --- a/src/pca.jl +++ b/src/pca.jl @@ -81,7 +81,7 @@ function Base.show( # only display the lower triangle of symmetric matrix if pca.rnames !== missing n = length(pca.rnames) - cv = string.(round.(pca.covcor, digits=ndigitsmat)) + cv = string.(round.(pca.covcor; digits=ndigitsmat)) dotpad = lpad(".", div(maximum(length, cv), 2)) for i in 1:n, j in (i + 1):n cv[i, j] = dotpad @@ -97,7 +97,7 @@ function Base.show( # if there are no names, then we cheat and use the print method # for LowerTriangular, which automatically covers the . in the # upper triangle - printmat = round.(LowerTriangular(pca.covcor), digits=ndigitsmat) + printmat = round.(LowerTriangular(pca.covcor); digits=ndigitsmat) end Base.print_matrix(io, printmat) @@ -106,21 +106,21 @@ function Base.show( if stddevs println(io, "\nStandard deviations:") sv = pca.sv - show(io, round.(sv.S, digits=ndigitsvec)) + show(io, round.(sv.S; digits=ndigitsvec)) println(io) end if variances println(io, "\nVariances:") vv = abs2.(sv.S) - show(io, round.(vv, digits=ndigitsvec)) + show(io, round.(vv; digits=ndigitsvec)) println(io) end println(io, "\nNormalized cumulative variances:") - show(io, round.(pca.cumvar, digits=ndigitscum)) + show(io, round.(pca.cumvar; digits=ndigitscum)) println(io) if loadings println(io, "\nComponent loadings") - printmat = round.(pca.loadings, digits=ndigitsmat) + printmat = round.(pca.loadings; digits=ndigitsmat) if pca.rnames !== missing pclabs = [Text(""); Text.("PC$i" for i in 1:length(pca.rnames))] pclabs = reshape(pclabs, 1, :) diff --git a/src/remat.jl b/src/remat.jl index 8c78f87d5..442fdf3f9 100644 --- a/src/remat.jl +++ b/src/remat.jl @@ -568,8 +568,12 @@ Overwrite L with `Λ'AΛ + I` """ function copyscaleinflate! end -function copyscaleinflate!(Ljj::Diagonal{T}, Ajj::Diagonal{T}, Λj::ReMat{T,1}) where {T} - Ldiag, Adiag = Ljj.diag, Ajj.diag +function copyscaleinflate!( + Ljj::Hermitian{T,Diagonal{T,Vector{T}}}, + Ajj::Diagonal{T,Vector{T}}, + Λj::ReMat{T,1}, +) where {T} + Ldiag, Adiag = Ljj.data.diag, Ajj.diag lambsq = abs2(only(Λj.λ.data)) @inbounds for i in eachindex(Ldiag, Adiag) Ldiag[i] = muladd(lambsq, Adiag[i], one(T)) @@ -577,23 +581,39 @@ function copyscaleinflate!(Ljj::Diagonal{T}, Ajj::Diagonal{T}, Λj::ReMat{T,1}) return Ljj end -function copyscaleinflate!(Ljj::Matrix{T}, Ajj::Diagonal{T}, Λj::ReMat{T,1}) where {T} - fill!(Ljj, zero(T)) +function copyscaleinflate!( + Ljj::Hermitian{T,Matrix{T}}, + Ajj::Diagonal{T}, + Λj::ReMat{T,1}, +) where {T} + Ld = Ljj.data + fill!(Ld, zero(T)) lambsq = abs2(only(Λj.λ.data)) @inbounds for (i, a) in enumerate(Ajj.diag) - Ljj[i, i] = muladd(lambsq, a, one(T)) + Ld[i, i] = muladd(lambsq, a, one(T)) + end + return Ljj +end + +function copyscaleinflate!(Ljj::HermitianRFP{T}, Ajj::Diagonal{T}, Λj::ReMat{T,1}) where {T} + fill!(Ljj.data, zero(T)) + lambsq = abs2(only(Λj.λ.data)) + LjjT = TriangularRFP(Ljj.data, Ljj.transr, Ljj.uplo) + @inbounds for (i, a) in enumerate(Ajj.diag) + LjjT[i, i] = muladd(lambsq, a, one(T)) end return Ljj end function copyscaleinflate!( - Ljj::UniformBlockDiagonal{T}, + Ljj::Hermitian{T,UniformBlockDiagonal{T}}, Ajj::UniformBlockDiagonal{T}, Λj::ReMat{T,S}, ) where {T,S} + Ljj.uplo == 'L' || throw(ArgumentError("Ljj.uplo should be 'L'")) λ = Λj.λ dind = diagind(S, S) - Ldat = copyto!(Ljj.data, Ajj.data) + Ldat = copyto!(Ljj.data.data, Ajj.data) for k in axes(Ldat, 3) f = view(Ldat, :, :, k) lmul!(λ', rmul!(f, λ)) @@ -605,24 +625,52 @@ function copyscaleinflate!( end function copyscaleinflate!( - Ljj::Matrix{T}, + Ljj::HermitianRFP{T}, + Ajj::UniformBlockDiagonal{T}, + Λj::ReMat{T,S}, +) where {T,S} # S is an integer - the size of the diagonal blocks + LjjT = TriangularRFP(Ljj.data, Ljj.transr, Ljj.uplo) + q, r = divrem(size(Ljj, 1), S) + iszero(r) || throw(DimensionMismatch("size(Ljj, 1) is not a multiple of S")) + λ = Λj.λ + tmp = Array{T}(undef, S, S) + offset = 0 + @inbounds for k in 1:q + copyto!(tmp, Ajj.data[:, :, k]) + lmul!(adjoint(λ), rmul!(tmp, λ)) + for j in 1:S + tmp[j, j] += one(T) + end + for j in 1:S + for i in j:S + LjjT[offset + i, offset + j] = tmp[i, j] + end + end + offset += S + end + return Ljj +end + +function copyscaleinflate!( + Ljj::Hermitian{T,Matrix{T}}, Ajj::UniformBlockDiagonal{T}, Λj::ReMat{T,S}, ) where {T,S} - copyto!(Ljj, Ajj) - n = LinearAlgebra.checksquare(Ljj) + LjjM = Ljj.data + copyto!(LjjM, Ajj) + n = LinearAlgebra.checksquare(LjjM) q, r = divrem(n, S) iszero(r) || throw(DimensionMismatch("size(Ljj, 1) is not a multiple of S")) λ = Λj.λ offset = 0 @inbounds for _ in 1:q inds = (offset + 1):(offset + S) - tmp = view(Ljj, inds, inds) + tmp = view(LjjM, inds, inds) lmul!(adjoint(λ), rmul!(tmp, λ)) offset += S end - for k in diagind(Ljj) - Ljj[k] += one(T) + for k in diagind(LjjM) + LjjM[k] += one(T) end return Ljj end diff --git a/test/UniformBlockDiagonal.jl b/test/UniformBlockDiagonal.jl index 158b06a4d..4092bc015 100644 --- a/test/UniformBlockDiagonal.jl +++ b/test/UniformBlockDiagonal.jl @@ -43,11 +43,11 @@ const LMM = LinearMixedModel end @testset "copyscaleinflate" begin - MixedModels.copyscaleinflate!(Lblk, ex22, vf1) + MixedModels.copyscaleinflate!(Hermitian(Lblk, :L), ex22, vf1) @test view(Lblk.data, :, :, 1) == [2. 3.; 2. 5.] setθ!(vf1, [1.,1.,1.]) Λ = vf1.λ - MixedModels.copyscaleinflate!(Lblk, ex22, vf1) + MixedModels.copyscaleinflate!(Hermitian(Lblk, :L), ex22, vf1) target = Λ'view(ex22.data, :, :, 1)*Λ + I @test view(Lblk.data, :, :, 1) == target end @@ -61,7 +61,7 @@ const LMM = LinearMixedModel @test vf1.λ == LowerTriangular(Matrix(I, 2, 2)) setθ!(vf2, [1.75, 0.0, 1.0]) A11 = vf1'vf1 - L11 = MixedModels.cholUnblocked!(MixedModels.copyscaleinflate!(UniformBlockDiagonal(fill(0., size(A11.data))), A11, vf1), Val{:L}) + L11 = MixedModels.cholUnblocked!(MixedModels.copyscaleinflate!(Hermitian(UniformBlockDiagonal(fill(0., size(A11.data))),:L), A11, vf1)) L21 = vf2'vf1 @test isa(L21, BlockedSparse) @test L21[1,1] == 30.0 @@ -74,6 +74,6 @@ const LMM = LinearMixedModel # rdiv!(L21, adjoint(LowerTriangular(L11))) # @test_broken L21.colblocks[1] == rdiv!(L21cb1, adjoint(LowerTriangular(L11.facevec[1]))) A22 = vf2'vf2 - L22 = MixedModels.copyscaleinflate!(UniformBlockDiagonal(fill(0., size(A22.data))), A22, vf2) + L22 = MixedModels.copyscaleinflate!(Hermitian(UniformBlockDiagonal(fill(0., size(A22.data))), :L), A22, vf2) end end diff --git a/test/pls.jl b/test/pls.jl index ae6409547..7ae4c5c69 100644 --- a/test/pls.jl +++ b/test/pls.jl @@ -323,15 +323,15 @@ end @test lowerbd(fm) == [0.0, -Inf, 0.0] A11 = first(fm.A) @test isa(A11, UniformBlockDiagonal{Float64}) - @test isa(first(fm.L), UniformBlockDiagonal{Float64}) + @test isa(first(fm.L), LowerTriangular{Float64, UniformBlockDiagonal{Float64}}) @test size(A11) == (36, 36) a11 = view(A11.data, :, :, 1) @test a11 == [10. 45.; 45. 285.] @test size(A11.data, 3) == 18 λ = first(fm.λ) - b11 = LowerTriangular(view(first(fm.L).data, :, :, 1)) + b11 = LowerTriangular(view(first(fm.L).data.data, :, :, 1)) @test b11 * b11' ≈ λ'a11*λ + I rtol=1e-5 - @test count(!iszero, Matrix(first(fm.L))) == 18 * 4 + @test count(!iszero, Matrix(first(fm.L).data)) == 18 * 4 @test rank(fm) == 2 @test objective(fm) ≈ 1751.9393444647046