From 3fff3319e88c92c3d826e38038bf185b505da082 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 4 May 2021 08:50:28 +1200 Subject: [PATCH 01/15] Breaking changes for MOI 0.10.0 --- bench/runbench.jl | 6 ++--- src/Clp.jl | 5 ++++ src/MOI_wrapper/MOI_wrapper.jl | 37 +++++++++++++++--------------- src/precompile.jl | 27 ++++++++++++++++++++++ test/MOI_wrapper.jl | 42 +++++++++++++++------------------- 5 files changed, 71 insertions(+), 46 deletions(-) create mode 100644 src/precompile.jl diff --git a/bench/runbench.jl b/bench/runbench.jl index 3d0f0d0..c37b064 100644 --- a/bench/runbench.jl +++ b/bench/runbench.jl @@ -36,7 +36,7 @@ function generate_moi_problem(model, At, b, c; else for row in 1:rows MOI.add_constraint(model, MOI.VectorAffineFunction( - [MOI.VectorAffineTerm(1, + [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(A_vals[i], x[A_cols[i]]) ) for i in nzrange(At, row)], [-b[row]]), MOI.Nonpositives(1)) @@ -112,7 +112,7 @@ function time_build_and_solve(to_build, to_solve, At, b, c, scalar = true) end @time @timeit "opt" MOI.optimize!(to_solve) MOI.get(to_solve, MOI.ObjectiveValue()) - val = MOI.get(to_solve, MOI.SolveTime()) + val = MOI.get(to_solve, MOI.SolveTimeSec()) println(val) end @@ -145,4 +145,4 @@ function solve_clp(seed, data; time_limit_sec=Inf) end -solve_clp(10, RandomLP(10000, 20000, 0.01); time_limit_sec=5) \ No newline at end of file +solve_clp(10, RandomLP(10000, 20000, 0.01); time_limit_sec=5) diff --git a/src/Clp.jl b/src/Clp.jl index 8909e32..5130612 100644 --- a/src/Clp.jl +++ b/src/Clp.jl @@ -50,4 +50,9 @@ for sym in names(@__MODULE__, all = true) end end +if VERSION > v"1.4.2" + include("precompile.jl") + _precompile_() +end + end diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index 169b348..bcaf66b 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -25,7 +25,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer Create a new Optimizer object. - Set optimizer attributes using `MOI.RawParameter` or + Set optimizer attributes using `MOI.RawOptimizerAttribute` or `JuMP.set_optimizer_atttribute`. For a list of supported parameter names, see `Clp.SUPPORTED_PARAMETERS`. @@ -41,14 +41,14 @@ mutable struct Optimizer <: MOI.AbstractOptimizer if length(kwargs) > 0 @warn("""Passing optimizer attributes as keyword arguments to Clp.Optimizer is deprecated. Use - MOI.set(model, MOI.RawParameter("key"), value) + MOI.set(model, MOI.RawOptimizerAttribute("key"), value) or JuMP.set_optimizer_attribute(model, "key", value) instead. """) end for (key, value) in kwargs - MOI.set(model, MOI.RawParameter(String(key)), value) + MOI.set(model, MOI.RawOptimizerAttribute(String(key)), value) end finalizer(model) do m Clp_deleteModel(m) @@ -75,7 +75,7 @@ end function MOI.empty!(model::Optimizer) # Copy parameters from old model into new model old_options = Dict( - key => MOI.get(model, MOI.RawParameter(key)) for + key => MOI.get(model, MOI.RawOptimizerAttribute(key)) for key in model.options_set ) empty!(model.options_set) @@ -84,7 +84,7 @@ function MOI.empty!(model::Optimizer) model.optimize_called = false model.solve_time = 0.0 for (key, value) in old_options - MOI.set(model, MOI.RawParameter(key), value) + MOI.set(model, MOI.RawOptimizerAttribute(key), value) end # Work-around for maximumSeconds Clp_setMaximumSeconds(model, model.maximumSeconds) @@ -111,11 +111,11 @@ const SUPPORTED_PARAMETERS = ( "InfeasibleReturn", ) -function MOI.supports(::Optimizer, param::MOI.RawParameter) +function MOI.supports(::Optimizer, param::MOI.RawOptimizerAttribute) return param.name in SUPPORTED_PARAMETERS end -function MOI.set(model::Optimizer, param::MOI.RawParameter, value) +function MOI.set(model::Optimizer, param::MOI.RawOptimizerAttribute, value) name = String(param.name) push!(model.options_set, name) if name == "PrimalTolerance" @@ -148,7 +148,7 @@ function MOI.set(model::Optimizer, param::MOI.RawParameter, value) return end -function MOI.get(model::Optimizer, param::MOI.RawParameter) +function MOI.get(model::Optimizer, param::MOI.RawOptimizerAttribute) name = String(param.name) if name == "PrimalTolerance" return Clp_primalTolerance(model) @@ -260,10 +260,9 @@ function _extract_bound_data(src, mapping, lb, ub, ::Type{S}) where {S} MOI.get(src, MOI.ListOfConstraintIndices{MOI.SingleVariable,S}()) f = MOI.get(src, MOI.ConstraintFunction(), con_index) s = MOI.get(src, MOI.ConstraintSet(), con_index) - column = mapping.varmap[f.variable].value + column = mapping[f.variable].value _add_bounds(lb, ub, column, s) - mapping.conmap[con_index] = - MOI.ConstraintIndex{MOI.SingleVariable,S}(column) + mapping[con_index] = MOI.ConstraintIndex{MOI.SingleVariable,S}(column) end end @@ -271,14 +270,14 @@ function _copy_to_columns(dest::Optimizer, src, mapping) x_src = MOI.get(src, MOI.ListOfVariableIndices()) N = Cint(length(x_src)) for i in 1:N - mapping.varmap[x_src[i]] = MOI.VariableIndex(i) + mapping[x_src[i]] = MOI.VariableIndex(i) end fobj = MOI.get(src, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) c = fill(0.0, N) for term in fobj.terms - i = mapping.varmap[term.variable_index].value + i = mapping[term.variable].value c[i] += term.coefficient end # Clp seems to negates the objective offset @@ -321,10 +320,10 @@ function _extract_row_data(src, mapping, lb, ub, I, J, V, ::Type{S}) where {S} f = fs[i]#MOI.get(src, MOI.ConstraintFunction(), c_index) for term in f.terms push!(I, row) - push!(J, Cint(mapping.varmap[term.variable_index].value)) + push!(J, Cint(mapping[term.variable].value)) push!(V, term.coefficient) end - mapping.conmap[c_index] = + mapping[c_index] = MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}(row) row += 1 end @@ -332,7 +331,7 @@ function _extract_row_data(src, mapping, lb, ub, I, J, V, ::Type{S}) where {S} end function test_data(src, dest) - for (F, S) in MOI.get(src, MOI.ListOfConstraints()) + for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) if !MOI.supports_constraint(dest, F, S) throw( MOI.UnsupportedConstraint{F,S}( @@ -409,7 +408,7 @@ function MOI.optimize!(model::Optimizer) return end -function MOI.get(model::Optimizer, ::MOI.SolveTime) +function MOI.get(model::Optimizer, ::MOI.SolveTimeSec) return model.solve_time end @@ -475,7 +474,7 @@ function MOI.get(model::Optimizer, ::MOI.ResultCount) end function MOI.get(model::Optimizer, attr::MOI.PrimalStatus) - if attr.N != 1 + if attr.result_index != 1 return MOI.NO_SOLUTION elseif Clp_isProvenDualInfeasible(model) != 0 return MOI.INFEASIBILITY_CERTIFICATE @@ -487,7 +486,7 @@ function MOI.get(model::Optimizer, attr::MOI.PrimalStatus) end function MOI.get(model::Optimizer, attr::MOI.DualStatus) - if attr.N != 1 + if attr.result_index != 1 return MOI.NO_SOLUTION elseif Clp_isProvenPrimalInfeasible(model) != 0 return MOI.INFEASIBILITY_CERTIFICATE diff --git a/src/precompile.jl b/src/precompile.jl new file mode 100644 index 0000000..3c50fa8 --- /dev/null +++ b/src/precompile.jl @@ -0,0 +1,27 @@ +function _fake_precompile() + cache = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + clp = MOI.instantiate(Clp.Optimizer, with_bridge_type = Float64) + MOI.copy_to(clp, cache; copy_names = false) + return clp +end + +function _precompile_() + ccall(:jl_generating_output, Cint, ()) == 1 || return nothing + T = Float64 + scalar_sets = ( + MOI.LessThan{T}, + MOI.GreaterThan{T}, + MOI.EqualTo{T}, + MOI.Interval{T}, + ) + scalar_functions = (MOI.SingleVariable, MOI.ScalarAffineFunction{T}) + MOI.precompile_model( + MOI.Bridges.LazyBridgeOptimizer{MOI.Utilities.CachingOptimizer{ + Optimizer, + MOI.Utilities.UniversalFallback{MOI.Utilities.Model{Float64}}, + }}, + [(F, S) for F in scalar_functions, S in scalar_sets], + ) + @assert Base.precompile(_fake_precompile, ()) + return +end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 1a05d70..bfd8436 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -89,41 +89,35 @@ function test_Nonexistant_unbounded_ray() @test status == MOI.DUAL_INFEASIBLE end -function test_RawParameter() +function test_RawOptimizerAttribute() model = Clp.Optimizer() - MOI.set(model, MOI.RawParameter("LogLevel"), 1) - @test MOI.get(model, MOI.RawParameter("LogLevel")) == 1 - @test MOI.get(model, MOI.RawParameter(:LogLevel)) == 1 - MOI.set(model, MOI.RawParameter(:LogLevel), 2) - @test MOI.get(model, MOI.RawParameter("LogLevel")) == 2 - @test MOI.get(model, MOI.RawParameter(:LogLevel)) == 2 + MOI.set(model, MOI.RawOptimizerAttribute("LogLevel"), 1) + @test MOI.get(model, MOI.RawOptimizerAttribute("LogLevel")) == 1 + MOI.set(model, MOI.RawOptimizerAttribute("LogLevel"), 2) + @test MOI.get(model, MOI.RawOptimizerAttribute("LogLevel")) == 2 - MOI.set(model, MOI.RawParameter("SolveType"), 1) - @test MOI.get(model, MOI.RawParameter("SolveType")) == 1 - @test MOI.get(model, MOI.RawParameter(:SolveType)) == 1 - MOI.set(model, MOI.RawParameter("SolveType"), 4) - @test MOI.get(model, MOI.RawParameter("SolveType")) == 4 - @test MOI.get(model, MOI.RawParameter(:SolveType)) == 4 + MOI.set(model, MOI.RawOptimizerAttribute("SolveType"), 1) + @test MOI.get(model, MOI.RawOptimizerAttribute("SolveType")) == 1 + MOI.set(model, MOI.RawOptimizerAttribute("SolveType"), 4) + @test MOI.get(model, MOI.RawOptimizerAttribute("SolveType")) == 4 - MOI.set(model, MOI.RawParameter("PresolveType"), 1) - @test MOI.get(model, MOI.RawParameter("PresolveType")) == 1 - @test MOI.get(model, MOI.RawParameter(:PresolveType)) == 1 - MOI.set(model, MOI.RawParameter("PresolveType"), 0) - @test MOI.get(model, MOI.RawParameter("PresolveType")) == 0 - @test MOI.get(model, MOI.RawParameter(:PresolveType)) == 0 + MOI.set(model, MOI.RawOptimizerAttribute("PresolveType"), 1) + @test MOI.get(model, MOI.RawOptimizerAttribute("PresolveType")) == 1 + MOI.set(model, MOI.RawOptimizerAttribute("PresolveType"), 0) + @test MOI.get(model, MOI.RawOptimizerAttribute("PresolveType")) == 0 end function test_All_parameters() model = Clp.Optimizer() - param = MOI.RawParameter("NotAnOption") + param = MOI.RawOptimizerAttribute("NotAnOption") @test !MOI.supports(model, param) @test_throws MOI.UnsupportedAttribute(param) MOI.get(model, param) @test_throws MOI.UnsupportedAttribute(param) MOI.set(model, param, false) for key in Clp.SUPPORTED_PARAMETERS - @test MOI.supports(model, MOI.RawParameter(key)) - value = MOI.get(model, MOI.RawParameter(key)) - MOI.set(model, MOI.RawParameter(key), value) - @test MOI.get(model, MOI.RawParameter(key)) == value + @test MOI.supports(model, MOI.RawOptimizerAttribute(key)) + value = MOI.get(model, MOI.RawOptimizerAttribute(key)) + MOI.set(model, MOI.RawOptimizerAttribute(key), value) + @test MOI.get(model, MOI.RawOptimizerAttribute(key)) == value end end From d6571f55e9444cbde1bea32233697b24f776c5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 7 Apr 2021 23:51:19 +0200 Subject: [PATCH 02/15] Use matrix of constraints --- src/MOI_wrapper/MOI_wrapper.jl | 195 +++++++++++---------------------- 1 file changed, 62 insertions(+), 133 deletions(-) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index bcaf66b..2c4ff9f 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -3,6 +3,27 @@ import SparseArrays const MOI = MathOptInterface +MOI.Utilities.@product_of_sets( + _LPProductOfSets, + MOI.EqualTo{T}, + MOI.LessThan{T}, + MOI.GreaterThan{T}, +) + +const _Model = MOI.Utilities.GenericOptimizer{ + Float64, + MOI.Utilities.MatrixOfConstraints{ + Float64, + MOI.Utilities.MutableSparseMatrixCSC{ + Float64, + Cint, + MOI.Utilities.ZeroBasedIndexing, + }, + MOI.Utilities.Box{Float64}, + _LPProductOfSets{Float64}, + }, +} + # Supported scalar sets const SCALAR_SETS = Union{ MOI.GreaterThan{Float64}, @@ -242,148 +263,32 @@ end # `copy_to` function # ======================= -function _add_bounds(::Vector{Float64}, ub, i, s::MOI.LessThan{Float64}) - return ub[i] = s.upper -end -function _add_bounds(lb, ::Vector{Float64}, i, s::MOI.GreaterThan{Float64}) - return lb[i] = s.lower -end -function _add_bounds(lb, ub, i, s::MOI.EqualTo{Float64}) - return lb[i], ub[i] = s.value, s.value -end -function _add_bounds(lb, ub, i, s::MOI.Interval{Float64}) - return lb[i], ub[i] = s.lower, s.upper -end - -function _extract_bound_data(src, mapping, lb, ub, ::Type{S}) where {S} - for con_index in - MOI.get(src, MOI.ListOfConstraintIndices{MOI.SingleVariable,S}()) - f = MOI.get(src, MOI.ConstraintFunction(), con_index) - s = MOI.get(src, MOI.ConstraintSet(), con_index) - column = mapping[f.variable].value - _add_bounds(lb, ub, column, s) - mapping[con_index] = MOI.ConstraintIndex{MOI.SingleVariable,S}(column) - end -end - -function _copy_to_columns(dest::Optimizer, src, mapping) - x_src = MOI.get(src, MOI.ListOfVariableIndices()) - N = Cint(length(x_src)) - for i in 1:N - mapping[x_src[i]] = MOI.VariableIndex(i) - end - - fobj = - MOI.get(src, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) - c = fill(0.0, N) - for term in fobj.terms - i = mapping[term.variable].value - c[i] += term.coefficient - end - # Clp seems to negates the objective offset - Clp_setObjectiveOffset(dest, -fobj.constant) - return N, c -end - -_bounds(s::MOI.GreaterThan{Float64}) = (s.lower, Inf) -_bounds(s::MOI.LessThan{Float64}) = (-Inf, s.upper) -_bounds(s::MOI.EqualTo{Float64}) = (s.value, s.value) -_bounds(s::MOI.Interval{Float64}) = (s.lower, s.upper) - -function add_sizehint!(vec, n) - len = length(vec) - return sizehint!(vec, len + n) -end - -function _extract_row_data(src, mapping, lb, ub, I, J, V, ::Type{S}) where {S} - row = length(I) == 0 ? 1 : I[end] + 1 - list = MOI.get( +function _copy_to(dest::Optimizer, src::_Model) + @assert MOI.is_empty(dest) + A = src.constraints.coefficients + row_bounds = src.constraints.constants + obj = MOI.get( src, - MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64},S}(), + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), ) - add_sizehint!(lb, length(list)) - add_sizehint!(ub, length(list)) - n_terms = 0 - fs = Array{MOI.ScalarAffineFunction{Float64}}(undef, length(list)) - for (i, c_index) in enumerate(list) - f = MOI.get(src, MOI.ConstraintFunction(), c_index) - fs[i] = f - l, u = _bounds(MOI.get(src, MOI.ConstraintSet(), c_index)) - push!(lb, l - f.constant) - push!(ub, u - f.constant) - n_terms += length(f.terms) - end - add_sizehint!(I, n_terms) - add_sizehint!(J, n_terms) - add_sizehint!(V, n_terms) - for (i, c_index) in enumerate(list) - f = fs[i]#MOI.get(src, MOI.ConstraintFunction(), c_index) - for term in f.terms - push!(I, row) - push!(J, Cint(mapping[term.variable].value)) - push!(V, term.coefficient) - end - mapping[c_index] = - MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}(row) - row += 1 + c = zeros(A.n) + for term in obj.terms + c[term.variable.value] += term.coefficient end - return -end - -function test_data(src, dest) - for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) - if !MOI.supports_constraint(dest, F, S) - throw( - MOI.UnsupportedConstraint{F,S}( - "Clp.Optimizer does not support constraints of type $F-in-$S.", - ), - ) - end - end - fobj_type = MOI.get(src, MOI.ObjectiveFunctionType()) - if !MOI.supports(dest, MOI.ObjectiveFunction{fobj_type}()) - throw(MOI.UnsupportedAttribute(MOI.ObjectiveFunction(fobj_type))) - end -end - -function MOI.copy_to( - dest::Optimizer, - src::MOI.ModelLike; - copy_names::Bool = false, -) - @assert MOI.is_empty(dest) - test_data(src, dest) - - mapping = MOI.Utilities.IndexMap() - N, c = _copy_to_columns(dest, src, mapping) - cl, cu = fill(-Inf, N), fill(Inf, N) - rl, ru, I, J, V = Float64[], Float64[], Cint[], Cint[], Float64[] - - _extract_bound_data(src, mapping, cl, cu, MOI.GreaterThan{Float64}) - _extract_row_data(src, mapping, rl, ru, I, J, V, MOI.GreaterThan{Float64}) - _extract_bound_data(src, mapping, cl, cu, MOI.LessThan{Float64}) - _extract_row_data(src, mapping, rl, ru, I, J, V, MOI.LessThan{Float64}) - _extract_bound_data(src, mapping, cl, cu, MOI.EqualTo{Float64}) - _extract_row_data(src, mapping, rl, ru, I, J, V, MOI.EqualTo{Float64}) - _extract_bound_data(src, mapping, cl, cu, MOI.Interval{Float64}) - _extract_row_data(src, mapping, rl, ru, I, J, V, MOI.Interval{Float64}) - - M = Cint(length(rl)) - A = SparseArrays.sparse(I, J, V, M, N) + Clp_setObjectiveOffset(dest, -obj.constant) Clp_loadProblem( dest, A.n, A.m, - A.colptr .- Cint(1), - A.rowval .- Cint(1), + A.colptr, + A.rowval, A.nzval, - cl, - cu, + src.lower_bound, + src.upper_bound, c, - rl, - ru, + row_bounds.lower, + row_bounds.upper, ) - sense = MOI.get(src, MOI.ObjectiveSense()) if sense == MOI.MIN_SENSE Clp_setObjSense(dest, 1) @@ -393,7 +298,31 @@ function MOI.copy_to( @assert sense == MOI.FEASIBILITY_SENSE Clp_setObjSense(dest, 0) end - return mapping + return MOI.Utilities.identity_index_map(src) +end + +function MOI.copy_to(dest::Optimizer, src::_Model; copy_names::Bool = false) + _copy_to(dest, src) + return MOI.Utilities.identity_index_map(src) +end + +function MOI.copy_to( + dest::Optimizer, + src::MOI.Utilities.UniversalFallback{_Model}; + copy_names::Bool = false, +) + return MOI.copy_to(dest, src.model) +end + +function MOI.copy_to( + dest::Optimizer, + src::MOI.ModelLike; + copy_names::Bool = false +) + model = _Model() + index_map = MOI.copy_to(model, src) + _copy_to(dest, model) + return index_map end # =============================== From c29ae2d96f19aed57abaea820124ac5ab0216b72 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 20 May 2021 16:31:27 +1200 Subject: [PATCH 03/15] WIP: update copy_to interface to MatrixOfConstraints --- src/MOI_wrapper/MOI_wrapper.jl | 56 ++++++++++++++++++++++++++++------ test/MOI_wrapper.jl | 5 +-- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index 2c4ff9f..a5d0d9b 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -8,9 +8,10 @@ MOI.Utilities.@product_of_sets( MOI.EqualTo{T}, MOI.LessThan{T}, MOI.GreaterThan{T}, + MOI.Interval{T}, ) -const _Model = MOI.Utilities.GenericOptimizer{ +const OptimizerCache = MOI.Utilities.GenericOptimizer{ Float64, MOI.Utilities.MatrixOfConstraints{ Float64, @@ -263,7 +264,32 @@ end # `copy_to` function # ======================= -function _copy_to(dest::Optimizer, src::_Model) +""" + _index_map(src::OptimizerCache) + +Create an `IndexMap` mapping the variables and constraints in `OptimizerCache` +to their corresponding 1-based columns and rows. +""" +function _index_map(src::OptimizerCache) + index_map = MOI.Utilities.IndexMap() + for (i, x) in enumerate(MOI.get(src, MOI.ListOfVariableIndices())) + index_map[x] = MOI.VariableIndex(i) + end + for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) + for ci in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) + if F == MOI.SingleVariable + col = index_map[MOI.VariableIndex(ci.value)].value + index_map[ci] = MOI.ConstraintIndex{F,S}(col) + else + row = MOI.Utilities.rows(src.constraints, ci) + index_map[ci] = MOI.ConstraintIndex{F,S}(row) + end + end + end + return index_map +end + +function _copy_to(dest::Optimizer, src::OptimizerCache) @assert MOI.is_empty(dest) A = src.constraints.coefficients row_bounds = src.constraints.constants @@ -298,17 +324,20 @@ function _copy_to(dest::Optimizer, src::_Model) @assert sense == MOI.FEASIBILITY_SENSE Clp_setObjSense(dest, 0) end - return MOI.Utilities.identity_index_map(src) + return _index_map(src) end -function MOI.copy_to(dest::Optimizer, src::_Model; copy_names::Bool = false) - _copy_to(dest, src) - return MOI.Utilities.identity_index_map(src) +function MOI.copy_to( + dest::Optimizer, + src::OptimizerCache; + copy_names::Bool = false, +) + return _copy_to(dest, src) end function MOI.copy_to( dest::Optimizer, - src::MOI.Utilities.UniversalFallback{_Model}; + src::MOI.Utilities.UniversalFallback{OptimizerCache}; copy_names::Bool = false, ) return MOI.copy_to(dest, src.model) @@ -319,9 +348,16 @@ function MOI.copy_to( src::MOI.ModelLike; copy_names::Bool = false ) - model = _Model() - index_map = MOI.copy_to(model, src) - _copy_to(dest, model) + cache = OptimizerCache() + src_cache = MOI.copy_to(cache, src) + cache_dest = _copy_to(dest, cache) + index_map = MOI.Utilities.IndexMap() + for (src_x, cache_x) in src_cache.var_map + index_map[src_x] = cache_dest[cache_x] + end + for (src_ci, cache_ci) in src_cache.con_map + index_map[src_ci] = cache_dest[cache_ci] + end return index_map end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index bfd8436..df27280 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -11,6 +11,7 @@ MOI.set(OPTIMIZER, MOI.Silent(), true) const CACHED = MOI.Utilities.CachingOptimizer( MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + # MOI.Utilities.UniversalFallback(Clp.ModelCache()), OPTIMIZER, ) @@ -25,8 +26,8 @@ end function test_supports_default_copy_to() @test !MOI.Utilities.supports_allocate_load(OPTIMIZER, false) @test !MOI.Utilities.supports_allocate_load(OPTIMIZER, true) - @test !MOI.Utilities.supports_default_copy_to(OPTIMIZER, false) - @test !MOI.Utilities.supports_default_copy_to(OPTIMIZER, true) + @test !MOI.supports_incremental_interface(OPTIMIZER, false) + @test !MOI.supports_incremental_interface(OPTIMIZER, true) end function test_basicconstraint() From 1d077ed13adb78ed7251544c6eeff052ade25d23 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 25 May 2021 10:18:10 +1200 Subject: [PATCH 04/15] Try get CI working --- .github/workflows/ci.yml | 2 ++ src/MOI_wrapper/MOI_wrapper.jl | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec323da..98687a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,8 @@ jobs: ${{ runner.os }}-test- ${{ runner.os }}- - uses: julia-actions/julia-buildpkg@v1 + - name: MOI#master + run: julia -e 'using Pkg; Pkg.add(PackageSpec(name="MathOptInterface", rev="master"))' - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v1 diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index a5d0d9b..2b73929 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -293,10 +293,8 @@ function _copy_to(dest::Optimizer, src::OptimizerCache) @assert MOI.is_empty(dest) A = src.constraints.coefficients row_bounds = src.constraints.constants - obj = MOI.get( - src, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - ) + obj = + MOI.get(src, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) c = zeros(A.n) for term in obj.terms c[term.variable.value] += term.coefficient @@ -346,7 +344,7 @@ end function MOI.copy_to( dest::Optimizer, src::MOI.ModelLike; - copy_names::Bool = false + copy_names::Bool = false, ) cache = OptimizerCache() src_cache = MOI.copy_to(cache, src) From 5320d851945505660ca1bd40a76b494734c31779 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 25 May 2021 10:27:55 +1200 Subject: [PATCH 05/15] Fix --- .github/workflows/ci.yml | 2 -- test/runtests.jl | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98687a1..ec323da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,8 +36,6 @@ jobs: ${{ runner.os }}-test- ${{ runner.os }}- - uses: julia-actions/julia-buildpkg@v1 - - name: MOI#master - run: julia -e 'using Pkg; Pkg.add(PackageSpec(name="MathOptInterface", rev="master"))' - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v1 diff --git a/test/runtests.jl b/test/runtests.jl index 6db7034..aa624bf 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,6 @@ +import Pkg +Pkg.add(PackageSpec(name="MathOptInterface", rev="master")) + using Test function runtests(mod) From 101a4f53793c86be64ebbe8a7c6faa4cc798ed3e Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 25 May 2021 10:49:30 +1200 Subject: [PATCH 06/15] Fix again --- Project.toml | 3 ++- test/runtests.jl | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 1a508d0..d4dd249 100644 --- a/Project.toml +++ b/Project.toml @@ -20,6 +20,7 @@ julia = "1" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" [targets] -test = ["Test"] +test = ["Test", "Pkg"] diff --git a/test/runtests.jl b/test/runtests.jl index aa624bf..769842e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,7 @@ -import Pkg -Pkg.add(PackageSpec(name="MathOptInterface", rev="master")) +if get(ENV, "GITHUB_ACTIONS", "") == "true" + import Pkg + Pkg.add(PackageSpec(name="MathOptInterface", rev="master")) +end using Test From 08875444f35c51bf8d83b97fd76bc2bdae574695 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 25 May 2021 10:59:50 +1200 Subject: [PATCH 07/15] Fix --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 769842e..cd80fc3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,6 @@ if get(ENV, "GITHUB_ACTIONS", "") == "true" import Pkg - Pkg.add(PackageSpec(name="MathOptInterface", rev="master")) + Pkg.add(Pkg.PackageSpec(name = "MathOptInterface", rev = "master")) end using Test From 4b2b2c2cb832c7e00eefc38508d05707841b5649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 26 Jun 2021 10:21:43 -0400 Subject: [PATCH 08/15] Updates --- Project.toml | 2 +- src/MOI_wrapper/MOI_wrapper.jl | 21 ++++++++------------- test/MOI_wrapper.jl | 15 +++++++-------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/Project.toml b/Project.toml index d4dd249..5819e93 100644 --- a/Project.toml +++ b/Project.toml @@ -19,8 +19,8 @@ MathOptInterface = "0.9.6" julia = "1" [extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test", "Pkg"] diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index 2b73929..3c78496 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -11,7 +11,7 @@ MOI.Utilities.@product_of_sets( MOI.Interval{T}, ) -const OptimizerCache = MOI.Utilities.GenericOptimizer{ +const OptimizerCache = MOI.Utilities.GenericModel{ Float64, MOI.Utilities.MatrixOfConstraints{ Float64, @@ -307,8 +307,8 @@ function _copy_to(dest::Optimizer, src::OptimizerCache) A.colptr, A.rowval, A.nzval, - src.lower_bound, - src.upper_bound, + src.variable_bounds.lower, + src.variable_bounds.upper, c, row_bounds.lower, row_bounds.upper, @@ -721,14 +721,9 @@ end function MOI.get( model::Optimizer, - ::MOI.ConstraintBasisStatus, - c::MOI.ConstraintIndex{MOI.SingleVariable,S}, -) where {S} - code = Clp_getColumnStatus(model, c.value - 1) - status = _CLP_BASIS_STATUS[code] - if status == MOI.NONBASIC_AT_UPPER || status == MOI.NONBASIC_AT_LOWER - return _nonbasic_status(status, S) - else - return status - end + ::MOI.VariableBasisStatus, + vi::MOI.VariableIndex, +) + code = Clp_getColumnStatus(model, vi.value - 1) + return _CLP_BASIS_STATUS[code] end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index df27280..59e1b44 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -17,7 +17,7 @@ const CACHED = MOI.Utilities.CachingOptimizer( const BRIDGED = MOI.Bridges.full_bridge_optimizer(CACHED, Float64) -const CONFIG = MOI.Test.TestConfig(dual_objective_value = false, basis = true) +const CONFIG = MOI.DeprecatedTest.Config(dual_objective_value = false, basis = true) function test_SolverName() @test MOI.get(OPTIMIZER, MOI.SolverName()) == "Clp" @@ -31,11 +31,11 @@ function test_supports_default_copy_to() end function test_basicconstraint() - return MOI.Test.basic_constraint_tests(CACHED, CONFIG) + return MOI.DeprecatedTest.basic_constraint_tests(CACHED, CONFIG) end function test_unittest() - return MOI.Test.unittest( + return MOI.DeprecatedTest.unittest( BRIDGED, CONFIG, [ @@ -58,22 +58,22 @@ function test_unittest() end function test_contlinear() - return MOI.Test.contlineartest(BRIDGED, CONFIG, [ + return MOI.DeprecatedTest.contlineartest(BRIDGED, CONFIG, [ # MOI.VariablePrimalStart not supported. "partial_start", ]) end function test_nametest() - return MOI.Test.nametest(BRIDGED) + return MOI.DeprecatedTest.nametest(BRIDGED, delete=false) end function test_validtest() - return MOI.Test.validtest(BRIDGED) + return MOI.DeprecatedTest.validtest(CACHED) end function test_emptytest() - return MOI.Test.emptytest(BRIDGED) + return MOI.DeprecatedTest.emptytest(BRIDGED) end function test_Nonexistant_unbounded_ray() @@ -325,7 +325,6 @@ function test_farkas_dual_max_ii() @test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb) c_dual = MOI.get(model, MOI.ConstraintDual(), c) - @show clb_dual, c_dual @test clb_dual[1] < -1e-6 @test clb_dual[2] < -1e-6 @test c_dual[1] < -1e-6 From 4ac920c9e12297702e48c277f74ac9793b13a127 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 10 Aug 2021 14:26:56 +1200 Subject: [PATCH 09/15] Update to MOI.Test --- src/MOI_wrapper/MOI_wrapper.jl | 57 +++--- test/MOI_wrapper.jl | 305 ++++++--------------------------- test/runtests.jl | 11 -- 3 files changed, 87 insertions(+), 286 deletions(-) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index 3c78496..aae6373 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -20,7 +20,7 @@ const OptimizerCache = MOI.Utilities.GenericModel{ Cint, MOI.Utilities.ZeroBasedIndexing, }, - MOI.Utilities.Box{Float64}, + MOI.Utilities.Hyperrectangle{Float64}, _LPProductOfSets{Float64}, }, } @@ -74,7 +74,8 @@ mutable struct Optimizer <: MOI.AbstractOptimizer end finalizer(model) do m Clp_deleteModel(m) - return ClpSolve_delete(m.solver_options) + ClpSolve_delete(m.solver_options) + return end return model end @@ -264,6 +265,32 @@ end # `copy_to` function # ======================= +function _index_map( + src::OptimizerCache, + index_map, + ::Type{F}, + ::Type{S}, +) where {F,S} + for ci in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) + row = MOI.Utilities.rows(src.constraints, ci)::Int + index_map[ci] = MOI.ConstraintIndex{F,S}(row) + end + return +end + +function _index_map( + src::OptimizerCache, + index_map, + F::Type{MOI.SingleVariable}, + ::Type{S}, +) where {S} + for ci in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) + col = index_map[MOI.VariableIndex(ci.value)].value + index_map[ci] = MOI.ConstraintIndex{F,S}(col) + end + return +end + """ _index_map(src::OptimizerCache) @@ -276,20 +303,16 @@ function _index_map(src::OptimizerCache) index_map[x] = MOI.VariableIndex(i) end for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) - for ci in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) - if F == MOI.SingleVariable - col = index_map[MOI.VariableIndex(ci.value)].value - index_map[ci] = MOI.ConstraintIndex{F,S}(col) - else - row = MOI.Utilities.rows(src.constraints, ci) - index_map[ci] = MOI.ConstraintIndex{F,S}(row) - end - end + _index_map(src, index_map, F, S) end return index_map end -function _copy_to(dest::Optimizer, src::OptimizerCache) +function MOI.copy_to( + dest::Optimizer, + src::OptimizerCache; + copy_names::Bool = false, +) @assert MOI.is_empty(dest) A = src.constraints.coefficients row_bounds = src.constraints.constants @@ -325,14 +348,6 @@ function _copy_to(dest::Optimizer, src::OptimizerCache) return _index_map(src) end -function MOI.copy_to( - dest::Optimizer, - src::OptimizerCache; - copy_names::Bool = false, -) - return _copy_to(dest, src) -end - function MOI.copy_to( dest::Optimizer, src::MOI.Utilities.UniversalFallback{OptimizerCache}; @@ -348,7 +363,7 @@ function MOI.copy_to( ) cache = OptimizerCache() src_cache = MOI.copy_to(cache, src) - cache_dest = _copy_to(dest, cache) + cache_dest = MOI.copy_to(dest, cache) index_map = MOI.Utilities.IndexMap() for (src_x, cache_x) in src_cache.var_map index_map[src_x] = cache_dest[cache_x] diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 59e1b44..00547f0 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -6,88 +6,71 @@ import Clp const MOI = MathOptInterface -const OPTIMIZER = Clp.Optimizer() -MOI.set(OPTIMIZER, MOI.Silent(), true) - -const CACHED = MOI.Utilities.CachingOptimizer( - MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), - # MOI.Utilities.UniversalFallback(Clp.ModelCache()), - OPTIMIZER, -) - -const BRIDGED = MOI.Bridges.full_bridge_optimizer(CACHED, Float64) - -const CONFIG = MOI.DeprecatedTest.Config(dual_objective_value = false, basis = true) +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end +end function test_SolverName() - @test MOI.get(OPTIMIZER, MOI.SolverName()) == "Clp" + @test MOI.get(Clp.Optimizer(), MOI.SolverName()) == "Clp" + return end function test_supports_default_copy_to() - @test !MOI.Utilities.supports_allocate_load(OPTIMIZER, false) - @test !MOI.Utilities.supports_allocate_load(OPTIMIZER, true) - @test !MOI.supports_incremental_interface(OPTIMIZER, false) - @test !MOI.supports_incremental_interface(OPTIMIZER, true) -end - -function test_basicconstraint() - return MOI.DeprecatedTest.basic_constraint_tests(CACHED, CONFIG) -end - -function test_unittest() - return MOI.DeprecatedTest.unittest( - BRIDGED, - CONFIG, - [ - # Not supported by upstream. - "number_threads", - - # Tests that require integer variables - "solve_integer_edge_cases", - "solve_zero_one_with_bounds_1", - "solve_zero_one_with_bounds_2", - "solve_zero_one_with_bounds_3", - "solve_objbound_edge_cases", - - # Tests that require quadratic objective / constraints - "solve_qcp_edge_cases", - "solve_qp_edge_cases", - "delete_soc_variables", + @test !MOI.supports_incremental_interface(Clp.Optimizer(), false) + @test !MOI.supports_incremental_interface(Clp.Optimizer(), true) + return +end + +function test_runtests() + model = MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + Clp.Optimizer(), + ), + Float64, + ) + MOI.set(model, MOI.Silent(), true) + MOI.Test.runtests( + model, + MOI.Test.Config( + exclude = Any[ + MOI.DualObjectiveValue, MOI.ObjectiveBound, + ], + ), + exclude = [ + # TODO(odow): bug in Clp.jl + "test_model_copy_to_UnsupportedAttribute", + # Unable to prove infeasibility + "test_conic_NormInfinityCone_INFEASIBLE", + "test_conic_NormOneCone_INFEASIBLE", ], ) -end - -function test_contlinear() - return MOI.DeprecatedTest.contlineartest(BRIDGED, CONFIG, [ - # MOI.VariablePrimalStart not supported. - "partial_start", - ]) -end - -function test_nametest() - return MOI.DeprecatedTest.nametest(BRIDGED, delete=false) -end - -function test_validtest() - return MOI.DeprecatedTest.validtest(CACHED) -end - -function test_emptytest() - return MOI.DeprecatedTest.emptytest(BRIDGED) + return end function test_Nonexistant_unbounded_ray() - MOI.empty!(BRIDGED) - x = MOI.add_variables(BRIDGED, 5) + model = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + Clp.Optimizer(), + ) + MOI.set(model, MOI.Silent(), true) + x = MOI.add_variables(model, 5) MOI.set( - BRIDGED, + model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, x), 0.0), ) - MOI.set(BRIDGED, MOI.ObjectiveSense(), MOI.MAX_SENSE) - MOI.optimize!(BRIDGED) - status = MOI.get(BRIDGED, MOI.TerminationStatus()) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + MOI.optimize!(model) + status = MOI.get(model, MOI.TerminationStatus()) @test status == MOI.DUAL_INFEASIBLE + return end function test_RawOptimizerAttribute() @@ -146,192 +129,6 @@ function test_options_after_empty!() @test MOI.get(model, MOI.Silent()) == true end -function test_farkas_dual_min() - MOI.empty!(BRIDGED) - model = BRIDGED - MOI.set(model, MOI.Silent(), true) - x = MOI.add_variables(model, 2) - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.SingleVariable}(), - MOI.SingleVariable(x[1]), - ) - clb = - MOI.add_constraint.(model, MOI.SingleVariable.(x), MOI.GreaterThan(0.0)) - c = MOI.add_constraint( - model, - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0), - MOI.LessThan(-1.0), - ) - MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE - @test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE - clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb) - c_dual = MOI.get(model, MOI.ConstraintDual(), c) - @show clb_dual, c_dual - @test clb_dual[1] > 1e-6 - @test clb_dual[2] > 1e-6 - @test c_dual[1] < -1e-6 - @test clb_dual[1] ≈ -2 * c_dual atol = 1e-6 - @test clb_dual[2] ≈ -c_dual atol = 1e-6 -end - -function test_farkas_dual_min_interval() - MOI.empty!(BRIDGED) - model = BRIDGED - MOI.set(model, MOI.Silent(), true) - x = MOI.add_variables(model, 2) - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.SingleVariable}(), - MOI.SingleVariable(x[1]), - ) - clb = - MOI.add_constraint.( - model, - MOI.SingleVariable.(x), - MOI.Interval(0.0, 10.0), - ) - c = MOI.add_constraint( - model, - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0), - MOI.LessThan(-1.0), - ) - MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE - @test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE - clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb) - c_dual = MOI.get(model, MOI.ConstraintDual(), c) - @show clb_dual, c_dual - @test clb_dual[1] > 1e-6 - @test clb_dual[2] > 1e-6 - @test c_dual[1] < -1e-6 - @test clb_dual[1] ≈ -2 * c_dual atol = 1e-6 - @test clb_dual[2] ≈ -c_dual atol = 1e-6 -end - -function test_farkas_dual_min_equalto() - MOI.empty!(BRIDGED) - model = BRIDGED - MOI.set(model, MOI.Silent(), true) - x = MOI.add_variables(model, 2) - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.SingleVariable}(), - MOI.SingleVariable(x[1]), - ) - clb = MOI.add_constraint.(model, MOI.SingleVariable.(x), MOI.EqualTo(0.0)) - c = MOI.add_constraint( - model, - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0), - MOI.LessThan(-1.0), - ) - MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE - @test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE - clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb) - c_dual = MOI.get(model, MOI.ConstraintDual(), c) - @show clb_dual, c_dual - @test clb_dual[1] > 1e-6 - @test clb_dual[2] > 1e-6 - @test c_dual[1] < -1e-6 - @test clb_dual[1] ≈ -2 * c_dual atol = 1e-6 - @test clb_dual[2] ≈ -c_dual atol = 1e-6 -end - -function test_farkas_dual_min_ii() - MOI.empty!(BRIDGED) - model = BRIDGED - MOI.set(model, MOI.Silent(), true) - x = MOI.add_variables(model, 2) - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-1.0, x[1])], 0.0), - ) - clb = MOI.add_constraint.(model, MOI.SingleVariable.(x), MOI.LessThan(0.0)) - c = MOI.add_constraint( - model, - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-2.0, -1.0], x), 0.0), - MOI.LessThan(-1.0), - ) - MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE - @test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE - clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb) - c_dual = MOI.get(model, MOI.ConstraintDual(), c) - @show clb_dual, c_dual - @test clb_dual[1] < -1e-6 - @test clb_dual[2] < -1e-6 - @test c_dual[1] < -1e-6 - @test clb_dual[1] ≈ 2 * c_dual atol = 1e-6 - @test clb_dual[2] ≈ c_dual atol = 1e-6 -end - -function test_farkas_dual_max() - MOI.empty!(BRIDGED) - model = BRIDGED - MOI.set(model, MOI.Silent(), true) - x = MOI.add_variables(model, 2) - MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.SingleVariable}(), - MOI.SingleVariable(x[1]), - ) - clb = - MOI.add_constraint.(model, MOI.SingleVariable.(x), MOI.GreaterThan(0.0)) - c = MOI.add_constraint( - model, - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0), - MOI.LessThan(-1.0), - ) - MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE - @test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE - clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb) - c_dual = MOI.get(model, MOI.ConstraintDual(), c) - @show clb_dual, c_dual - @test clb_dual[1] > 1e-6 - @test clb_dual[2] > 1e-6 - @test c_dual[1] < -1e-6 - @test clb_dual[1] ≈ -2 * c_dual atol = 1e-6 - @test clb_dual[2] ≈ -c_dual atol = 1e-6 -end - -function test_farkas_dual_max_ii() - MOI.empty!(BRIDGED) - model = BRIDGED - MOI.set(model, MOI.Silent(), true) - x = MOI.add_variables(model, 2) - MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-1.0, x[1])], 0.0), - ) - clb = MOI.add_constraint.(model, MOI.SingleVariable.(x), MOI.LessThan(0.0)) - c = MOI.add_constraint( - model, - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-2.0, -1.0], x), 0.0), - MOI.LessThan(-1.0), - ) - MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE - @test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE - clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb) - c_dual = MOI.get(model, MOI.ConstraintDual(), c) - @test clb_dual[1] < -1e-6 - @test clb_dual[2] < -1e-6 - @test c_dual[1] < -1e-6 - @test clb_dual[1] ≈ 2 * c_dual atol = 1e-6 - @test clb_dual[2] ≈ c_dual atol = 1e-6 -end - end # module TestMOIWrapper -runtests(TestMOIWrapper) +TestMOIWrapper.runtests() diff --git a/test/runtests.jl b/test/runtests.jl index cd80fc3..cdaf52a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,17 +5,6 @@ end using Test -function runtests(mod) - for name in names(mod; all = true) - if !startswith("$(name)", "test_") - continue - end - @testset "$(name)" begin - getfield(mod, name)() - end - end -end - @testset "MathOptInterface" begin include("MOI_wrapper.jl") end From 0f929ce3d40560cec84a905a3ed2f6e078a1df23 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 10 Aug 2021 14:44:01 +1200 Subject: [PATCH 10/15] Fix formatting --- test/MOI_wrapper.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 00547f0..f8a6791 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -39,9 +39,7 @@ function test_runtests() MOI.Test.runtests( model, MOI.Test.Config( - exclude = Any[ - MOI.DualObjectiveValue, MOI.ObjectiveBound, - ], + exclude = Any[MOI.DualObjectiveValue, MOI.ObjectiveBound], ), exclude = [ # TODO(odow): bug in Clp.jl From 513cd2c1e12ef205bcc5820943d4d817fc6252d1 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 10 Aug 2021 14:45:11 +1200 Subject: [PATCH 11/15] Remove type assert --- src/MOI_wrapper/MOI_wrapper.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index aae6373..ea5db6e 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -272,7 +272,7 @@ function _index_map( ::Type{S}, ) where {F,S} for ci in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) - row = MOI.Utilities.rows(src.constraints, ci)::Int + row = MOI.Utilities.rows(src.constraints, ci) index_map[ci] = MOI.ConstraintIndex{F,S}(row) end return From fbe63442c57d5e6a194d651ef3d68425d0f9361b Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 12 Aug 2021 16:48:18 +1200 Subject: [PATCH 12/15] More updates --- src/MOI_wrapper/MOI_wrapper.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index ea5db6e..db37838 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -13,6 +13,8 @@ MOI.Utilities.@product_of_sets( const OptimizerCache = MOI.Utilities.GenericModel{ Float64, + MOI.Utilities.ObjectiveFunctionContainer{Float64}, + MOI.Utilities.SingleVariableConstraints{Float64}, MOI.Utilities.MatrixOfConstraints{ Float64, MOI.Utilities.MutableSparseMatrixCSC{ @@ -330,8 +332,8 @@ function MOI.copy_to( A.colptr, A.rowval, A.nzval, - src.variable_bounds.lower, - src.variable_bounds.upper, + src.variables.lower, + src.variables.upper, c, row_bounds.lower, row_bounds.upper, From 87403fd1bd0f85e30239c3ba339d36975b9216ba Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 2 Sep 2021 15:15:21 +1200 Subject: [PATCH 13/15] Updates to latest MOI --- src/Clp.jl | 5 -- src/MOI_wrapper/MOI_wrapper.jl | 91 ++++++++++++---------------------- src/precompile.jl | 27 ---------- test/MOI_wrapper.jl | 8 ++- 4 files changed, 38 insertions(+), 93 deletions(-) delete mode 100644 src/precompile.jl diff --git a/src/Clp.jl b/src/Clp.jl index 5130612..8909e32 100644 --- a/src/Clp.jl +++ b/src/Clp.jl @@ -50,9 +50,4 @@ for sym in names(@__MODULE__, all = true) end end -if VERSION > v"1.4.2" - include("precompile.jl") - _precompile_() -end - end diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index db37838..30ac111 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -13,8 +13,8 @@ MOI.Utilities.@product_of_sets( const OptimizerCache = MOI.Utilities.GenericModel{ Float64, - MOI.Utilities.ObjectiveFunctionContainer{Float64}, - MOI.Utilities.SingleVariableConstraints{Float64}, + MOI.Utilities.ObjectiveContainer{Float64}, + MOI.Utilities.VariablesContainer{Float64}, MOI.Utilities.MatrixOfConstraints{ Float64, MOI.Utilities.MutableSparseMatrixCSC{ @@ -27,7 +27,6 @@ const OptimizerCache = MOI.Utilities.GenericModel{ }, } -# Supported scalar sets const SCALAR_SETS = Union{ MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, @@ -55,9 +54,11 @@ mutable struct Optimizer <: MOI.AbstractOptimizer ## Example - using JuMP, Clp - model = JuMP.Model(Clp.Optimizer) - set_optimizer_attribute(model, "LogLevel", 0) + ```julia + using JuMP, Clp + model = JuMP.Model(Clp.Optimizer) + set_optimizer_attribute(model, "LogLevel", 0) + ``` """ function Optimizer(; kwargs...) model = @@ -200,21 +201,19 @@ function MOI.get(model::Optimizer, param::MOI.RawOptimizerAttribute) return ClpSolve_getSolveType(model.solver_options) elseif name == "InfeasibleReturn" return ClpSolve_infeasibleReturn(model.solver_options) - else - throw(MOI.UnsupportedAttribute(param)) end + return throw(MOI.UnsupportedAttribute(param)) end MOI.supports(::Optimizer, ::MOI.Silent) = true + function MOI.set(model::Optimizer, ::MOI.Silent, value::Bool) push!(model.options_set, "LogLevel") Clp_setLogLevel(model, value ? 0 : 1) return end -function MOI.get(model::Optimizer, ::MOI.Silent) - return Clp_logLevel(model) == 0 -end +MOI.get(model::Optimizer, ::MOI.Silent) = Clp_logLevel(model) == 0 MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true @@ -240,15 +239,7 @@ MOI.supports(::Optimizer, ::MOI.NumberOfThreads) = false function MOI.supports_constraint( ::Optimizer, - ::Type{MOI.ScalarAffineFunction{Float64}}, - ::Type{<:SCALAR_SETS}, -) - return true -end - -function MOI.supports_constraint( - ::Optimizer, - ::Type{MOI.SingleVariable}, + ::Type{<:Union{MOI.VariableIndex,MOI.ScalarAffineFunction{Float64}}}, ::Type{<:SCALAR_SETS}, ) return true @@ -283,7 +274,7 @@ end function _index_map( src::OptimizerCache, index_map, - F::Type{MOI.SingleVariable}, + F::Type{MOI.VariableIndex}, ::Type{S}, ) where {S} for ci in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) @@ -300,7 +291,7 @@ Create an `IndexMap` mapping the variables and constraints in `OptimizerCache` to their corresponding 1-based columns and rows. """ function _index_map(src::OptimizerCache) - index_map = MOI.Utilities.IndexMap() + index_map = MOI.IndexMap() for (i, x) in enumerate(MOI.get(src, MOI.ListOfVariableIndices())) index_map[x] = MOI.VariableIndex(i) end @@ -310,11 +301,7 @@ function _index_map(src::OptimizerCache) return index_map end -function MOI.copy_to( - dest::Optimizer, - src::OptimizerCache; - copy_names::Bool = false, -) +function MOI.copy_to(dest::Optimizer, src::OptimizerCache) @assert MOI.is_empty(dest) A = src.constraints.coefficients row_bounds = src.constraints.constants @@ -352,21 +339,19 @@ end function MOI.copy_to( dest::Optimizer, - src::MOI.Utilities.UniversalFallback{OptimizerCache}; - copy_names::Bool = false, + src::MOI.Utilities.UniversalFallback{OptimizerCache}, ) return MOI.copy_to(dest, src.model) end function MOI.copy_to( dest::Optimizer, - src::MOI.ModelLike; - copy_names::Bool = false, + src::MOI.ModelLike, ) cache = OptimizerCache() src_cache = MOI.copy_to(cache, src) cache_dest = MOI.copy_to(dest, cache) - index_map = MOI.Utilities.IndexMap() + index_map = MOI.IndexMap() for (src_x, cache_x) in src_cache.var_map index_map[src_x] = cache_dest[cache_x] end @@ -388,13 +373,9 @@ function MOI.optimize!(model::Optimizer) return end -function MOI.get(model::Optimizer, ::MOI.SolveTimeSec) - return model.solve_time -end +MOI.get(model::Optimizer, ::MOI.SolveTimeSec) = model.solve_time -function MOI.get(model::Optimizer, ::MOI.NumberOfVariables) - return Clp_getNumCols(model) -end +MOI.get(model::Optimizer, ::MOI.NumberOfVariables) = Clp_getNumCols(model) function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue) MOI.check_result_index_bounds(model, attr) @@ -415,9 +396,8 @@ function MOI.get(model::Optimizer, ::MOI.TerminationStatus) elseif st == 3 # No more granular information that "some limit is reached" return MOI.OTHER_LIMIT - else - return MOI.OTHER_ERROR end + return MOI.OTHER_ERROR end function MOI.get(model::Optimizer, ::MOI.RawStatusString) @@ -433,10 +413,9 @@ function MOI.get(model::Optimizer, ::MOI.RawStatusString) return "2 - dual infeasible" elseif st == 3 return "3 - stopped on iterations etc" - elseif st == 4 - return "4 - stopped due to errors" else - error("Expected integer in [0, 4] but got $st") + @assert st == 4 + return "4 - stopped due to errors" end end @@ -460,9 +439,8 @@ function MOI.get(model::Optimizer, attr::MOI.PrimalStatus) return MOI.INFEASIBILITY_CERTIFICATE elseif Clp_primalFeasible(model) != 0 return MOI.FEASIBLE_POINT - else - return MOI.UNKNOWN_RESULT_STATUS end + return MOI.UNKNOWN_RESULT_STATUS end function MOI.get(model::Optimizer, attr::MOI.DualStatus) @@ -472,9 +450,8 @@ function MOI.get(model::Optimizer, attr::MOI.DualStatus) return MOI.INFEASIBILITY_CERTIFICATE elseif Clp_dualFeasible(model) != 0 return MOI.FEASIBLE_POINT - else - return MOI.UNKNOWN_RESULT_STATUS end + return MOI.UNKNOWN_RESULT_STATUS end # =================== @@ -519,9 +496,8 @@ function MOI.get( Clp_getNumCols(model), x.value, ) - else - error("Primal solution not available") end + return error("Primal solution not available") end function MOI.get( @@ -548,9 +524,8 @@ function MOI.get( Clp_getNumCols(model), col_indices, ) - else - error("Primal solution not available") end + return error("Primal solution not available") end # TODO: What happens if model is unbounded / infeasible? @@ -572,7 +547,7 @@ end function MOI.get( model::Optimizer, attr::MOI.ConstraintPrimal, - c::MOI.ConstraintIndex{MOI.SingleVariable,<:SCALAR_SETS}, + c::MOI.ConstraintIndex{MOI.VariableIndex,<:SCALAR_SETS}, ) MOI.check_result_index_bounds(model, attr) return MOI.get(model, MOI.VariablePrimal(), MOI.VariableIndex(c.value)) @@ -631,15 +606,14 @@ function MOI.get( c.value; own = true, ) - else - error("Dual solution not available") end + return error("Dual solution not available") end function MOI.get( model::Optimizer, attr::MOI.ConstraintDual, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.LessThan{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}, ) MOI.check_result_index_bounds(model, attr) if MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE @@ -657,7 +631,7 @@ end function MOI.get( model::Optimizer, attr::MOI.ConstraintDual, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.GreaterThan{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}, ) MOI.check_result_index_bounds(model, attr) if MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE @@ -676,7 +650,7 @@ function MOI.get( model::Optimizer, attr::MOI.ConstraintDual, c::MOI.ConstraintIndex{ - MOI.SingleVariable, + MOI.VariableIndex, <:Union{MOI.Interval{Float64},MOI.EqualTo{Float64}}, }, ) @@ -731,9 +705,8 @@ function MOI.get( status = _CLP_BASIS_STATUS[code] if status == MOI.NONBASIC_AT_UPPER || status == MOI.NONBASIC_AT_LOWER return _nonbasic_status(status, S) - else - return status end + return status end function MOI.get( diff --git a/src/precompile.jl b/src/precompile.jl deleted file mode 100644 index 3c50fa8..0000000 --- a/src/precompile.jl +++ /dev/null @@ -1,27 +0,0 @@ -function _fake_precompile() - cache = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) - clp = MOI.instantiate(Clp.Optimizer, with_bridge_type = Float64) - MOI.copy_to(clp, cache; copy_names = false) - return clp -end - -function _precompile_() - ccall(:jl_generating_output, Cint, ()) == 1 || return nothing - T = Float64 - scalar_sets = ( - MOI.LessThan{T}, - MOI.GreaterThan{T}, - MOI.EqualTo{T}, - MOI.Interval{T}, - ) - scalar_functions = (MOI.SingleVariable, MOI.ScalarAffineFunction{T}) - MOI.precompile_model( - MOI.Bridges.LazyBridgeOptimizer{MOI.Utilities.CachingOptimizer{ - Optimizer, - MOI.Utilities.UniversalFallback{MOI.Utilities.Model{Float64}}, - }}, - [(F, S) for F in scalar_functions, S in scalar_sets], - ) - @assert Base.precompile(_fake_precompile, ()) - return -end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index f8a6791..6639f71 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -22,8 +22,7 @@ function test_SolverName() end function test_supports_default_copy_to() - @test !MOI.supports_incremental_interface(Clp.Optimizer(), false) - @test !MOI.supports_incremental_interface(Clp.Optimizer(), true) + @test !MOI.supports_incremental_interface(Clp.Optimizer()) return end @@ -44,6 +43,7 @@ function test_runtests() exclude = [ # TODO(odow): bug in Clp.jl "test_model_copy_to_UnsupportedAttribute", + "test_model_ModelFilter_AbstractConstraintAttribute", # Unable to prove infeasibility "test_conic_NormInfinityCone_INFEASIBLE", "test_conic_NormOneCone_INFEASIBLE", @@ -87,6 +87,7 @@ function test_RawOptimizerAttribute() @test MOI.get(model, MOI.RawOptimizerAttribute("PresolveType")) == 1 MOI.set(model, MOI.RawOptimizerAttribute("PresolveType"), 0) @test MOI.get(model, MOI.RawOptimizerAttribute("PresolveType")) == 0 + return end function test_All_parameters() @@ -101,6 +102,7 @@ function test_All_parameters() MOI.set(model, MOI.RawOptimizerAttribute(key), value) @test MOI.get(model, MOI.RawOptimizerAttribute(key)) == value end + return end function test_copy_to_bug() @@ -116,6 +118,7 @@ function test_copy_to_bug() clp = Clp.Optimizer() index_map = MOI.copy_to(clp, model) @test index_map[con[1]] != index_map[con[2]] + return end function test_options_after_empty!() @@ -125,6 +128,7 @@ function test_options_after_empty!() @test MOI.get(model, MOI.Silent()) == true MOI.empty!(model) @test MOI.get(model, MOI.Silent()) == true + return end end # module TestMOIWrapper From 6a122394557d3a642e5b489b46e5537f4eacdc7a Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 8 Sep 2021 09:27:54 +1200 Subject: [PATCH 14/15] Update to MOI 0.10 --- Project.toml | 5 ++--- test/runtests.jl | 5 ----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Project.toml b/Project.toml index 5819e93..15b5c9a 100644 --- a/Project.toml +++ b/Project.toml @@ -15,12 +15,11 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" BinaryProvider = "0.5.9" CEnum = "0.3, 0.4" Clp_jll = "=1.17.6, ~100.1700.600" -MathOptInterface = "0.9.6" +MathOptInterface = "0.10.0" julia = "1" [extras] -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "Pkg"] +test = ["Test"] diff --git a/test/runtests.jl b/test/runtests.jl index cdaf52a..b9206bb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,8 +1,3 @@ -if get(ENV, "GITHUB_ACTIONS", "") == "true" - import Pkg - Pkg.add(Pkg.PackageSpec(name = "MathOptInterface", rev = "master")) -end - using Test @testset "MathOptInterface" begin From c1235e8257f7cbcdbb63d6db42c8b6c749c2efa8 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 8 Sep 2021 09:56:19 +1200 Subject: [PATCH 15/15] Fix formatting --- src/MOI_wrapper/MOI_wrapper.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index 30ac111..cc8c68c 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -344,10 +344,7 @@ function MOI.copy_to( return MOI.copy_to(dest, src.model) end -function MOI.copy_to( - dest::Optimizer, - src::MOI.ModelLike, -) +function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) cache = OptimizerCache() src_cache = MOI.copy_to(cache, src) cache_dest = MOI.copy_to(dest, cache)