Skip to content

Commit 6408a34

Browse files
authored
remove lowerbd field from OptSummary (#849)
* remove lowerbd field from OptSummary * NEWS * fix initial step * remove lower bounds from optsummary show methods * update CI for current release
1 parent fa73f34 commit 6408a34

20 files changed

+32
-109
lines changed

.github/workflows/current.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ jobs:
1919
ci:
2020
runs-on: ${{ matrix.os }}
2121
strategy:
22+
fail-fast: false
2223
matrix:
2324
julia-version: [1]
2425
# julia-arch: [x64]
2526
os:
26-
- ubuntu-22.04
27-
- macOS-14 # apple silicon!
27+
- ubuntu-24.04
28+
- macOS-15 # apple silicon!
2829
steps:
2930
- uses: actions/checkout@v5
3031
- uses: julia-actions/setup-julia@v2

NEWS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ MixedModels v5.0.0 Release Notes
22
==============================
33
- Optimization is now performed _without constraints_. In a post-fitting step, the Cholesky factor is canonicalized to have non-negative diagonal elements. [#840]
44
- The default optimizer has changed to NLopt's implementation of NEWUOA where possible. NLopt's implementation fails on 1-dimensional problems, so in the case of a single, scalar random effect, BOBYQA is used instead. In the future, the default optimizer backend will likely change to PRIMA and NLopt support will be moved to an extension. Blocking this change in backend is an issue with PRIMA.jl when running in VSCode's built-in REPL on Linux. [#840]
5-
- [BREAKING] Support for constrained optimization has been completely removed, i.e. the field `lowerbd` has been removed from `OptSummary`.
5+
- [BREAKING] Support for constrained optimization has been completely removed, i.e. the field `lowerbd` has been removed from `OptSummary`. [#849]
66
- [BREAKING] A fitlog is always kept -- the deprecated keyword argument `thin` has been removed as has the `fitlog` keyword argument. [#850]
77
- The fitlog is now stored as Tables.jl-compatible column table. [#850]
88
- Internal code around the default optimizer has been restructured. In particular, the NLopt backend has been moved to a submodule, which will make it easier to move it to an extension if we promote another backend to the default. [#853]

ext/MixedModelsPRIMAExt.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ function MixedModels.optimize!(m::GeneralizedLinearMixedModel, ::PRIMABackend;
105105
sc
106106
end
107107
info = _optimizer!(Val(optsum.optimizer), obj, optsum.final;
108-
xl=optsum.lowerbd, maxfun,
108+
maxfun,
109109
optsum.rhoend, optsum.rhobeg,
110110
scale)
111111
ProgressMeter.finish!(prog)

src/MixedModelsNLoptExt.jl

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ function MixedModels.optimize!(m::GeneralizedLinearMixedModel, ::NLoptBackend;
9797
end
9898

9999
function NLopt.Opt(optsum::OptSummary)
100-
lb = optsum.lowerbd
101-
n = length(lb)
100+
n = length(optsum.initial)
102101

103102
if optsum.optimizer == :LN_NEWUOA && isone(n) # :LN_NEWUOA doesn't allow n == 1
104103
optsum.optimizer = :LN_BOBYQA
@@ -110,11 +109,10 @@ function NLopt.Opt(optsum::OptSummary)
110109
if length(optsum.xtol_abs) == n # not true for fast=false optimization in GLMM
111110
NLopt.xtol_abs!(opt, optsum.xtol_abs) # absolute criterion on parameter values
112111
end
113-
# NLopt.lower_bounds!(opt, lb) # use unconstrained optimization even for :LN_BOBYQA
114112
NLopt.maxeval!(opt, optsum.maxfeval)
115113
NLopt.maxtime!(opt, optsum.maxtime)
116114
if isempty(optsum.initial_step)
117-
optsum.initial_step = NLopt.initial_step(opt, optsum.initial, similar(lb))
115+
optsum.initial_step = NLopt.initial_step(opt, optsum.initial)
118116
else
119117
NLopt.initial_step!(opt, optsum.initial_step)
120118
end

src/bootstrap.jl

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Object returned by `parametericbootstrap` with fields
1212
- `fits`: the parameter estimates from the bootstrap replicates as a vector of named tuples.
1313
- `λ`: `Vector{LowerTriangular{T,Matrix{T}}}` containing copies of the λ field from `ReMat` model terms
1414
- `inds`: `Vector{Vector{Int}}` containing copies of the `inds` field from `ReMat` model terms
15-
- `lowerbd`: `Vector{T}` containing the vector of lower bounds (corresponds to the identically named field of [`OptSummary`](@ref))
15+
- `lowerbd`: `Vector{T}` containing the vector of lower bounds (corresponds to the `lowerbd(model)` of the original model)
1616
- `fcnames`: NamedTuple whose keys are the grouping factor names and whose values are the column names
1717
1818
The schema of `fits` is, by default,
@@ -34,7 +34,7 @@ struct MixedModelBootstrap{T<:AbstractFloat} <: MixedModelFitCollection{T}
3434
fits::Vector
3535
λ::Vector{Union{LowerTriangular{T},Diagonal{T}}}
3636
inds::Vector{Vector{Int}}
37-
lowerbd::Vector{T}
37+
lowerbd::Vector{T} # we need to store this explicitly because we no longer have access to the ReMats
3838
fcnames::NamedTuple
3939
end
4040

@@ -112,7 +112,7 @@ function restorereplicates(
112112
samp,
113113
map(vv -> T.(vv), m.λ), # also does a deepcopy if no type conversion is necessary
114114
getfield.(m.reterms, :inds),
115-
T.(m.optsum.lowerbd[1:length(first(samp).θ)]),
115+
T.(lowerbd(m)),
116116
NamedTuple{Symbol.(fnames(m))}(map(t -> Tuple(t.cnames), m.reterms)),
117117
)
118118
end
@@ -175,6 +175,8 @@ function Base.show(io::IO, mime::MIME"text/plain", x::MixedModelBootstrap)
175175
return nothing
176176
end
177177

178+
lowerbd(x::MixedModelFitCollection) = x.lowerbd
179+
178180
"""
179181
parametricbootstrap([rng::AbstractRNG], nsamp::Integer, m::MixedModel{T}, ftype=T;
180182
β = fixef(m), σ = m.σ, θ = m.θ, progress=true, optsum_overrides=(;))
@@ -243,8 +245,6 @@ function parametricbootstrap(
243245
for (key, val) in pairs(optsum_overrides)
244246
setfield!(m.optsum, key, val)
245247
end
246-
# this seemed to slow things down?!
247-
# _copy_away_from_lowerbd!(m.optsum.initial, morig.optsum.final, m.lowerbd; incr=0.05)
248248

249249
β_names = Tuple(Symbol.(coefnames(morig)))
250250

@@ -267,7 +267,7 @@ function parametricbootstrap(
267267
samp,
268268
map(vv -> ftype.(vv), morig.λ), # also does a deepcopy if no type conversion is necessary
269269
getfield.(morig.reterms, :inds),
270-
ftype.(morig.optsum.lowerbd[1:length(first(samp).θ)]),
270+
ftype.(lowerbd(morig)),
271271
NamedTuple{Symbol.(fnames(morig))}(map(t -> Tuple(t.cnames), morig.reterms)),
272272
)
273273
end
@@ -421,7 +421,7 @@ function issingular(
421421
bsamp::MixedModelFitCollection; atol::Real=0, rtol::Real=atol > 0 ? 0 : eps()
422422
)
423423
return map(bsamp.θ) do θ
424-
return _issingular(bsamp.lowerbd, θ; atol, rtol)
424+
return _issingular(lowerbd(bsamp), θ; atol, rtol)
425425
end
426426
end
427427

src/generalizedlinearmixedmodel.jl

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ In addition to the fieldnames, the following names are also accessible through t
3030
- `theta`: synonym for `θ`
3131
- `beta`: synonym for `β`
3232
- `σ` or `sigma`: common scale parameter (value is `NaN` for distributions without a scale parameter)
33-
- `lowerbd`: vector of lower bounds on the combined elements of `β` and `θ`
3433
- `formula`, `trms`, `A`, `L`, and `optsum`: fields of the `LMM` field
3534
- `X`: fixed-effects model matrix
3635
- `y`: response vector
@@ -280,7 +279,6 @@ function StatsAPI.fit!(
280279
end
281280

282281
if !fast
283-
optsum.lowerbd = vcat(fill!(similar(β), T(-Inf)), optsum.lowerbd)
284282
optsum.initial = vcat(β, lm.optsum.final)
285283
optsum.final = copy(optsum.initial)
286284
end
@@ -296,8 +294,9 @@ function StatsAPI.fit!(
296294

297295
## check if very small parameter values bounded below by zero can be set to zero
298296
xmin_ = copy(xmin)
297+
lb = fast ? lowerbd(m) : vcat(zero(β), lowerbd(m))
299298
for i in eachindex(xmin_)
300-
if iszero(optsum.lowerbd[i]) && zero(T) < xmin_[i] < optsum.xtol_zero_abs
299+
if iszero(lb[i]) && zero(T) < xmin_[i] < optsum.xtol_zero_abs
301300
xmin_[i] = zero(T)
302301
end
303302
end
@@ -524,6 +523,8 @@ function StatsAPI.loglikelihood(m::GeneralizedLinearMixedModel{T}) where {T}
524523
return accum - (mapreduce(u -> sum(abs2, u), +, m.u) + logdet(m)) / 2
525524
end
526525

526+
lowerbd(m::GeneralizedLinearMixedModel) = lowerbd(m.LMM)
527+
527528
# Base.Fix1 doesn't forward kwargs
528529
function objective!(m::GeneralizedLinearMixedModel; fast=false, kwargs...)
529530
return x -> _objective!(m, x, Val(fast); kwargs...)
@@ -560,7 +561,6 @@ function Base.propertynames(m::GeneralizedLinearMixedModel, private::Bool=false)
560561
:sigma,
561562
:X,
562563
:y,
563-
:lowerbd,
564564
:objective,
565565
:σρs,
566566
:σs,
@@ -785,11 +785,6 @@ function unfit!(model::GeneralizedLinearMixedModel{T}) where {T}
785785

786786
reterms = model.LMM.reterms
787787
optsum = model.LMM.optsum
788-
# we need to reset optsum so that it
789-
# plays nice with the modifications fit!() does
790-
optsum.lowerbd = mapfoldl(lowerbd, vcat, reterms) # probably don't need this anymore - now trivial with all elements = -Inf
791-
# for variances (bounded at zero), we have ones, while
792-
# for everything else (bounded at -Inf), we have zeros
793788
optsum.initial = map(x -> T(x[2] == x[3]), model.LMM.parmap)
794789
optsum.final = copy(optsum.initial)
795790
optsum.xtol_abs = fill!(copy(optsum.initial), 1.0e-10)
@@ -839,7 +834,7 @@ function StatsAPI.weights(m::GeneralizedLinearMixedModel{T}) where {T}
839834
end
840835

841836
# delegate GLMM method to LMM field
842-
for f in (:feL, :fetrm, :fixefnames, :(LinearAlgebra.logdet), :lowerbd, :PCA, :rePCA)
837+
for f in (:feL, :fetrm, :fixefnames, :(LinearAlgebra.logdet), :PCA, :rePCA)
843838
@eval begin
844839
$f(m::GeneralizedLinearMixedModel) = $f(m.LMM)
845840
end

src/linearmixedmodel.jl

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ Linear mixed-effects model representation
2424
* `σ` or `sigma`: current value of the standard deviation of the per-observation noise
2525
* `b`: random effects on the original scale, as a vector of matrices
2626
* `u`: random effects on the orthogonal scale, as a vector of matrices
27-
* `lowerbd`: lower bounds on the elements of θ
2827
* `X`: the fixed-effects model matrix
2928
* `y`: the response vector
3029
"""
@@ -173,9 +172,8 @@ function LinearMixedModel(
173172
reweight!.(reterms, Ref(sqrtwts))
174173
reweight!(Xy, sqrtwts)
175174
A, L = createAL(reterms, Xy)
176-
lbd = foldl(vcat, lowerbd(c) for c in reterms)
177175
θ = foldl(vcat, getθ(c) for c in reterms)
178-
optsum = OptSummary, lbd)
176+
optsum = OptSummary(θ)
179177
optsum.sigma = isnothing(σ) ? nothing : T(σ)
180178
return LinearMixedModel(
181179
form,
@@ -662,8 +660,6 @@ function Base.getproperty(m::LinearMixedModel{T}, s::Symbol) where {T}
662660
stderror(m)
663661
elseif s == :u
664662
ranef(m; uscale=true)
665-
elseif s == :lowerbd
666-
m.optsum.lowerbd
667663
elseif s == :X
668664
modelmatrix(m)
669665
elseif s == :y
@@ -791,7 +787,7 @@ function StatsAPI.loglikelihood(m::LinearMixedModel)
791787
return -objective(m) / 2
792788
end
793789

794-
lowerbd(m::LinearMixedModel) = m.optsum.lowerbd
790+
lowerbd(m::LinearMixedModel) = foldl(vcat, lowerbd(c) for c in m.reterms)
795791

796792
function mkparmap(reterms::Vector{<:AbstractReMat{T}}) where {T}
797793
parmap = NTuple{3,Int}[]
@@ -856,7 +852,6 @@ function Base.propertynames(m::LinearMixedModel, private::Bool=false)
856852
:sigmarhos,
857853
:b,
858854
:u,
859-
:lowerbd,
860855
:X,
861856
:y,
862857
:corr,

src/mimeshow.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ end
184184
function _markdown(s::OptSummary)
185185
optimizer_settings = [["Optimizer", "`$(s.optimizer)`"],
186186
["Backend", "`$(s.backend)`"],
187-
["Lower bounds", string(s.lowerbd)]]
187+
]
188188

189189
for param in opt_params(Val(s.backend))
190190
push!(optimizer_settings, [string(param), string(getfield(s, param))])

src/optsummary.jl

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ Summary of an optimization
88
## Tolerances, initial and final values
99
* `initial`: a copy of the initial parameter values in the optimization
1010
* `finitial`: the initial value of the objective
11-
* `lowerbd`: lower bounds on the parameter values
1211
* `final`: a copy of the final parameter values from the optimization
1312
* `fmin`: the final value of the objective
1413
* `feval`: the number of function evaluations
@@ -65,7 +64,6 @@ Similarly, the list of applicable optimization parameters can be inspected with
6564
Base.@kwdef mutable struct OptSummary{T<:AbstractFloat}
6665
initial::Vector{T}
6766
finitial::T = Inf * one(eltype(initial))
68-
lowerbd::Vector{T}
6967
final::Vector{T} = copy(initial)
7068
fmin::T = Inf * one(eltype(initial))
7169
feval::Int = -1
@@ -100,11 +98,9 @@ end
10098

10199
function OptSummary(
102100
initial::Vector{T},
103-
lowerbd::Vector{S},
104101
optimizer::Symbol=:LN_NEWUOA; kwargs...,
105-
) where {T<:AbstractFloat,S<:AbstractFloat}
106-
TS = promote_type(T, S)
107-
return OptSummary{TS}(; initial, lowerbd, optimizer, kwargs...)
102+
) where {T<:AbstractFloat}
103+
return OptSummary{T}(; initial, optimizer, kwargs...)
108104
end
109105

110106
"""
@@ -141,7 +137,6 @@ function Base.show(io::IO, ::MIME"text/plain", s::OptSummary)
141137
println(io)
142138
println(io, "Backend: ", s.backend)
143139
println(io, "Optimizer: ", s.optimizer)
144-
println(io, "Lower bounds: ", s.lowerbd)
145140

146141
for param in opt_params(Val(s.backend))
147142
println(io, rpad(string(param, ":"), length("Initial parameter vector: ")),

src/profile/fixefpr.jl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@ function FeProfile(m::LinearMixedModel, tc::TableColumns, j::Integer)
4444
LinearMixedModel(y₀ - xⱼ * m.β[j], feterm, reterms, m.formula); progress=false
4545
)
4646
# not sure this next call makes sense - should the second argument be m.optsum.final?
47-
_copy_away_from_lowerbd!(
48-
mnew.optsum.initial, mnew.optsum.final, mnew.lowerbd; incr=0.05
49-
)
47+
copyto!(mnew.optsum.initial, mnew.optsum.final)
5048
return FeProfile(mnew, tc, y₀, xⱼ, j)
5149
end
5250

0 commit comments

Comments
 (0)