Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0b23679
Preliminary (non-working) version with RFP.
dmbates Mar 22, 2025
03015a7
[WIP] allow TriangularRFP for diagonal blocks of L [ci skip]
dmbates Mar 28, 2025
4516cc3
Documentation correction per #813
dmbates Mar 30, 2025
c04c01d
Merge branch 'main' into db/RFP
dmbates Apr 1, 2025
ae1e292
format
palday Apr 2, 2025
a7c2f56
Merge branch 'main' into db/RFP
dmbates Apr 8, 2025
2dedb3e
Adjustments for tests; reformat
dmbates Apr 9, 2025
09ad439
Enhance coverage
dmbates Apr 9, 2025
06ffbb3
BlueStyle
palday Apr 10, 2025
f01f8f7
Generalize the rankUpdate! for `HermOrSym`
dmbates May 26, 2025
c71ce98
Use linear indexing in rankUpdate! method for HermitianRFP
dmbates Jul 17, 2026
ac540ad
Tweak tests.
dmbates Jul 17, 2026
8c49fb6
Merge branch 'main' of github.com:JuliaStats/MixedModels.jl into db/RFP
palday Jul 18, 2026
feb9c7f
fix forwarddiff extension
palday Jul 18, 2026
5992857
pass RFPthreshold through `fit`
palday Jul 18, 2026
ac494c4
bugfix: off-diagonal entries
palday Jul 18, 2026
870f924
missing method + tests
palday Jul 18, 2026
0b2172d
style
palday Jul 18, 2026
bab0c5a
remove unnecessary RNG specification
palday Jul 18, 2026
00f1210
Merge branch 'main' into db/RFP
dmbates Jul 19, 2026
6cf3f17
basic sorting of levels
palday Jul 19, 2026
945637f
fix predict
palday Jul 19, 2026
b2b195d
enable by default, fix nnz test
palday Jul 19, 2026
4cd5ae6
sync sorting of contrast matrix levels
palday Jul 19, 2026
a783717
optimization: do the sorting in place instead of allocating and copyi…
palday Jul 21, 2026
1f5089f
clearly internal
palday Jul 21, 2026
cb2d49e
note
palday Jul 21, 2026
26039ab
notes
palday Jul 21, 2026
80cf7a6
add a keyword elements to LMM constructor docstring
palday Jul 21, 2026
2c230e6
loosen tolerance on simulation comparison
palday Jul 21, 2026
ee1bf47
thinko
palday Jul 21, 2026
e1f4400
Merge pull request #906 from JuliaStats/pa/RFP-sorted
palday Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
RectangularFullPacked = "27983f2f-6524-42ba-a408-2b5a31c238e4"
RegressionFormulae = "545c379f-4ec2-4339-9aea-38f2fb6a8ba2"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
Expand Down Expand Up @@ -71,6 +72,7 @@ PrecompileTools = "1"
Printf = "1"
ProgressMeter = "1.7"
Random = "1"
RectangularFullPacked = "0.2.1"
RegressionFormulae = "0.1.3"
SparseArrays = "1"
StableRNGs = "0.1, 1"
Expand Down
41 changes: 18 additions & 23 deletions ext/MixedModelsForwardDiffExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ using MixedModels
using MixedModels: AbstractReMat,
block,
BlockedSparse,
cholUnblocked!,
copyscaleinflate!,
kp1choose2,
LD,
Expand Down Expand Up @@ -140,7 +139,9 @@ function MixedModels.fd_updateL!(A::Vector, L::Vector, reterms::Vector)
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)
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)
Expand All @@ -151,16 +152,11 @@ function MixedModels.fd_updateL!(A::Vector, L::Vector, reterms::Vector)
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)
fd_rankUpdate!(
Hermitian(Ljj, :L),
L[block(j, jj)],
-one(eltype(Ljj)),
one(eltype(Ljj)),
)
fd_rankUpdate!(LjjH, L[block(j, jj)], -one(eltype(Ljj)), one(eltype(Ljj)))
end
fd_cholUnblocked!(Ljj, Val{:L})
LjjT = isa(Ljj, Diagonal) ? Ljj : LowerTriangular(Ljj)
fd_cholUnblocked!(LjjH)
for i in (j + 1):(k + 1)
Lij = L[block(i, j)]
for jj in 1:(j - 1)
Expand All @@ -172,7 +168,7 @@ function MixedModels.fd_updateL!(A::Vector, L::Vector, reterms::Vector)
one(eltype(Lij)),
)
end
rdiv!(Lij, LjjT')
rdiv!(Lij, Ljj')
end
end
return nothing
Expand All @@ -194,26 +190,25 @@ end
##### Cholesky factorization
#####

function MixedModels.fd_cholUnblocked!(A::StridedMatrix, ::Type{Val{:L}})
cholesky!(Hermitian(A, :L))
function MixedModels.fd_cholUnblocked!(D::Hermitian{T,Diagonal{T,Vector{T}}}) where {T}
D.data.diag .= sqrt.(D.data.diag)
return D
end

function MixedModels.fd_cholUnblocked!(A::Hermitian{T,Matrix{T}}) where {T}
A.uplo == 'L' || throw(ArgumentError("A.uplo should be 'L'"))
cholesky!(A)
return A
end

function MixedModels.fd_cholUnblocked!(D::UniformBlockDiagonal, ::Type{T}) where {T}
Ddat = D.data
function MixedModels.fd_cholUnblocked!(D::Hermitian{T,UniformBlockDiagonal{T}}) where {T}
Ddat = D.data.data
for k in axes(Ddat, 3)
fd_cholUnblocked!(view(Ddat, :, :, k), T)
cholesky!(Hermitian(view(Ddat, :, :, k), :L))
end
return D
end

function MixedModels.fd_cholUnblocked!(A::Diagonal, ::Type{T}) where {T}
A.diag .= sqrt.(A.diag)
return A
end

MixedModels.fd_cholUnblocked!(A::AbstractMatrix, ::Type{T}) where {T} = cholUnblocked!(A, T)

#####
##### Rank update
#####
Expand Down
4 changes: 3 additions & 1 deletion src/MixedModels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ using PrecompileTools: PrecompileTools, @setup_workload, @compile_workload
using Printf: @sprintf
using ProgressMeter: ProgressMeter, Progress, finish!, next!
using Random: Random, AbstractRNG, randn!
using RectangularFullPacked: HermitianRFP, TriangularRFP
using RectangularFullPacked.LAPACK_RFP: sfrk!
using RegressionFormulae: fulldummy
using SparseArrays: SparseArrays, SparseMatrixCSC, SparseVector, dropzeros!, findnz
using SparseArrays: SparseArrays, SparseMatrixCSC, SparseVector, dropzeros!
using SparseArrays: nnz, nonzeros, nzrange, rowvals, sparse
using StaticArrays: StaticArrays, SVector
using Statistics: Statistics, mean, quantile, std
Expand Down
10 changes: 8 additions & 2 deletions src/blockdescription.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 5 additions & 3 deletions src/linalg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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(""))
Expand All @@ -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
Expand All @@ -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
Expand Down
20 changes: 13 additions & 7 deletions src/linalg/cholUnblocked.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
8 changes: 7 additions & 1 deletion src/linalg/logdet.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
89 changes: 84 additions & 5 deletions src/linalg/rankUpdate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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}
sfrk!(C.transr, C.uplo, 'N', T(α), A, T(β), C.data)
return C
end

