From b03be7f4e3fc670ead3544e3bfe266dc4f71c42f Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sun, 10 Feb 2019 15:01:16 -0600 Subject: [PATCH] Add an MOI wrapper (#83) * Add an MOI wrapper * Deal with ScalarAffine constraints * Deal with MOI objectives * Add ScalarQuadraticFunction * Add comments * Test on linear and gradratic MOI tests * Update wrapper * Update show * Add MINLPTests as a build stage * Add JuMP master to MINLPTests * Add Ipopt to MINLPTests project * Put all MINLPTests in testset * Bump MINLPTests version * Update run_minlptests.jl * Doh; exclude the right test --- .travis.yml | 14 +- REQUIRE | 1 + src/AmplNLWriter.jl | 23 +- src/MOI_wrapper.jl | 495 ++++++++++++++++++++++++++++++ test/MINLPTests/Manifest.toml | 204 ++++++++++++ test/MINLPTests/Project.toml | 5 + test/MINLPTests/run_minlptests.jl | 31 ++ test/MOI_wrapper.jl | 69 +++++ test/runtests.jl | 2 + 9 files changed, 830 insertions(+), 14 deletions(-) create mode 100644 src/MOI_wrapper.jl create mode 100644 test/MINLPTests/Manifest.toml create mode 100644 test/MINLPTests/Project.toml create mode 100644 test/MINLPTests/run_minlptests.jl create mode 100644 test/MOI_wrapper.jl diff --git a/.travis.yml b/.travis.yml index b7ef106..82d478e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,11 @@ language: julia os: - linux - - osx julia: - 0.6 - 0.7 - 1.0 - - nightly -matrix: - allow_failures: - - julia: nightly + - 1.1 branches: only: - master @@ -27,3 +23,11 @@ before_cache: - cp -R $HOME/.julia/*/Ipopt/deps/usr $HOME after_success: - julia -e 'if VERSION >= v"0.7-"; using Pkg; else; cd(Pkg.dir("AmplNLWriter")); end; Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())' +jobs: + include: + - stage: "MINLPTests" + julia: 1.0 + os: linux + script: + - julia --project=test/MINLPTests -e 'using Pkg; Pkg.instantiate(); Pkg.add(PackageSpec(path=pwd()))' + - julia --project=test/MINLPTests --color=yes test/MINLPTests/run_minlptests.jl diff --git a/REQUIRE b/REQUIRE index 14f50f9..eed655a 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,3 +1,4 @@ julia 0.6 Compat 1.0 MathProgBase 0.5 0.8 +MathOptInterface 0.8.1 0.9 diff --git a/src/AmplNLWriter.jl b/src/AmplNLWriter.jl index 9640e74..a30169c 100644 --- a/src/AmplNLWriter.jl +++ b/src/AmplNLWriter.jl @@ -255,16 +255,19 @@ function SolverInterface.loadproblem!(outer::AmplNLNonlinearModel, nvar::Integer # Process objective m.obj = obj_expr(m.d) - if length(m.obj.args) < 2 - m.obj = 0 - else - # Convert non-linear expression to non-linear, linear and constant - m.obj, constant, m.objlinearity = process_expression!( - m.obj, m.lin_obj, m.varlinearities_obj) + if typeof(m.obj) <: Expr + if length(m.obj.args) < 2 + # TODO(odow): what does this case mean? + m.obj = 0 + else + # Convert non-linear expression to non-linear, linear and constant + m.obj, constant, m.objlinearity = process_expression!( + m.obj, m.lin_obj, m.varlinearities_obj) - # Add constant back into non-linear expression - if constant != 0 - m.obj = add_constant(m.obj, constant) + # Add constant back into non-linear expression + if constant != 0 + m.obj = add_constant(m.obj, constant) + end end end m @@ -791,4 +794,6 @@ function clean_solverdata() end end +include("MOI_wrapper.jl") + end diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl new file mode 100644 index 0000000..06f04a8 --- /dev/null +++ b/src/MOI_wrapper.jl @@ -0,0 +1,495 @@ +# TODO(odow): +# - remove the `try-catch`es once the following PR is merged +# https://github.com/JuliaOpt/MathOptInterface.jl/pull/623 + +using MathOptInterface +const MOI = MathOptInterface +const MOIU = MOI.Utilities + +import MathProgBase +const MPB = MathProgBase.SolverInterface + +MOIU.@model(InnerModel, + (MOI.ZeroOne, MOI.Integer), + (MOI.EqualTo, MOI.GreaterThan, MOI.LessThan, MOI.Interval), + (), + (), + (MOI.SingleVariable,), + (MOI.ScalarAffineFunction, MOI.ScalarQuadraticFunction), + (), + () +) + +const MOI_SCALAR_SETS = ( + MOI.EqualTo{Float64}, MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, + MOI.Interval{Float64} +) + +const MOI_SCALAR_FUNCTIONS = ( + MOI.ScalarAffineFunction{Float64}, MOI.ScalarQuadraticFunction{Float64} +) + +# Make `Model` a constant for use in the rest of the file. +const Model = MOIU.UniversalFallback{InnerModel{Float64}} + +# Only support the constraint types defined by `InnerModel`. +function MOI.supports_constraint(model::Model, F::Type{<:MOI.AbstractFunction}, + S::Type{<:MOI.AbstractSet}) + return MOI.supports_constraint(model.model, F, S) +end + +"Attribute for the MathProgBase solver." +struct MPBSolver <: MOI.AbstractOptimizerAttribute end + +"Attribute for the MathProgBase status." +struct MPBSolutionAttribute <: MOI.AbstractModelAttribute end + +"Struct to contain the MPB solution." +struct MPBSolution + status::Symbol + objective_value::Float64 + primal_solution::Dict{MOI.VariableIndex, Float64} +end + +""" + Optimizer( + solver_command::String, + options::Vector{String} = String[]; + filename::String = "" + ) + +# Example + + Optimizer(Ipopt.amplexe, ["print_level=0"]) +""" +function Optimizer( + solver_command::String, + options::Vector{String} = String[]; + filename::String = "") + model = MOIU.UniversalFallback(InnerModel{Float64}()) + MOI.set(model, MPBSolver(), + AmplNLSolver(solver_command, options, filename = filename) + ) + return model +end + +Base.show(io::IO, ::Model) = print(io, "An AmplNLWriter model") + +MOI.get(::Model, ::MOI.SolverName) = "AmplNLWriter" + +# We re-define is_empty and empty! to prevent the universal fallback from +# deleting the solver that we are caching in it. +function MOI.is_empty(model::Model) + return MOI.is_empty(model.model) && + isempty(model.constraints) && + isempty(model.modattr) && + isempty(model.varattr) && + isempty(model.conattr) && + length(model.optattr) == 1 && + haskey(model.optattr, MPBSolver()) +end + +function MOI.empty!(model::Model) + MOI.empty!(model.model) + empty!(model.constraints) + model.nextconstraintid = 0 + empty!(model.con_to_name) + model.name_to_con = nothing + empty!(model.modattr) + empty!(model.varattr) + empty!(model.conattr) + mpb_solver = model.optattr[MPBSolver()] + empty!(model.optattr) + model.optattr[MPBSolver()] = mpb_solver + return +end + +set_to_bounds(set::MOI.LessThan) = (-Inf, set.upper) +set_to_bounds(set::MOI.GreaterThan) = (set.lower, Inf) +set_to_bounds(set::MOI.EqualTo) = (set.value, set.value) +set_to_bounds(set::MOI.Interval) = (set.lower, set.upper) +set_to_cat(set::MOI.ZeroOne) = :Bin +set_to_cat(set::MOI.Integer) = :Int + +struct NLPEvaluator{T} <: MPB.AbstractNLPEvaluator + inner::T + variable_map::Dict{MOI.VariableIndex, Int} + num_inner_con::Int + objective_expr::Union{Nothing, Expr} + scalar_constraint_expr::Vector{Expr} +end + +""" +MathProgBase expects expressions with variables denoted by `x[i]` for contiguous +`i`. However, JuMP 0.19 creates expressions with `x[MOI.VariableIndex(i)]`. So +we have to recursively walk the expression replacing instances of +MOI.VariableIndex by a corresponding integer. +""" +function replace_variableindex_by_int(variable_map, expr::Expr) + for (i, arg) in enumerate(expr.args) + expr.args[i] = replace_variableindex_by_int(variable_map, arg) + end + return expr +end +function replace_variableindex_by_int(variable_map, expr::MOI.VariableIndex) + return variable_map[expr] +end +replace_variableindex_by_int(variable_map, expr) = expr + +# ============================================================================== +# In the next section we match up the MPB nonlinear functions to the MOI API. +# This is basically just a rename. +function MPB.initialize(d::NLPEvaluator, requested_features::Vector{Symbol}) + if d.inner !== nothing + MOI.initialize(d.inner, requested_features) + end + return +end + +function MPB.features_available(d::NLPEvaluator) + if d.inner !== nothing + return MOI.features_available(d.inner) + else + return [:ExprGraph] + end +end + +function MPB.obj_expr(d::NLPEvaluator) + # d.objective_expr is a SingleVariable, ScalarAffineFunction, or a + # ScalarQuadraticFunction from MOI. If it is unset, it will be `nothing` (we + # enforce this when creating the NLPEvaluator in `optimize!`). + if d.objective_expr !== nothing + return d.objective_expr + elseif d.inner !== nothing && d.inner.has_nlobj + # If d.objective_expr === nothing, then the objective must be nonlinear. + # Query it from the inner NLP evaluator. + expr = MOI.objective_expr(d.inner) + return replace_variableindex_by_int(d.variable_map, expr) + else + return :(0.0) + end +end + +function MPB.constr_expr(d::NLPEvaluator, i) + # There are two types of constraints in the model: + # i = 1, 2, ..., d.num_inner_con are the nonlinear constraints. We access + # these from the inner NLP evaluator. + # i = d.num_inner_con + 1, d.num_inner_con + 2, ..., N are the linear or + # quadratic constraints added by MOI. + if i <= d.num_inner_con + expr = MOI.constraint_expr(d.inner, i) + return replace_variableindex_by_int(d.variable_map, expr) + else + return d.scalar_constraint_expr[i - d.num_inner_con] + end +end + +# ============================================================================== +# In the next section, we need to convert MOI functions and sets into expression +# graphs. First, we convert functions (SingleVariable, ScalarAffine, and +# ScalarQuadratic) into expression graphs. +function func_to_expr_graph(func::MOI.SingleVariable, variable_map) + return Expr(:ref, :x, variable_map[func.variable]) +end + +function func_to_expr_graph(func::MOI.ScalarAffineFunction, variable_map) + expr = Expr(:call, :+, func.constant) + for term in func.terms + push!(expr.args, Expr(:call, :*, term.coefficient, + Expr(:ref, :x, variable_map[term.variable_index]) + )) + end + return expr +end + +function func_to_expr_graph(func::MOI.ScalarQuadraticFunction, variable_map) + expr = Expr(:call, :+, func.constant) + for term in func.affine_terms + push!(expr.args, Expr(:call, :*, term.coefficient, + Expr(:ref, :x, variable_map[term.variable_index]) + )) + end + for term in func.quadratic_terms + index_1 = variable_map[term.variable_index_1] + index_2 = variable_map[term.variable_index_2] + coef = term.coefficient + # MOI defines quadratic as 1/2 x' Q x :( + if index_1 == index_2 + coef *= 0.5 + end + push!(expr.args, Expr(:call, :*, coef, + Expr(:ref, :x, index_1), + Expr(:ref, :x, index_2) + )) + end + return expr +end + +# Next, we need to combine a function `Expr` with a MOI set into a comparison. + +function funcset_to_expr_graph(func::Expr, set::MOI.LessThan) + return Expr(:call, :<=, func, set.upper) +end + +function funcset_to_expr_graph(func::Expr, set::MOI.GreaterThan) + return Expr(:call, :>=, func, set.lower) +end + +function funcset_to_expr_graph(func::Expr, set::MOI.EqualTo) + return Expr(:call, :(==), func, set.value) +end + +function funcset_to_expr_graph(func::Expr, set::MOI.Interval) + return Expr(:comparison, set.lower, :<=, func, :<=, set.upper) +end + +# A helper function that converts a MOI function and MOI set into a comparison +# expression. + +function moi_to_expr_graph(func, set, variable_map) + func_expr = func_to_expr_graph(func, variable_map) + return funcset_to_expr_graph(func_expr, set) +end + +# ============================================================================== +# Now we're ready to optimize model. +function MOI.optimize!(model::Model) + # Extract the MathProgBase solver from the model. Recall it's a MOI + # attribute which we're careful not to delete on calls to `empty!`. + mpb_solver = MOI.get(model, MPBSolver()) + + # Get the optimzation sense. + opt_sense = MOI.get(model, MOI.ObjectiveSense()) + sense = opt_sense == MOI.MAX_SENSE ? :Max : :Min + + # Get the NLPBlock from the model. + nlp_block = try + MOI.get(model, MOI.NLPBlock()) + catch + nothing + end + + # ========================================================================== + # Extract the nonlinear constraint bounds. We're going to append to these + # g_l and g_u vectors later. + num_con = 0 + moi_nlp_evaluator = nlp_block !== nothing ? nlp_block.evaluator : nothing + if nlp_block !== nothing + num_con += length(nlp_block.constraint_bounds) + end + g_l = fill(-Inf, num_con) + g_u = fill(Inf, num_con) + if nlp_block !== nothing + for (i, bound) in enumerate(nlp_block.constraint_bounds) + g_l[i] = bound.lower + g_u[i] = bound.upper + end + end + + # ========================================================================== + # Intialize the variables. We need to form a mapping between the MOI + # VariableIndex and an Int in order to replace instances of + # `x[VariableIndex]` with `x[i]` in the expression graphs. + variables = MOI.get(model, MOI.ListOfVariableIndices()) + num_var = length(variables) + variable_map = Dict{MOI.VariableIndex, Int}() + for (i, variable) in enumerate(variables) + variable_map[variable] = i + end + + # ========================================================================== + # Extract variable bounds. + x_l = fill(-Inf, num_var) + x_u = fill(Inf, num_var) + for set_type in MOI_SCALAR_SETS + for c_ref in MOI.get(model, + MOI.ListOfConstraintIndices{MOI.SingleVariable, set_type}()) + c_func = MOI.get(model, MOI.ConstraintFunction(), c_ref) + c_set = MOI.get(model, MOI.ConstraintSet(), c_ref) + v_index = variable_map[c_func.variable] + lower, upper = set_to_bounds(c_set) + # Note that there might be multiple bounds on the same variable + # (e.g., LessThan and GreaterThan), so we should only update bounds + # if they differ from the defaults. + if lower > -Inf + x_l[v_index] = lower + end + if upper < Inf + x_u[v_index] = upper + end + end + end + + # ========================================================================== + # We have to convert all ScalarAffineFunction-in-Set constraints to an + # expression graph. + scalar_constraint_expr = Expr[] + for func_type in MOI_SCALAR_FUNCTIONS + for set_type in MOI_SCALAR_SETS + for c_ref in MOI.get(model, MOI.ListOfConstraintIndices{ + func_type, set_type}()) + c_func = MOI.get(model, MOI.ConstraintFunction(), c_ref) + c_set = MOI.get(model, MOI.ConstraintSet(), c_ref) + expr = moi_to_expr_graph(c_func, c_set, variable_map) + push!(scalar_constraint_expr, expr) + lower, upper = set_to_bounds(c_set) + push!(g_l, lower) + push!(g_u, upper) + end + end + end + + # ========================================================================== + # MOI objective + obj_type = MOI.get(model, MOI.ObjectiveFunctionType()) + obj_func = MOI.get(model, MOI.ObjectiveFunction{obj_type}()) + obj_func_expr = func_to_expr_graph(obj_func, variable_map) + if obj_func_expr == :(+ 0.0) + obj_func_expr = nothing + end + + # ========================================================================== + # Build the nlp_evaluator + nlp_evaluator = NLPEvaluator(moi_nlp_evaluator, variable_map, num_con, + obj_func_expr, scalar_constraint_expr) + + # ========================================================================== + # Create the MathProgBase model. Note that we pass `num_con` and the number + # of linear constraints. + mpb_model = MPB.NonlinearModel(mpb_solver) + MPB.loadproblem!(mpb_model, num_var, + num_con + length(scalar_constraint_expr), x_l, x_u, g_l, g_u, sense, + nlp_evaluator) + + # ========================================================================== + # Set any variables to :Bin if they are in ZeroOne and :Int if they are + # Integer. The default is just :Cont. + x_cat = fill(:Cont, num_var) + for set_type in (MOI.ZeroOne, MOI.Integer) + for c_ref in MOI.get(model, + MOI.ListOfConstraintIndices{MOI.SingleVariable, set_type}()) + c_func = MOI.get(model, MOI.ConstraintFunction(), c_ref) + c_set = MOI.get(model, MOI.ConstraintSet(), c_ref) + v_index = variable_map[c_func.variable] + x_cat[v_index] = set_to_cat(c_set) + end + end + MPB.setvartype!(mpb_model, x_cat) + + # ========================================================================== + # Set the VariablePrimalStart attributes for variables. + variable_primal_start = fill(0.0, num_var) + for (i, variable) in enumerate(variables) + try + start_val = MOI.get(model, MOI.VariablePrimalStart(), variable) + if start_val !== nothing + variable_primal_start[i] = start_val + end + catch + end + end + MPB.setwarmstart!(mpb_model, variable_primal_start) + + # ========================================================================== + # Set the VariablePrimalStart attributes for variables. + MPB.optimize!(mpb_model) + + # ========================================================================== + # Extract and save the MathProgBase solution. + primal_solution = Dict{MOI.VariableIndex, Float64}() + for (variable, sol) in zip(variables, MPB.getsolution(mpb_model)) + primal_solution[variable] = sol + end + mpb_solution = MPBSolution( + MPB.status(mpb_model), + MPB.getobjval(mpb_model), + primal_solution + ) + MOI.set(model, MPBSolutionAttribute(), mpb_solution) + return +end + +# ============================================================================== +# MOI accessors for the solution info. + +function MOI.get(model::Model, ::MOI.VariablePrimal, var::MOI.VariableIndex) + mpb_solution = try + MOI.get(model, MPBSolutionAttribute()) + catch + nothing + end + if mpb_solution === nothing + return nothing + end + return mpb_solution.primal_solution[var] +end + +function MOI.get(model::Model, ::MOI.ConstraintPrimal, idx::MOI.ConstraintIndex) + return MOIU.get_fallback(model, MOI.ConstraintPrimal(), idx) +end + +function MOI.get(model::Model, ::MOI.ObjectiveValue) + mpb_solution = try + MOI.get(model, MPBSolutionAttribute()) + catch + nothing + end + if mpb_solution === nothing + return nothing + end + return mpb_solution.objective_value +end + +function MOI.get(model::Model, ::MOI.TerminationStatus) + mpb_solution = try + MOI.get(model, MPBSolutionAttribute()) + catch + nothing + end + if mpb_solution === nothing + return MOI.OPTIMIZE_NOT_CALLED + end + status = mpb_solution.status + if status == :Optimal + # TODO(odow): this is not always the case. What if Ipopt solves a + # convex problem? + return MOI.LOCALLY_SOLVED + elseif status == :Infeasible + return MOI.INFEASIBLE + elseif status == :Unbounded + return MOI.DUAL_INFEASIBLE + elseif status == :UserLimit + return MOI.OTHER_LIMIT + elseif status == :Error + return MOI.OTHER_ERROR + end + return MOI.OTHER_ERROR +end + +function MOI.get(model::Model, ::MOI.PrimalStatus) + mpb_solution = try + MOI.get(model, MPBSolutionAttribute()) + catch + nothing + end + if mpb_solution === nothing + return MOI.NO_SOLUTION + end + status = mpb_solution.status + if status == :Optimal + return MOI.FEASIBLE_POINT + end + return MOI.NO_SOLUTION +end + +function MOI.get(model::Model, ::MOI.DualStatus) + return MOI.NO_SOLUTION +end + +function MOI.get(model::Model, ::MOI.ResultCount) + if MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + return 1 + else + return 0 + end +end diff --git a/test/MINLPTests/Manifest.toml b/test/MINLPTests/Manifest.toml new file mode 100644 index 0000000..3eca974 --- /dev/null +++ b/test/MINLPTests/Manifest.toml @@ -0,0 +1,204 @@ +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[BinDeps]] +deps = ["Compat", "Libdl", "SHA", "URIParser"] +git-tree-sha1 = "12093ca6cdd0ee547c39b1870e0c9c3f154d9ca9" +uuid = "9e28174c-4ba2-5203-b857-d8d62c4213ee" +version = "0.8.10" + +[[BinaryProvider]] +deps = ["Libdl", "Pkg", "SHA", "Test"] +git-tree-sha1 = "055eb2690182ebc31087859c3dd8598371d3ef9e" +uuid = "b99e7846-7c00-51b0-8f62-c81ae34c0232" +version = "0.5.3" + +[[Calculus]] +deps = ["Compat"] +git-tree-sha1 = "f60954495a7afcee4136f78d1d60350abd37a409" +uuid = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +version = "0.4.1" + +[[CommonSubexpressions]] +deps = ["Test"] +git-tree-sha1 = "efdaf19ab11c7889334ca247ff4c9f7c322817b0" +uuid = "bbf7d656-a473-5ed7-a52c-81e309532950" +version = "0.2.0" + +[[Compat]] +deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"] +git-tree-sha1 = "49269e311ffe11ac5b334681d212329002a9832a" +uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" +version = "1.5.1" + +[[DataStructures]] +deps = ["InteractiveUtils", "OrderedCollections", "Random", "Serialization", "Test"] +git-tree-sha1 = "ca971f03e146cf144a9e2f2ce59674f5bf0e8038" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.15.0" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[DelimitedFiles]] +deps = ["Mmap"] +uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" + +[[DiffResults]] +deps = ["Compat", "StaticArrays"] +git-tree-sha1 = "db8acf46717b13d6c48deb7a12007c7f85a70cf7" +uuid = "163ba53b-c6d8-5494-b064-1a9d43ac40c5" +version = "0.0.3" + +[[DiffRules]] +deps = ["Random", "Test"] +git-tree-sha1 = "09d69da75967ec48a8b1ad0897ec9144ee052bf9" +uuid = "b552c78f-8df3-52c6-915a-8e097449b14b" +version = "0.0.8" + +[[Distributed]] +deps = ["LinearAlgebra", "Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[ForwardDiff]] +deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "InteractiveUtils", "LinearAlgebra", "NaNMath", "Random", "SparseArrays", "SpecialFunctions", "StaticArrays", "Test"] +git-tree-sha1 = "e393bd3b9102659fb24fe88caedec41f2bc2e7de" +uuid = "f6369f11-7733-5829-9624-2563aa707210" +version = "0.10.2" + +[[InteractiveUtils]] +deps = ["LinearAlgebra", "Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[Ipopt]] +deps = ["BinaryProvider", "Compat", "Libdl", "MathOptInterface", "MathProgBase"] +git-tree-sha1 = "ea845abc09b752ad052c74aba786c20931ee5970" +uuid = "b6b21f68-93f8-5de0-b562-5493be1d77c9" +version = "0.5.3" + +[[JuMP]] +deps = ["Calculus", "DataStructures", "ForwardDiff", "LinearAlgebra", "MathOptInterface", "NaNMath", "Random", "SparseArrays", "Statistics", "Test"] +git-tree-sha1 = "fb4c9d5a3cf974859c2090eb3b71d06d948307af" +repo-rev = "master" +repo-url = "https://github.com/JuliaOpt/JuMP.jl.git" +uuid = "4076af6c-e467-56ae-b986-b466b2749572" +version = "0.18.5+" + +[[LibGit2]] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[LinearAlgebra]] +deps = ["Libdl"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[MINLPTests]] +deps = ["JuMP", "Test"] +git-tree-sha1 = "c306fc983902bed95f3e7628c38c1309132777d9" +repo-rev = "od/moi" +repo-url = "https://github.com/lanl-ansi/MINLPTests.jl" +uuid = "c91edd20-2690-11e9-21e6-495b70d5f171" +version = "0.1.0" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[MathOptInterface]] +deps = ["Compat", "Unicode"] +git-tree-sha1 = "6f3df4b5346485b299ae5d2b313ecb428bb46bb9" +uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +version = "0.8.1" + +[[MathProgBase]] +deps = ["Compat"] +git-tree-sha1 = "3bf2e534e635df810e5f4b4f1a8b6de9004a0d53" +uuid = "fdba3010-5040-5b88-9595-932c9decdf73" +version = "0.7.7" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[NaNMath]] +deps = ["Compat"] +git-tree-sha1 = "ce3b85e484a5d4c71dd5316215069311135fa9f2" +uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +version = "0.3.2" + +[[OrderedCollections]] +deps = ["Random", "Serialization", "Test"] +git-tree-sha1 = "85619a3f3e17bb4761fe1b1fd47f0e979f964d5b" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.0.2" + +[[Pkg]] +deps = ["Dates", "LibGit2", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[SharedArrays]] +deps = ["Distributed", "Mmap", "Random", "Serialization"] +uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[SparseArrays]] +deps = ["LinearAlgebra", "Random"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[SpecialFunctions]] +deps = ["BinDeps", "BinaryProvider", "Libdl", "Test"] +git-tree-sha1 = "0b45dc2e45ed77f445617b99ff2adf0f5b0f23ea" +uuid = "276daf66-3868-5448-9aa4-cd146d93841b" +version = "0.7.2" + +[[StaticArrays]] +deps = ["InteractiveUtils", "LinearAlgebra", "Random", "Statistics", "Test"] +git-tree-sha1 = "1eb114d6e23a817cd3e99abc3226190876d7c898" +uuid = "90137ffa-7385-5640-81b9-e52037218182" +version = "0.10.2" + +[[Statistics]] +deps = ["LinearAlgebra", "SparseArrays"] +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[Test]] +deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[URIParser]] +deps = ["Test", "Unicode"] +git-tree-sha1 = "6ddf8244220dfda2f17539fa8c9de20d6c575b69" +uuid = "30578b45-9adc-5946-b283-645ec420af67" +version = "0.4.0" + +[[UUIDs]] +deps = ["Random"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/test/MINLPTests/Project.toml b/test/MINLPTests/Project.toml new file mode 100644 index 0000000..7a02ca1 --- /dev/null +++ b/test/MINLPTests/Project.toml @@ -0,0 +1,5 @@ +[deps] +Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +MINLPTests = "c91edd20-2690-11e9-21e6-495b70d5f171" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/MINLPTests/run_minlptests.jl b/test/MINLPTests/run_minlptests.jl new file mode 100644 index 0000000..176b2b9 --- /dev/null +++ b/test/MINLPTests/run_minlptests.jl @@ -0,0 +1,31 @@ +using MINLPTests, JuMP, Ipopt, AmplNLWriter, Test + +const OPTIMIZER = MINLPTests.JuMP.with_optimizer( + AmplNLWriter.Optimizer, Ipopt.amplexe, ["print_level=0"] +) + +@testset "MINLPTests" begin + ### + ### src/nlp tests. + ### + + MINLPTests.test_nlp(OPTIMIZER, exclude = [ + "005_011", # Uses the function `\` + "006_010", # User-defined function + "007_010" # Infeasible model + ], objective_tol = 1e-5, primal_tol = 1e-5, dual_tol = NaN) + + @testset "nlp_007_010" begin + MINLPTests.nlp_007_010(OPTIMIZER, 1e-5, NaN, NaN; + termination_target = MOI.INFEASIBLE, + primal_target = MOI.NO_SOLUTION) + end + + ### + ### src/nlp-cvx tests. + ### + + MINLPTests.test_nlp_cvx(OPTIMIZER, exclude = [ + "109_010" # Ipopt fails to converge + ]) +end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl new file mode 100644 index 0000000..ed339d1 --- /dev/null +++ b/test/MOI_wrapper.jl @@ -0,0 +1,69 @@ +using AmplNLWriter, Ipopt + +import MathOptInterface +const MOI = MathOptInterface +const MOIT = MOI.Test + +const OPTIMIZER = AmplNLWriter.Optimizer(Ipopt.amplexe, ["print_level = 0"]) + +@test sprint(show, OPTIMIZER) == "An AmplNLWriter model" + +const CONFIG = MOIT.TestConfig( + atol = 1e-4, rtol = 1e-4, optimal_status = MOI.LOCALLY_SOLVED, + infeas_certificates = false, duals = false +) + +@testset "Unit Tests" begin + MOIT.unittest(OPTIMIZER, CONFIG, [ + "solve_objbound_edge_cases", # ObjectiveBound not implemented + "solve_integer_edge_cases", # Ipopt doesn't handle integer + "solve_affine_deletion_edge_cases", # VectorAffineFunction + # It seems that the AMPL NL reader declares NL files with no objective + # and no constraints as corrupt, even if they have variable bounds. Yuk. + "solve_blank_obj" + ]) +end + +@testset "Linear tests" begin + MOIT.contlineartest(OPTIMIZER, CONFIG, [ + "linear7", "linear15" # VectorAffineFunction + ]) +end + +@testset "Quadratic tests" begin + MOIT.contquadratictest(OPTIMIZER, CONFIG, [ + "qcp1" # VectorAffineFunction + ]) +end + +@testset "ModelLike tests" begin + @test MOI.get(OPTIMIZER, MOI.SolverName()) == "AmplNLWriter" + @testset "default_objective_test" begin + MOIT.default_objective_test(OPTIMIZER) + end + @testset "default_status_test" begin + MOIT.default_status_test(OPTIMIZER) + end + @testset "nametest" begin + MOIT.nametest(OPTIMIZER) + end + @testset "validtest" begin + MOIT.validtest(OPTIMIZER) + end + @testset "emptytest" begin + # Requires VectorOfVariables + # MOIT.emptytest(OPTIMIZER) + end + @testset "orderedindicestest" begin + MOIT.orderedindicestest(OPTIMIZER) + end + @testset "copytest" begin + # Requires VectorOfVariables + # MOIT.copytest(OPTIMIZER, AmplNLWriter.Optimizer(Ipopt.amplexe)) + end +end + +@testset "MOI NLP tests" begin + # Requires ExprGraph in MOI tests + # MOIT.nlptest(OPTIMIZER, CONFIG) +end diff --git a/test/runtests.jl b/test/runtests.jl index 1ca10fe..9a05be0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,6 +2,8 @@ using AmplNLWriter, JuMP, Ipopt using Compat using Compat.Test +include("MOI_wrapper.jl") + include("nl_convert.jl") include("nl_linearity.jl") include("nl_write.jl")