From 29edcc2b6b3325227dc45bea5d8174f7cf7d014c Mon Sep 17 00:00:00 2001 From: Douglas Bates Date: Sun, 11 Apr 2021 14:21:32 -0500 Subject: [PATCH 1/7] Define StructType for OptSummary --- Project.toml | 4 ++++ src/MixedModels.jl | 2 ++ src/optsummary.jl | 3 +++ 3 files changed, 9 insertions(+) diff --git a/Project.toml b/Project.toml index 5eaf3541f..ae8c124f4 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45" DataAPI = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" GLM = "38e38edf-8417-5370-95a0-9cbb8c7f171a" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" LazyArtifacts = "4af54fe1-eca0-43a8-85a7-787d91b784e3" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" @@ -21,6 +22,7 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" StatsFuns = "4c63d2b9-4356-54db-8cca-17b64c39e42c" StatsModels = "3eaba693-59b7-5ba5-a881-562e759f1c8d" +StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" [compat] @@ -28,6 +30,7 @@ Arrow = "1" DataAPI = "1" Distributions = "0.21, 0.22, 0.23, 0.24" GLM = "1" +JSON3 = "1" LazyArtifacts = "1" NLopt = "0.5, 0.6" PooledArrays = "0.5, 1" @@ -36,6 +39,7 @@ StaticArrays = "0.11, 0.12, 1" StatsBase = "0.31, 0.32, 0.33" StatsFuns = "0.8, 0.9" StatsModels = "0.6" +StructTypes = "1" Tables = "1" julia = "1.4" diff --git a/src/MixedModels.jl b/src/MixedModels.jl index 49afef0ae..81228f64d 100644 --- a/src/MixedModels.jl +++ b/src/MixedModels.jl @@ -4,6 +4,7 @@ using Arrow using DataAPI using Distributions using GLM +using JSON3 using LazyArtifacts using LinearAlgebra using Markdown @@ -16,6 +17,7 @@ using StaticArrays using Statistics using StatsBase using StatsModels +using StructTypes using Tables using LinearAlgebra: BlasFloat, BlasReal, HermOrSym, PosDefException, copytri! diff --git a/src/optsummary.jl b/src/optsummary.jl index bc2ed089c..5c375a3c1 100644 --- a/src/optsummary.jl +++ b/src/optsummary.jl @@ -110,3 +110,6 @@ function NLopt.Opt(optsum::OptSummary) end opt end + +StructTypes.StructType(::Type{<:OptSummary}) = StructTypes.Mutable() +StructTypes.excludes(::Type{<:OptSummary}) = (:lowerbd, ) From 77e274150b6bfd562dde0bb55fdd0a6049a2d452 Mon Sep 17 00:00:00 2001 From: Douglas Bates Date: Mon, 12 Apr 2021 10:06:14 -0500 Subject: [PATCH 2/7] Add test for JSON form of OptSummary --- test/Project.toml | 2 ++ test/pls.jl | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/test/Project.toml b/test/Project.toml index 281ed3891..4dd018eea 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -3,7 +3,9 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" GLM = "38e38edf-8417-5370-95a0-9cbb8c7f171a" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MixedModels = "ff71e718-51f3-5ec2-a782-8ffcbfa3c316" PooledArrays = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" diff --git a/test/pls.jl b/test/pls.jl index ef99450f7..6f5c10435 100644 --- a/test/pls.jl +++ b/test/pls.jl @@ -1,3 +1,4 @@ +using JSON3 using LinearAlgebra using MixedModels using PooledArrays @@ -401,6 +402,29 @@ end @test countlines(seekstart(io)) == 3 @test "BlkDiag" in Set(split(String(take!(io)), r"\s+")) + @testset "optsumJSON" begin + optsum = last(models(:sleepstudy)).optsum + JSON3.write(seekstart(io), optsum) + dict = JSON3.read(take!(io)) + @test haskey(dict, :initial) +#= This fails in Pkg.test but not in the REPL + for k in keys(dict) + dk = getproperty(dict, k) + osk = getproperty(optsum, k) + if dk isa String + @test osk == Symbol(dk) + else + @test isapprox(dk, osk) + end + end +=# + @test dict.initial == optsum.initial + @test Symbol(dict.optimizer) == optsum.optimizer + @test Symbol(dict.returnvalue) == optsum.returnvalue + @test isapprox(dict.finitial, optsum.finitial) + @test isapprox(dict.fmin, optsum.fmin) + @test isapprox(copy(dict.final), optsum.final) + end end @testset "d3" begin From 9d9261f56eea87a02a2e3d4e959dce18fbd5286c Mon Sep 17 00:00:00 2001 From: Douglas Bates Date: Mon, 12 Apr 2021 16:04:28 -0500 Subject: [PATCH 3/7] Add and export saveoptsum, restoreoptsum! and test --- src/MixedModels.jl | 2 ++ src/linearmixedmodel.jl | 55 +++++++++++++++++++++++++++++++++++++++++ test/pls.jl | 36 +++++++++++---------------- 3 files changed, 72 insertions(+), 21 deletions(-) diff --git a/src/MixedModels.jl b/src/MixedModels.jl index 81228f64d..47c890287 100644 --- a/src/MixedModels.jl +++ b/src/MixedModels.jl @@ -110,6 +110,8 @@ export @formula, replicate, residuals, response, + restoreoptsum!, + saveoptsum, shortestcovint, sdest, setθ!, diff --git a/src/linearmixedmodel.jl b/src/linearmixedmodel.jl index 908f89bfc..924b621ae 100644 --- a/src/linearmixedmodel.jl +++ b/src/linearmixedmodel.jl @@ -782,6 +782,45 @@ StatsBase.residuals(m::LinearMixedModel) = response(m) .- fitted(m) StatsBase.response(m::LinearMixedModel) = m.y +""" + restoreoptsum!(m::LinearMixedModel, io::IO) + restoreoptsum!(m::LinearMixedModel, fnm::AbstractString) + +Read, check, and restore the `optsum` field from a JSON stream or filename. +""" +function restoreoptsum!(m::LinearMixedModel, io::IO) + dict = JSON3.read(io) + ops = m.optsum + okay = (setdiff(propertynames(ops), keys(dict)) == [:lowerbd]) && + isapprox(ops.initial, copy(dict.initial)) && + isapprox(ops.xtol_abs, copy(dict.xtol_abs)) && + ops.ftol_rel == dict.ftol_rel && + ops.xtol_rel == dict.xtol_rel && + ops.ftol_abs == dict.ftol_abs && + ops.maxfeval == dict.maxfeval && + all(ops.lowerbd .≤ dict.final) + if !okay + throw(ArgumentError("io is not a JSON-formatted optsum for model m")) + end + ops.finitial = dict.finitial + copyto!(ops.final, dict.final) + ops.initial_step = copy(dict.initial_step) + ops.fmin = dict.fmin + ops.feval = dict.feval + ops.optimizer = Symbol(dict.optimizer) + ops.returnvalue = Symbol(dict.returnvalue) + ops.nAGQ = dict.nAGQ + ops.REML = dict.REML + updateL!(setθ!(m, ops.final)) + m +end + +function restoreoptsum!(m::LinearMixedModel, fnm::AbstractString) + open(fnm, "r") do io + restoreoptsum!(m, io) + end +end + function reweight!(m::LinearMixedModel, weights) sqrtwts = map!(sqrt, m.sqrtwts, weights) reweight!.(m.reterms, Ref(sqrtwts)) @@ -790,6 +829,22 @@ function reweight!(m::LinearMixedModel, weights) updateL!(m) end +""" + saveoptsum(io::IO, m::LinearMixedModel) + saveoptsum(fnm::AbstractString, m::LinearMixedModel) + +Save `m.optsum` (w/o the `lowerbd` field) in JSON format to an IO stream or a file + +The reason for omitting the `lowerbd` field is because it often contains `-Inf` +values that are not allowed in JSON. +""" +saveoptsum(io::IO, m::LinearMixedModel) = JSON3.write(io, m.optsum) +function saveoptsum(fnm::AbstractString, m::LinearMixedModel) + open(fnm, "w") do io + saveoptsum(io, m) + end +end + """ sdest(m::LinearMixedModel) diff --git a/test/pls.jl b/test/pls.jl index 6f5c10435..c1fdd419f 100644 --- a/test/pls.jl +++ b/test/pls.jl @@ -403,27 +403,21 @@ end @test "BlkDiag" in Set(split(String(take!(io)), r"\s+")) @testset "optsumJSON" begin - optsum = last(models(:sleepstudy)).optsum - JSON3.write(seekstart(io), optsum) - dict = JSON3.read(take!(io)) - @test haskey(dict, :initial) -#= This fails in Pkg.test but not in the REPL - for k in keys(dict) - dk = getproperty(dict, k) - osk = getproperty(optsum, k) - if dk isa String - @test osk == Symbol(dk) - else - @test isapprox(dk, osk) - end - end -=# - @test dict.initial == optsum.initial - @test Symbol(dict.optimizer) == optsum.optimizer - @test Symbol(dict.returnvalue) == optsum.returnvalue - @test isapprox(dict.finitial, optsum.finitial) - @test isapprox(dict.fmin, optsum.fmin) - @test isapprox(copy(dict.final), optsum.final) + fm = last(models(:sleepstudy)) + # using a IOBuffer for saving JSON + saveoptsum(seekstart(io), fm) + m = LinearMixedModel(fm.formula, MixedModels.dataset(:sleepstudy)) + restoreoptsum!(m, seekstart(io)) + @test loglikelihood(fm) ≈ loglikelihood(m) + @test bic(fm) ≈ bic(m) + @test coef(fm) ≈ coef(m) + # using a temporary file for saving JSON + fnm = mktemp() + saveoptsum(fnm, fm) + restoreoptsum!(m, fnm) + @test loglikelihood(fm) ≈ loglikelihood(m) + @test bic(fm) ≈ bic(m) + @test coef(fm) ≈ coef(m) end end From 0c06927065048afa87e084f83a48f3eae927215b Mon Sep 17 00:00:00 2001 From: Douglas Bates Date: Mon, 12 Apr 2021 16:16:36 -0500 Subject: [PATCH 4/7] Correction in a test --- test/pls.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pls.jl b/test/pls.jl index c1fdd419f..48bc536ec 100644 --- a/test/pls.jl +++ b/test/pls.jl @@ -412,7 +412,7 @@ end @test bic(fm) ≈ bic(m) @test coef(fm) ≈ coef(m) # using a temporary file for saving JSON - fnm = mktemp() + fnm = first(mktemp()) saveoptsum(fnm, fm) restoreoptsum!(m, fnm) @test loglikelihood(fm) ≈ loglikelihood(m) From 22f3a52d0a08152e4609d35658ab553eb812714c Mon Sep 17 00:00:00 2001 From: Douglas Bates Date: Mon, 12 Apr 2021 16:38:42 -0500 Subject: [PATCH 5/7] Prune some dependencies. --- test/Project.toml | 2 -- test/pls.jl | 1 - 2 files changed, 3 deletions(-) diff --git a/test/Project.toml b/test/Project.toml index 4dd018eea..281ed3891 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -3,9 +3,7 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" GLM = "38e38edf-8417-5370-95a0-9cbb8c7f171a" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -MixedModels = "ff71e718-51f3-5ec2-a782-8ffcbfa3c316" PooledArrays = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" diff --git a/test/pls.jl b/test/pls.jl index 48bc536ec..8d7a57b76 100644 --- a/test/pls.jl +++ b/test/pls.jl @@ -1,4 +1,3 @@ -using JSON3 using LinearAlgebra using MixedModels using PooledArrays From 6bdeb620123c7ac1a974f7ec4cea2fa2f607fd7d Mon Sep 17 00:00:00 2001 From: Douglas Bates Date: Tue, 13 Apr 2021 09:25:53 -0500 Subject: [PATCH 6/7] Ensure second test is on a freshly-defined model instance Co-authored-by: Phillip Alday --- test/pls.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/pls.jl b/test/pls.jl index 8d7a57b76..200543907 100644 --- a/test/pls.jl +++ b/test/pls.jl @@ -413,6 +413,7 @@ end # using a temporary file for saving JSON fnm = first(mktemp()) saveoptsum(fnm, fm) + m = LinearMixedModel(fm.formula, MixedModels.dataset(:sleepstudy)) restoreoptsum!(m, fnm) @test loglikelihood(fm) ≈ loglikelihood(m) @test bic(fm) ≈ bic(m) From 1a812c3b7760d939af2259627dcedf09d5e60337 Mon Sep 17 00:00:00 2001 From: Douglas Bates Date: Tue, 13 Apr 2021 11:18:54 -0500 Subject: [PATCH 7/7] Reformulate restoreoptsum! with better checks. --- src/linearmixedmodel.jl | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/linearmixedmodel.jl b/src/linearmixedmodel.jl index 924b621ae..753d75f12 100644 --- a/src/linearmixedmodel.jl +++ b/src/linearmixedmodel.jl @@ -792,26 +792,25 @@ function restoreoptsum!(m::LinearMixedModel, io::IO) dict = JSON3.read(io) ops = m.optsum okay = (setdiff(propertynames(ops), keys(dict)) == [:lowerbd]) && - isapprox(ops.initial, copy(dict.initial)) && - isapprox(ops.xtol_abs, copy(dict.xtol_abs)) && - ops.ftol_rel == dict.ftol_rel && - ops.xtol_rel == dict.xtol_rel && - ops.ftol_abs == dict.ftol_abs && - ops.maxfeval == dict.maxfeval && + all(ops.lowerbd .≤ dict.initial) && all(ops.lowerbd .≤ dict.final) if !okay - throw(ArgumentError("io is not a JSON-formatted optsum for model m")) + throw(ArgumentError("initial or final parameters in io do not satify lowerbd")) + end + for fld in (:feval, :finitial, :fmin, :ftol_rel, :ftol_abs, :maxfeval, :nAGQ, :REML) + setproperty!(ops, fld, getproperty(dict, fld)) end - ops.finitial = dict.finitial - copyto!(ops.final, dict.final) ops.initial_step = copy(dict.initial_step) - ops.fmin = dict.fmin - ops.feval = dict.feval + ops.xtol_rel = copy(dict.xtol_rel) + copyto!(ops.initial, dict.initial) + copyto!(ops.final, dict.final) + for (v, f) in (:initial => :finitial, :final => :fmin) + if !isapprox(objective(updateL!(setθ!(m, getfield(ops, v)))), getfield(ops, f)) + throw(ArgumentError("model m at $v does not give stored $f")) + end + end ops.optimizer = Symbol(dict.optimizer) ops.returnvalue = Symbol(dict.returnvalue) - ops.nAGQ = dict.nAGQ - ops.REML = dict.REML - updateL!(setθ!(m, ops.final)) m end @@ -857,7 +856,7 @@ sdest(m::LinearMixedModel) = √varest(m) Install `v` as the θ parameters in `m`. """ -function setθ!(m::LinearMixedModel{T}, θ::Vector{T}) where {T} +function setθ!(m::LinearMixedModel{T}, θ::AbstractVector) where {T} parmap, reterms = m.parmap, m.reterms length(θ) == length(parmap) || throw(DimensionMismatch()) reind = 1