Skip to content

Commit

Permalink
Make MathOptInterface.jl a weak dependency (#1081)
Browse files Browse the repository at this point in the history
* Create weak extension

* Adapt tests

* Add Requires

* Add missing import

* Revert formatting

* fix compilation extension

* fix references and pass MOIWrapper tests locally

* Use PackageExtensionCompat instead

* remove Requires

* Solve issue with struct assignment

* readapt tests

* Use more common solution for any julia version

* Add MOI to test
  • Loading branch information
theogf committed Mar 21, 2024
1 parent 78ab1f4 commit 9c65c72
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 30 deletions.
12 changes: 10 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@ FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b"
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
NLSolversBase = "d41bc354-129a-5804-8e4c-c37616107c6c"
NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930"
Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a"
PositiveFactorizations = "85a6dd25-e78a-55b7-8502-1745935b8125"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"

[weakdeps]
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"

[extensions]
OptimMOIExt = "MathOptInterface"

[compat]
Compat = "3.2.0, 3.3.0, 3.4.0, 3.5.0, 3.6.0, 4"
FillArrays = "0.6.2, 0.7, 0.8, 0.9, 0.10, 0.11, 0.12, 0.13, 1"
Expand All @@ -27,6 +33,7 @@ MathOptInterface = "1.17"
NLSolversBase = "7.8.0"
NaNMath = "0.3.2, 1"
OptimTestProblems = "2.0.3"
PackageExtensionCompat = "1"
Parameters = "0.10, 0.11, 0.12"
PositiveFactorizations = "0.2.2"
Printf = "<0.0.1, 1.6"
Expand All @@ -39,6 +46,7 @@ julia = "1.6"
[extras]
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7"
NLSolversBase = "d41bc354-129a-5804-8e4c-c37616107c6c"
OptimTestProblems = "cec144fc-5a64-5bc6-99fb-dde8f63e154c"
Expand All @@ -49,4 +57,4 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "Distributions", "Measurements", "OptimTestProblems", "Random", "RecursiveArrayTools", "StableRNGs", "LineSearches", "NLSolversBase", "PositiveFactorizations"]
test = ["Test", "Distributions", "MathOptInterface", "Measurements", "OptimTestProblems", "Random", "RecursiveArrayTools", "StableRNGs", "LineSearches", "NLSolversBase", "PositiveFactorizations"]
101 changes: 75 additions & 26 deletions src/MOI_wrapper.jl → ext/OptimMOIExt.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
module OptimMOIExt

using Optim
using LinearAlgebra
import MathOptInterface as MOI

function __init__()
@static if VERSION >= v"1.9"
@eval Optim begin
OptimMOIExt = Base.get_extension(@__MODULE__, :OptimMOIExt)
const Optimizer = OptimMOIExt.Optimizer
end
# setglobal!(Optim, :Optimizer, Optimizer)
else
@eval Optim begin
using .OptimMOIExt
const Optimizer = OptimMOIExt.Optimizer
end
end
end

mutable struct Optimizer{T} <: MOI.AbstractOptimizer
# Problem data.
variables::MOI.Utilities.VariablesContainer{T}
Expand All @@ -8,12 +27,12 @@ mutable struct Optimizer{T} <: MOI.AbstractOptimizer
sense::MOI.OptimizationSense

# Parameters.
method::Union{AbstractOptimizer,Nothing}
method::Union{Optim.AbstractOptimizer,Nothing}
silent::Bool
options::Dict{Symbol,Any}

# Solution attributes.
results::Union{Nothing,MultivariateOptimizationResults}
results::Union{Nothing,Optim.MultivariateOptimizationResults}
end

