Skip to content

Commit b5ba3ec

Browse files
authored
Generalize sparseL as sparsemat (#880)
* Generalize sparseL as sparsemat * Update NEWS.md * Run src files through format * Add sparseA extractor * Apply dropzeros! to sparseL but not sparseA * minor version bump
1 parent 7ae8df4 commit b5ba3ec

File tree

5 files changed

+82
-52
lines changed

5 files changed

+82
-52
lines changed

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
MixedModels v5.3.0 Release Notes
2+
==============================
3+
- Implement `sparseL` as a specialization of `sparsemat`. Replace `_coord` utility with `_findnz` which, in most cases, falls through to `SparseArrays.findnz`. [#880]
4+
15
MixedModels v5.2.2 Release Notes
26
==============================
37
- Small update to `show` methods to accommodate coming changes in Julia Markdown stdlib. [#876]
@@ -725,3 +729,4 @@ Package dependencies
725729
[#873]: https://github.com/JuliaStats/MixedModels.jl/issues/873
726730
[#875]: https://github.com/JuliaStats/MixedModels.jl/issues/875
727731
[#876]: https://github.com/JuliaStats/MixedModels.jl/issues/876
732+
[#880]: https://github.com/JuliaStats/MixedModels.jl/issues/880

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "MixedModels"
22
uuid = "ff71e718-51f3-5ec2-a782-8ffcbfa3c316"
33
author = ["Phillip Alday <me@phillipalday.com>", "Douglas Bates <dmbates@gmail.com>"]
4-
version = "5.2.2"
4+
version = "5.3.0"
55

66
[deps]
77
Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45"

src/MixedModels.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ using Printf: @sprintf
2727
using ProgressMeter: ProgressMeter, Progress, finish!, next!
2828
using Random: Random, AbstractRNG, randn!
2929
using RegressionFormulae: fulldummy
30-
using SparseArrays: SparseArrays, SparseMatrixCSC, SparseVector, dropzeros!, nnz
31-
using SparseArrays: nonzeros, nzrange, rowvals, sparse
30+
using SparseArrays: SparseArrays, SparseMatrixCSC, SparseVector, dropzeros!, findnz
31+
using SparseArrays: nnz, nonzeros, nzrange, rowvals, sparse
3232
using StaticArrays: StaticArrays, SVector
3333
using Statistics: Statistics, mean, quantile, std
3434
using StatsAPI: StatsAPI, aic, aicc, bic, coef, coefnames, coeftable, confint
@@ -156,6 +156,7 @@ export @formula,
156156
simulate,
157157
simulate!,
158158
sparse,
159+
sparseA,
159160
sparseL,
160161
std,
161162
stderror,

src/linearmixedmodel.jl

Lines changed: 68 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -800,8 +800,9 @@ Return the vector of _canonical_ lower bounds on the parameters, `θ`.
800800
Note that this method does not distinguish between constrained optimization and
801801
unconstrained optimization with post-fit canonicalization.
802802
"""
803-
lowerbd(m::LinearMixedModel{T}) where {T} =
804-
[(pm[2] == pm[3]) ? zero(T) : T(-Inf) for pm in m.parmap]
803+
function lowerbd(m::LinearMixedModel{T}) where {T}
804+
return [(pm[2] == pm[3]) ? zero(T) : T(-Inf) for pm in m.parmap]
805+
end
805806

806807
function mkparmap(reterms::Vector{<:AbstractReMat{T}}) where {T}
807808
parmap = NTuple{3,Int}[]
@@ -1133,82 +1134,101 @@ end
11331134
Base.show(io::IO, m::LinearMixedModel) = Base.show(io, MIME("text/plain"), m)
11341135

11351136
"""
1136-
_coord(A::AbstractMatrix)
1137+
_findnz(A::AbstractMatrix)
1138+
1139+
Return the positions and values of the nonzeros in `A` as an (I, J, V) tuple
11371140
1138-
Return the positions and values of the nonzeros in `A` as a
1139-
`NamedTuple{(:i, :j, :v), Tuple{Vector{Int32}, Vector{Int32}, Vector{Float64}}}`
1141+
When possible, methods for this generic pass through to methods for `SparseArrays.findnz`.
1142+
The exceptions are the `Matrix` and `LinearAlgebra.Diagonal` classes where our defining a
1143+
`findnz` method would be type piracy.
11401144
"""
1141-
function _coord(A::Diagonal)
1142-
return (i=Int32.(axes(A, 1)), j=Int32.(axes(A, 2)), v=A.diag)
1143-
end
11441145

1145-
function _coord(A::UniformBlockDiagonal)
1146-
dat = A.data
1147-
r, c, k = size(dat)
1148-
blk = repeat(r .* (0:(k - 1)); inner=r * c)
1146+
function _findnz(A::Matrix)
1147+
m, n = size(A)
11491148
return (
1150-
i=Int32.(repeat(1:r; outer=c * k) .+ blk),
1151-
j=Int32.(repeat(1:c; inner=r, outer=k) .+ blk),
1152-
v=vec(dat),
1149+
repeat(axes(A, 1); outer=n),
1150+
repeat(axes(A, 2); inner=m),
1151+
vec(A),
11531152
)
11541153
end
11551154

1156-
function _coord(A::SparseMatrixCSC{T,Int32}) where {T}
1157-
rv = rowvals(A)
1158-
cv = similar(rv)
1159-
for j in axes(A, 2), k in nzrange(A, j)
1160-
cv[k] = j
1161-
end
1162-
return (i=rv, j=cv, v=nonzeros(A))
1155+
function _findnz(A::Diagonal)
1156+
return (axes(A, 1), axes(A, 2), A.diag)
11631157
end
11641158

1165-
_coord(A::BlockedSparse) = _coord(A.cscmat)
1159+
_findnz(x::AbstractMatrix) = findnz(x)
11661160

1167-
function _coord(A::Matrix)
1168-
m, n = size(A)
1161+
function SparseArrays.findnz(A::UniformBlockDiagonal)
1162+
dat = A.data
1163+
r, c, k = size(dat)
1164+
blk = repeat(r .* (0:(k - 1)); inner=r * c)
11691165
return (
1170-
i=Int32.(repeat(axes(A, 1); outer=n)),
1171-
j=Int32.(repeat(axes(A, 2); inner=m)),
1172-
v=vec(A),
1166+
repeat(1:r; outer=c * k) .+ blk,
1167+
repeat(1:c; inner=r, outer=k) .+ blk,
1168+
vec(dat),
11731169
)
11741170
end
11751171

1176-
"""
1177-
sparseL(m::LinearMixedModel; fname::Symbol=first(fnames(m)), full::Bool=false)
1178-
1179-
Return the lower Cholesky factor `L` as a `SparseMatrix{T,Int32}`.
1172+
SparseArrays.findnz(A::BlockedSparse) = findnz(A.cscmat)
11801173

1181-
`full` indicates whether the parts of `L` associated with the fixed-effects and response
1182-
are to be included.
1183-
1184-
`fname` specifies the first grouping factor to include. Blocks to the left of the block corresponding
1185-
to `fname` are dropped. The default is the first, i.e., leftmost block and hence all blocks.
1186-
"""
1187-
function sparseL(
1188-
m::LinearMixedModel{T}; fname::Symbol=first(fnames(m)), full::Bool=false
1174+
function sparsemat(
1175+
typ::Symbol, m::LinearMixedModel{T}; fname::Symbol=first(fnames(m)), full::Bool=false
11891176
) where {T}
1190-
L, reterms = m.L, m.reterms
1177+
reterms = m.reterms
1178+
if typ (:A, :L)
1179+
throw(ArgumentError("typ passed as $typ should be :A or :L"))
1180+
end
1181+
bmat = getproperty(m, typ)
11911182
sblk = findfirst(isequal(fname), fnames(m))
11921183
if isnothing(sblk)
11931184
throw(ArgumentError("fname = $fname is not the name of a grouping factor"))
11941185
end
11951186
blks = sblk:(length(reterms) + full)
11961187
rowoffset, coloffset = 0, 0
1197-
val = (i=Int32[], j=Int32[], v=T[])
1188+
I = Int32[]
1189+
J = Int32[]
1190+
V = T[]
11981191
for i in blks, j in first(blks):i
1199-
Lblk = L[block(i, j)]
1200-
cblk = _coord(Lblk)
1201-
append!(val.i, cblk.i .+ Int32(rowoffset))
1202-
append!(val.j, cblk.j .+ Int32(coloffset))
1203-
append!(val.v, cblk.v)
1192+
Lblk = bmat[block(i, j)]
1193+
cblk = _findnz(Lblk)
1194+
append!(I, first(cblk) .+ Int32(rowoffset))
1195+
append!(J, cblk[2] .+ Int32(coloffset))
1196+
append!(V, last(cblk))
12041197
if i == j
12051198
coloffset = 0
12061199
rowoffset += size(Lblk, 1)
12071200
else
12081201
coloffset += size(Lblk, 2)
12091202
end
12101203
end
1211-
return dropzeros!(tril!(sparse(val...)))
1204+
return tril!(sparse(I, J, V))
1205+
end
1206+
1207+
"""
1208+
sparseL(m::LinearMixedModel; fname::Symbol=first(fnames(m)), full::Bool=false)
1209+
1210+
Return the lower Cholesky factor `L` as a `SparseMatrix{T,Int32}`.
1211+
1212+
`full` indicates whether the parts of `L` associated with the fixed-effects and response
1213+
are to be included.
1214+
1215+
`fname` specifies the first grouping factor to include. Blocks to the left of the block corresponding
1216+
to `fname` are dropped. The default is the first, i.e., leftmost block and hence all blocks.
1217+
1218+
The matrix that is returned is lower-triangular but has not been wrapped in `LowerTriangular`.
1219+
"""
1220+
function sparseL(m::LinearMixedModel; fname::Symbol=first(fnames(m)), full::Bool=false)
1221+
return dropzeros!(sparsemat(:L, m; fname, full))
1222+
end
1223+
1224+
"""
1225+
sparseA(m::LinearMixedModel; fname::Symbol=first(fnames(m)), full::Bool=false)
1226+
1227+
Same as [`sparseL`](@ref) but returning the sparse lower triangle of the symmetric `A` matrix.
1228+
Most of the time the result is wrapped as, e.g. `Symmetric(sparseM(m; full=true), :L)`
1229+
"""
1230+
function sparseA(m::LinearMixedModel; fname::Symbol=first(fnames(m)), full::Bool=false)
1231+
return sparsemat(:A, m; fname, full)
12121232
end
12131233

12141234
"""

test/pls.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,14 +277,18 @@ end
277277
end
278278

279279
@testset "InstEval" begin
280-
fm1 = models(:insteval)[2] # at one time this was the fist of the :insteval models
280+
fm1 = models(:insteval)[2] # at one time this was the first of the :insteval models
281281
@test size(fm1) == (73421, 2, 4114, 3)
282282
@test fm1.optsum.initial == ones(3)
283283

284284
spL = sparseL(fm1)
285285
@test size(spL) == (4114, 4114)
286286
@test 733090 < nnz(spL) < 733100
287287

288+
spA = Symmetric(sparseA(fm1; full=true), :L)
289+
@test size(spA) == (4117, 4117)
290+
@test 132600 < nnz(spA.data) < 132650
291+
288292
@test objective(fm1) 237721.76877450474 atol = 0.001
289293
ftd1 = fitted(fm1)
290294
@test size(ftd1) == (73421,)

0 commit comments

Comments
 (0)