"""
_columndot(rv, nz, rngi, rngj)

Expand All @@ -64,12 +76,24 @@ 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())
adim = lower ? 1 : 2
if size(C, 1) ≠ size(A, adim)
throw(
DimensionMismatch(
"size(C,1) = $(size(C, 1)) does not match size(A,$adim) = $(size(A, adim))"
),
)
end

Cd, rv, nz = C.data, A.rowval, A.nzval
isone(β) || rmul!(lower ? LowerTriangular(Cd) : UpperTriangular(Cd), β)
if lower
@inbounds for jj in axes(A, 2)
Expand All @@ -96,10 +120,65 @@ 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)
if uppercase(C.transr) ≠ 'N' || uppercase(C.uplo) ≠ 'L'
throw(ArgumentError("HermitianRFP C is not non-trans, lower"))
end
rv, nz = A.rowval, A.nzval
Cdat = C.data
An = size(A, 1)
if An ≠ size(C, 1)
throw(DimensionMismatch("size(A, 1) == $An ≠ $(size(C, 1)) = size(C, 1)"))
end
tall = iseven(An) # is Cdat in the tall format or the wide format?
Cdn, Cdm = size(Cdat)
@assert (Cdn == An + tall) && (Cdm == ((An + !tall) >> 1))

isone(β) || rmul!(Cdat, β)
for jj in axes(A, 2)
rngjj = nzrange(A, jj)
lenrngjj = length(rngjj)
for (k, j) in enumerate(rngjj)
anzj = α * nz[j]
rvj = rv[j] # updates in rvj column of C
if rvj ≤ Cdm # in the trapezoidal part of Cdat
offset = (rvj - 1) * Cdn + tall
for i in k:lenrngjj
kk = rngjj[i]
linind = offset + rv[kk]
Cdat[linind] = muladd(nz[kk], anzj, Cdat[linind])
end
else # in the transposed triangular part of Cdat
Cdrow = rvj - Cdm
for i in k:lenrngjj
kk = rngjj[i]
rvkk = rv[kk]
@assert rvkk ≥ rvj
linind = (rvkk - Cdm - tall) * Cdn + Cdrow
Cdat[linind] = muladd(nz[kk], anzj, Cdat[linind])
end
end
end
end
return C
end

function rankUpdate!(C::HermOrSym, A::BlockedSparse, α, β)
return rankUpdate!(C, sparse(A), α, β)
end

# HermitianRFP is not a HermOrSym, so it needs its own BlockedSparse method that
# forwards to the HermitianRFP/SparseMatrixCSC kernel.
function rankUpdate!(C::HermitianRFP{T}, A::BlockedSparse{T}, α, β) where {T}
return rankUpdate!(C, sparse(A), α, β)
end

function rankUpdate!(
C::HermOrSym{T,Diagonal{T,Vector{T}}}, A::StridedMatrix{T}, α, β
) where {T}
Expand Down
Loading
Loading