function Optimizer{T}() where {T}
Expand All @@ -37,7 +56,7 @@ function MOI.supports(::Optimizer, ::Union{MOI.ObjectiveSense,MOI.ObjectiveFunct
end
MOI.supports(::Optimizer, ::MOI.Silent) = true
function MOI.supports(::Optimizer, p::MOI.RawOptimizerAttribute)
return p.name == "method" || hasfield(Options, Symbol(p.name))
return p.name == "method" || hasfield(Optim.Options, Symbol(p.name))
end

function MOI.supports(::Optimizer, ::MOI.VariablePrimalStart, ::Type{MOI.VariableIndex})
Expand Down Expand Up @@ -103,7 +122,7 @@ function MOI.get(model::Optimizer, ::MOI.TimeLimitSec)
return get(model.options, Symbol(TIME_LIMIT), nothing)
end

MOI.Utilities.map_indices(::Function, opt::AbstractOptimizer) = opt
MOI.Utilities.map_indices(::Function, opt::Optim.AbstractOptimizer) = opt

function MOI.set(model::Optimizer, p::MOI.RawOptimizerAttribute, value)
if p.name == "method"
Expand Down Expand Up @@ -151,7 +170,11 @@ function MOI.is_valid(model::Optimizer, index::Union{MOI.VariableIndex,MOI.Const
return MOI.is_valid(model.variables, index)
end

function MOI.add_constraint(model::Optimizer{T}, vi::MOI.VariableIndex, set::BOUNDS{T}) where {T}
function MOI.add_constraint(
model::Optimizer{T},
vi::MOI.VariableIndex,
set::BOUNDS{T},
) where {T}
return MOI.add_constraint(model.variables, vi, set)
end

Expand Down Expand Up @@ -187,17 +210,17 @@ function MOI.set(
return
end

function requested_features(::ZerothOrderOptimizer, has_constraints)
function requested_features(::Optim.ZerothOrderOptimizer, has_constraints)
return Symbol[]
end
function requested_features(::FirstOrderOptimizer, has_constraints)
function requested_features(::Optim.FirstOrderOptimizer, has_constraints)
features = [:Grad]
if has_constraints
push!(features, :Jac)
end
return features
end
function requested_features(::Union{IPNewton,SecondOrderOptimizer}, has_constraints)
function requested_features(::Union{IPNewton,Optim.SecondOrderOptimizer}, has_constraints)
features = [:Grad, :Hess]
if has_constraints
push!(features, :Jac)
Expand Down Expand Up @@ -255,7 +278,12 @@ function MOI.optimize!(model::Optimizer{T}) where {T}
method = model.method
nl_constrained = !isempty(nlp_data.constraint_bounds)
features = MOI.features_available(evaluator)
has_bounds = any(vi -> isfinite(model.variables.lower[vi.value]) || isfinite(model.variables.upper[vi.value]), vars)
has_bounds = any(
vi ->
isfinite(model.variables.lower[vi.value]) ||
isfinite(model.variables.upper[vi.value]),
vars,
)
if method === nothing
if nl_constrained
method = IPNewton()
Expand All @@ -264,12 +292,12 @@ function MOI.optimize!(model::Optimizer{T}) where {T}
# are variable bounds, `Newton` is not supported. On the other hand,
# `fallback_method(f, g!)` returns `LBFGS` which is supported if `has_bounds`.
if :Hess in features && !has_bounds
method = fallback_method(f, g!, h!)
method = Optim.fallback_method(f, g!, h!)
else
method = fallback_method(f, g!)
method = Optim.fallback_method(f, g!)
end
else
method = fallback_method(f)
method = Optim.fallback_method(f)
end
end
used_features = requested_features(method, nl_constrained)
Expand All @@ -283,22 +311,35 @@ function MOI.optimize!(model::Optimizer{T}) where {T}
initial_x = starting_value.(model, eachindex(model.starting_values))
options = copy(model.options)
if !nl_constrained && has_bounds && !(method isa IPNewton)
options = Options(; options...)
model.results = optimize(f, g!, model.variables.lower, model.variables.upper, initial_x, Fminbox(method), options; inplace = true)
options = Optim.Options(; options...)
model.results = optimize(
f,
g!,
model.variables.lower,
model.variables.upper,
initial_x,
Fminbox(method),
options;
inplace = true,
)
else
d = promote_objtype(method, initial_x, :finite, true, f, g!, h!)
add_default_opts!(options, method)
options = Options(; options...)
d = Optim.promote_objtype(method, initial_x, :finite, true, f, g!, h!)
Optim.add_default_opts!(options, method)
options = Optim.Options(; options...)
if nl_constrained || has_bounds
if nl_constrained
lc = [b.lower for b in nlp_data.constraint_bounds]
uc = [b.upper for b in nlp_data.constraint_bounds]
c!(c, x) = MOI.eval_constraint(evaluator, c, x)
if !(:Jac in features)
error("Nonlinear constraints should be differentiable to be used with Optim.")
error(
"Nonlinear constraints should be differentiable to be used with Optim.",
)
end
if !(:Hess in features)
error("Nonlinear constraints should be twice differentiable to be used with Optim.")
error(
"Nonlinear constraints should be twice differentiable to be used with Optim.",
)
end
jacobian_structure = MOI.jacobian_structure(evaluator)
J_nzval = zeros(T, length(jacobian_structure))
Expand All @@ -315,13 +356,20 @@ function MOI.optimize!(model::Optimizer{T}) where {T}
return H
end
c = TwiceDifferentiableConstraints(
c!, jacobian!, con_hessian!,
model.variables.lower, model.variables.upper, lc, uc,
c!,
jacobian!,
con_hessian!,
model.variables.lower,
model.variables.upper,
lc,
uc,
)
else
@assert has_bounds
c = TwiceDifferentiableConstraints(
model.variables.lower, model.variables.upper)
model.variables.lower,
model.variables.upper,
)
end
model.results = optimize(d, c, initial_x, method, options)
else
Expand All @@ -334,7 +382,7 @@ end
function MOI.get(model::Optimizer, ::MOI.TerminationStatus)
if model.results === nothing
return MOI.OPTIMIZE_NOT_CALLED
elseif converged(model.results)
elseif Optim.converged(model.results)
return MOI.LOCALLY_SOLVED
else
return MOI.OTHER_ERROR
Expand All @@ -354,7 +402,7 @@ function MOI.get(model::Optimizer, attr::MOI.PrimalStatus)
if !(1 <= attr.result_index <= MOI.get(model, MOI.ResultCount()))
return MOI.NO_SOLUTION
end
if converged(model.results)
if Optim.converged(model.results)
return MOI.FEASIBLE_POINT
else
return MOI.UNKNOWN_RESULT_STATUS
Expand All @@ -374,7 +422,7 @@ end
function MOI.get(model::Optimizer, attr::MOI.VariablePrimal, vi::MOI.VariableIndex)
MOI.check_result_index_bounds(model, attr)
MOI.throw_if_not_valid(model, vi)
return minimizer(model.results)[vi.value]
return Optim.minimizer(model.results)[vi.value]
end

function MOI.get(
Expand All @@ -384,5 +432,6 @@ function MOI.get(
) where {T}
MOI.check_result_index_bounds(model, attr)
MOI.throw_if_not_valid(model, ci)
return minimizer(model.results)[ci.value]
return Optim.minimizer(model.results)[ci.value]
end
end # module
6 changes: 4 additions & 2 deletions src/Optim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ using Printf # For printing, maybe look into other options
using FillArrays # For handling scalar bounds in Fminbox

#using Compat # for compatibility across multiple julia versions
using PackageExtensionCompat # For retrocompatibility on package extensions

# for extensions of functions defined in Base.
import Base: length, push!, show, getindex, setindex!, maximum, minimum
Expand Down Expand Up @@ -220,7 +221,8 @@ include("multivariate/solvers/constrained/ipnewton/utilities/trace.jl")
# Maximization convenience wrapper
include("maximize.jl")

# MathOptInterface wrapper
include("MOI_wrapper.jl")
function __init__()
@require_extensions
end

end

0 comments on commit 9c65c72

Please sign in to comment.