diff --git a/.gitignore b/.gitignore index 1a7eb04..3ea6f07 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ Manifest.toml *.sublime-workspace *.bat *.sublime-project +*.mof.json +*.prt +*.txt diff --git a/Project.toml b/Project.toml index 6cf9a57..cc6a143 100644 --- a/Project.toml +++ b/Project.toml @@ -1,21 +1,13 @@ name = "QuadraticToBinary" uuid = "014a38d5-7acb-4e20-b6c0-4fe5c2344fd1" authors = ["Joaquim Garcia "] -version = "0.3.0" +version = "0.4.0" [deps] MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" [compat] -MathOptInterface = "0.10.6" +MathOptInterface = "1" DataStructures = "0.17.10, 0.18.0" -julia = "1" - -[extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" - -[targets] -test = ["Test", "Cbc"] - +julia = "1.6" diff --git a/README.md b/README.md index 7d26b97..3adaa9b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Non-convex quadratic programs are extremely hard to solve. This problem class ca be solved by Global Solvers such as [Couenne](https://projects.coin-or.org/Couenne). Another possibility is to rely on binary expasion of products terms that appear in the problem, in this case the problem is *approximated* and can be solved by off-the-shelf -MIP solvers such as [Cbc](https://github.com/JuliaOpt/Cbc.jl), [CPLEX](https://github.com/JuliaOpt/CPLEX.jl), [GLPK](https://github.com/JuliaOpt/GLPK.jl), [Gurobi](https://github.com/JuliaOpt/Gurobi.jl), [Xpress](https://github.com/JuliaOpt/Xpress.jl). +MIP solvers such as [Cbc](https://github.com/jump-dev/Cbc.jl), [CPLEX](https://github.com/jump-dev/CPLEX.jl), [GLPK](https://github.com/jump-dev/GLPK.jl), [Gurobi](https://github.com/jump-dev/Gurobi.jl), [HiGHS](https://github.com/jump-dev/HiGHS.jl), [Xpress](https://github.com/jump-dev/Xpress.jl). ## Example @@ -77,15 +77,15 @@ objective_value(model) # ≈ 9.0 @assert value(c) ≈ 4.0 ``` -### MathOptInterface with Cbc solver +### MathOptInterface with HiGHS solver ```julia using MathOptInterface using QuadraticToBinary const MOI = MathOptInterface -using Cbc +using HiGHS -optimizer = MOI.instantiate(Cbc.Optimizer, with_bridge_type = Float64) +optimizer = MOI.instantiate(HiGHS.Optimizer, with_bridge_type = Float64) model = QuadraticToBinary.Optimizer{Float64}(optimizer) @@ -118,7 +118,8 @@ MOI.optimize!(model) @assert MOI.get(model, MOI.ConstraintPrimal(), c) ≈ 4.0 ``` -Note that duals are not available because the problem was approximated as a MIP. +Note: +that duals are not available because the problem was approximated as a MIP. ## QuadraticToBinary.Optimizer Attributes @@ -140,6 +141,16 @@ MOI.set(model, QuadraticToBinary.VariablePrecision(), vi, val) The precision for each varible will be `val * (UB - LB)`. Where `UB` and `LB` are, respectively, the upper and lower bound of the variable. +Note: +binary expansion problem can be numerically challenging for high precision. You +might need to modify solver options accordingly. In the case of HiGHS: + +```julia +tol = 1e-9 +MOI.set(highs_high_tol, MOI.RawOptimizerAttribute("mip_feasibility_tolerance"), tol) +MOI.set(highs_high_tol, MOI.RawOptimizerAttribute("primal_feasibility_tolerance"), tol) +``` + ### Bounds For the sake of simplicity, the following two attributes are made available: diff --git a/src/moi.jl b/src/moi.jl index 9357eba..3107620 100644 --- a/src/moi.jl +++ b/src/moi.jl @@ -229,7 +229,7 @@ end # MOI.supports(model::Optimizer, args...) = MOI.supports(model.optimizer, args...) -# TODO, call this on inner solver +# Q2B is always incremental because it uses add_constraint to cacehc onversions MOI.supports_incremental_interface(::Optimizer) = true function MOI.supports(model::Optimizer, attr::MOI.VariableName, tp::Type{MOI.VariableIndex}) @@ -270,6 +270,7 @@ function MOI.set(model::Optimizer, attr::Union{ MOI.Silent, MOI.NumberOfThreads, MOI.TimeLimitSec, + MOI.RawOptimizerAttribute, }, val ) return MOI.set(model.optimizer, attr, val) @@ -307,8 +308,8 @@ quadratic_type(::Type{MOI.VectorAffineFunction{T}}) where T = MOI.VectorQuadrati function MOI.get(model::Optimizer, ::MOI.SolverName) - return "Binary reformulation of quadratic model with solver " * - MOI.get(model.optimizer, MOI.SolverName()) * " attached" + return "QuadraticToBinary [" * + MOI.get(model.optimizer, MOI.SolverName()) * "]" end function MOI.supports_add_constrained_variables( @@ -340,20 +341,8 @@ function MOI.supports_add_constrained_variables( return MOI.supports_add_constrained_variables(model.optimizer, S) end -# function MOI.set(model::Optimizer, param::MOI.RawOptimizerAttribute, value) -# # if in a subset of the q2b save it -# # otherwise pass it forward -# end - -# function MOI.get(model::Optimizer, param::MOI.RawOptimizerAttribute) -# end - -function MOI.Utilities.supports_default_copy_to(model::Optimizer, val::Bool) - return true # val # MOI.Utilities.supports_default_copy_to(model.optimizer, val) -end - -function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kwargs...) - return MOI.Utilities.automatic_copy_to(dest, src; kwargs...) +function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) + return MOI.Utilities.default_copy_to(dest, src) end @@ -989,25 +978,14 @@ end function delete_additional_constraints(model) ch = model.index_cache - if true - MOI.delete(model.optimizer, ch.sa_eq) - MOI.delete(model.optimizer, ch.sa_lt) - MOI.delete(model.optimizer, ch.sa_gt) - MOI.delete(model.optimizer, ch.vi) - else - for ci in ch.sa_eq - MOI.delete(model.optimizer, ci) - end - for ci in ch.sa_lt - MOI.delete(model.optimizer, ci) - end - for ci in ch.sa_gt - MOI.delete(model.optimizer, ci) - end - for vi in ch.vi - MOI.delete(model.optimizer, vi) - end - end + MOI.delete(model.optimizer, ch.sa_eq) + MOI.delete(model.optimizer, ch.sa_lt) + MOI.delete(model.optimizer, ch.sa_gt) + MOI.delete(model.optimizer, ch.sv_zo) + MOI.delete(model.optimizer, ch.sv_lt) + MOI.delete(model.optimizer, ch.sv_gt) + MOI.delete(model.optimizer, ch.vi) + return nothing end function lower(model, info) @@ -1481,7 +1459,8 @@ function MOI.set( ::GlobalVariablePrecision, value::Union{Nothing, Float64} ) - val = value === nothing ? NaN : value + # default global precision is 1e-4 + val = value === nothing ? 1e-4 : value @assert 0 < val <= 1 model.global_initial_precision = val return diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..54bbab4 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,7 @@ +[deps] +HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +HiGHS = "1.1.0" \ No newline at end of file diff --git a/test/moi.jl b/test/moi.jl index 2728218..c11044d 100644 --- a/test/moi.jl +++ b/test/moi.jl @@ -1,12 +1,11 @@ -function ncqcp1test_mod(model::MOI.ModelLike, config::MOIT.Config) +function ncqcp1test_mod(model::MOI.ModelLike, config) atol = config.atol rtol = config.rtol # Max 2x + y # s.t. x * y <= 4 (c) # x, y >= 1 - # @test MOIU.supports_default_copy_to(model, #=copy_names=# false) @test MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) @test MOI.supports_constraint(model, MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}) @@ -35,38 +34,34 @@ function ncqcp1test_mod(model::MOI.ModelLike, config::MOIT.Config) MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE - if config.query - @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) - end + @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) - if config.solve - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) + MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 9.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ObjectiveValue()) ≈ 9.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 4.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), y) ≈ 1.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 4.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), y) ≈ 1.0 atol=atol rtol=rtol + + @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ 4.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), vc1) ≈ 4.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), vc2) ≈ 1.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ 4.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), vc1) ≈ 4.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), vc2) ≈ 1.0 atol=atol rtol=rtol - end end -function ncqcp1test_mod2(model::MOI.ModelLike, config::MOIT.Config) +function ncqcp1test_mod2(model::MOI.ModelLike, config) atol = config.atol rtol = config.rtol # Max 2x + y # s.t. x * y <= 4 (c) # x, y >= 1 - # @test MOIU.supports_default_copy_to(model, #=copy_names=# false) @test MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) @test MOI.supports_constraint(model, MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}) @@ -102,31 +97,28 @@ function ncqcp1test_mod2(model::MOI.ModelLike, config::MOIT.Config) MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE - if config.query - @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) - end + @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) - if config.solve - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) + MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 9.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ObjectiveValue()) ≈ 9.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 4.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), y) ≈ 1.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 4.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), y) ≈ 1.0 atol=atol rtol=rtol + + @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ 4.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), vc1) ≈ 4.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), vc2) ≈ 1.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ 4.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), vc1) ≈ 4.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), vc2) ≈ 1.0 atol=atol rtol=rtol - end end -function ncqcp2test_mod(model::MOI.ModelLike, config::MOIT.Config) +function ncqcp2test_mod(model::MOI.ModelLike, config) atol = config.atol rtol = config.rtol # Find x, y @@ -134,7 +126,6 @@ function ncqcp2test_mod(model::MOI.ModelLike, config::MOIT.Config) # x * x == 4 (c2) # x, y >= 0 - # @test MOIU.supports_default_copy_to(model, #=copy_names=# false) @test MOI.supports_constraint(model, MOI.ScalarQuadraticFunction{Float64}, MOI.EqualTo{Float64}) MOI.empty!(model) @@ -164,31 +155,28 @@ function ncqcp2test_mod(model::MOI.ModelLike, config::MOIT.Config) @test MOI.get(model, MOI.NumberOfConstraints{MOI.ScalarQuadraticFunction{Float64}, MOI.EqualTo{Float64}}()) == 2 - if config.query - @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) - @test cf2 ≈ MOI.get(model, MOI.ConstraintFunction(), c2) - end + @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) + @test cf2 ≈ MOI.get(model, MOI.ConstraintFunction(), c2) - if config.solve - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) + MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 2.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), y) ≈ 2.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 2.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), y) ≈ 2.0 atol=atol rtol=rtol + + @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ 4.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), c2) ≈ 4.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), vc1) ≈ 2.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), vc2) ≈ 2.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ 4.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), c2) ≈ 4.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), vc1) ≈ 2.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), vc2) ≈ 2.0 atol=atol rtol=rtol - end end -function qp1test_mod(model::MOI.ModelLike, config::MOIT.Config) +function qp1test_mod(model::MOI.ModelLike, config) atol = config.atol rtol = config.rtol # homogeneous quadratic objective @@ -197,7 +185,6 @@ function qp1test_mod(model::MOI.ModelLike, config::MOIT.Config) # x + y >= 1 (c2) # x, y, z \in R - @test MOIU.supports_default_copy_to(model, #=copy_names=# false) MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}()) MOI.supports_constraint(model, MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) @@ -237,40 +224,36 @@ function qp1test_mod(model::MOI.ModelLike, config::MOIT.Config) obj = 1.0 * x * x + 1.0 * x * y + 1.0 * y * y + 1.0 * y * z + 1.0 * z * z MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj) - if config.query - @test obj ≈ MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}()) + @test obj ≈ MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}()) - @test cf1 ≈ MOI.get(model, MOI.ConstraintFunction(), c1) + @test cf1 ≈ MOI.get(model, MOI.ConstraintFunction(), c1) - @test MOI.GreaterThan(4.0) == MOI.get(model, MOI.ConstraintSet(), c1) - end + @test MOI.GreaterThan(4.0) == MOI.get(model, MOI.ConstraintSet(), c1) - if config.solve - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + MOI.optimize!(model) - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 13/7 atol=atol rtol=rtol + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.VariablePrimal(), v) ≈ [4/7, 3/7, 6/7] atol=atol rtol=rtol + @test MOI.get(model, MOI.ObjectiveValue()) ≈ 13/7 atol=atol rtol=rtol + + @test MOI.get(model, MOI.VariablePrimal(), v) ≈ [4/7, 3/7, 6/7] atol=atol rtol=rtol + + # @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + # # The dual constraint gives + # # [λ_c1 + λ_c2, 2λ_c1 + λ_c2, 3λ_c1] = [2 1 0; 1 2 1; 0 1 2] * x + # # = [11, 16, 15] / 7 + # # hence λ_c1 = 5/7 and λ_c2 = 6/7. + # @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ 5/7 atol=atol rtol=rtol + # @test MOI.get(model, MOI.ConstraintDual(), c2) ≈ 6/7 atol=atol rtol=rtol - if config.duals - @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - # The dual constraint gives - # [λ_c1 + λ_c2, 2λ_c1 + λ_c2, 3λ_c1] = [2 1 0; 1 2 1; 0 1 2] * x - # = [11, 16, 15] / 7 - # hence λ_c1 = 5/7 and λ_c2 = 6/7. - @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ 5/7 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintDual(), c2) ≈ 6/7 atol=atol rtol=rtol - end - end end -function qp2test_mod(model::MOI.ModelLike, config::MOIT.Config) +function qp2test_mod(model::MOI.ModelLike, config) atol = config.atol rtol = config.rtol # Same as `qp1` but with duplicate terms then change the objective and sense @@ -280,7 +263,6 @@ function qp2test_mod(model::MOI.ModelLike, config::MOIT.Config) # x + y >= 1 (c2) # x, y, z \in R - @test MOIU.supports_default_copy_to(model, #=copy_names=# false) @test MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}()) @test MOI.supports_constraint(model, MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) @@ -322,33 +304,30 @@ function qp2test_mod(model::MOI.ModelLike, config::MOIT.Config) 0.0) MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj) - if config.query - @test obj ≈ MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}()) + @test obj ≈ MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}()) - @test c1f ≈ MOI.get(model, MOI.ConstraintFunction(), c1) + @test c1f ≈ MOI.get(model, MOI.ConstraintFunction(), c1) - @test MOI.GreaterThan(4.0) == MOI.get(model, MOI.ConstraintSet(), c1) - end + @test MOI.GreaterThan(4.0) == MOI.get(model, MOI.ConstraintSet(), c1) - if config.solve - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + MOI.optimize!(model) - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 13/7 atol=atol rtol=rtol + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.VariablePrimal(), v) ≈ [4/7,3/7,6/7] atol=atol rtol=rtol + @test MOI.get(model, MOI.ObjectiveValue()) ≈ 13/7 atol=atol rtol=rtol - if config.duals - @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ 5/7 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintDual(), c2) ≈ 6/7 atol=atol rtol=rtol - end - end + @test MOI.get(model, MOI.VariablePrimal(), v) ≈ [4/7,3/7,6/7] atol=atol rtol=rtol + + # if config.duals + # @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + # @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ 5/7 atol=atol rtol=rtol + # @test MOI.get(model, MOI.ConstraintDual(), c2) ≈ 6/7 atol=atol rtol=rtol + # end # change objective to Max -2(x^2 + xy + y^2 + yz + z^2) # First clear the objective, this is needed if a `Bridges.Objective.SlackBridge` is used. @@ -364,30 +343,25 @@ function qp2test_mod(model::MOI.ModelLike, config::MOIT.Config) 0.0) MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj2) - if config.query - @test obj2 ≈ MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}()) - end + @test obj2 ≈ MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}()) - if config.solve - MOI.optimize!(model) + MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ObjectiveValue()) ≈ -2*13/7 atol=atol rtol=rtol + @test MOI.get(model, MOI.ObjectiveValue()) ≈ -2*13/7 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), v) ≈ [4/7, 3/7, 6/7] atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), v) ≈ [4/7, 3/7, 6/7] atol=atol rtol=rtol + + # @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + # @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ 10/7 atol=atol rtol=rtol + # @test MOI.get(model, MOI.ConstraintDual(), c2) ≈ 12/7 atol=atol rtol=rtol - if config.duals - @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ 10/7 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintDual(), c2) ≈ 12/7 atol=atol rtol=rtol - end - end end -function qp3test_mod(model::MOI.ModelLike, config::MOIT.Config) +function qp3test_mod(model::MOI.ModelLike, config) atol = config.atol rtol = config.rtol # non-homogeneous quadratic objective @@ -395,7 +369,6 @@ function qp3test_mod(model::MOI.ModelLike, config::MOIT.Config) # s.t. x, y >= 0 # x + y = 1 - @test MOIU.supports_default_copy_to(model, #=copy_names=# false) @test MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}()) MOI.supports_constraint(model, MOI.VariableIndex, MOI.GreaterThan{Float64}) MOI.supports_constraint(model, MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) @@ -434,31 +407,31 @@ function qp3test_mod(model::MOI.ModelLike, config::MOIT.Config) ) MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj) - if config.solve - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) + MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + h = MOI.get(model.optimizer, MOI.RawSolver()) + HiGHS.Highs_writeOptions(h, "options.txt") - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 2.875 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), [x, y]) ≈ [0.25, 0.75] atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), vc1) ≈ 0.25 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), vc2) ≈ 0.75 atol=atol rtol=rtol + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - if config.duals - @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - # The dual constraint gives [λ_vc1 + λ_c1, λ_vc2 + λ_c1] = [4 1; 1 2] * x + [1, 1] = [11, 11] / 4 - # since `vc1` and `vc2` are not active, `λ_vc1` and `λ_vc2` are - # zero so `λ_c1 = 11/4`. - @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ 11 / 4 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintDual(), vc1) ≈ 0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintDual(), vc2) ≈ 0 atol=atol rtol=rtol - end - end + @test MOI.get(model, MOI.ObjectiveValue()) ≈ 2.875 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), [x, y]) ≈ [0.25, 0.75] atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), vc1) ≈ 0.25 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), vc2) ≈ 0.75 atol=atol rtol=rtol + + # @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + # # The dual constraint gives [λ_vc1 + λ_c1, λ_vc2 + λ_c1] = [4 1; 1 2] * x + [1, 1] = [11, 11] / 4 + # # since `vc1` and `vc2` are not active, `λ_vc1` and `λ_vc2` are + # # zero so `λ_c1 = 11/4`. + # @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ 11 / 4 atol=atol rtol=rtol + # @test MOI.get(model, MOI.ConstraintDual(), vc1) ≈ 0 atol=atol rtol=rtol + # @test MOI.get(model, MOI.ConstraintDual(), vc2) ≈ 0 atol=atol rtol=rtol + #= # change back to linear # max 2x + y + 1 # s.t. x, y >= 0 @@ -470,28 +443,25 @@ function qp3test_mod(model::MOI.ModelLike, config::MOIT.Config) objf = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], [x, y]), 1.0) MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), objf) - if config.solve - MOI.optimize!(model) + MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 3.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), [x,y]) ≈ [1.0, 0.0] atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), vc1) ≈ 1.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), vc2) ≈ 0.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ObjectiveValue()) ≈ 3.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), [x,y]) ≈ [1.0, 0.0] atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), vc1) ≈ 1.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), vc2) ≈ 0.0 atol=atol rtol=rtol +=# + # @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + # @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ -2 atol=atol rtol=rtol + # @test MOI.get(model, MOI.ConstraintDual(), vc1) ≈ 0 atol=atol rtol=rtol + # @test MOI.get(model, MOI.ConstraintDual(), vc2) ≈ 1 atol=atol rtol=rtol - if config.duals - @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ -2 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintDual(), vc1) ≈ 0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintDual(), vc2) ≈ 1 atol=atol rtol=rtol - end - end end -function qcp1test_mod(model::MOI.ModelLike, config::MOIT.Config) +function qcp1test_mod(model::MOI.ModelLike, config) atol = config.atol rtol = config.rtol # quadratic constraint @@ -502,7 +472,6 @@ function qcp1test_mod(model::MOI.ModelLike, config::MOIT.Config) # Optimal solution # x = 1/2, y = 7/4 - @test MOIU.supports_default_copy_to(model, #=copy_names=# false) @test MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) @test MOI.supports_constraint(model, MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) @test MOI.supports_constraint(model, MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}) @@ -546,37 +515,31 @@ function qcp1test_mod(model::MOI.ModelLike, config::MOIT.Config) MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE - if config.query - @test c2f ≈ MOI.get(model, MOI.ConstraintFunction(), c2) atol=atol rtol=rtol - end + @test c2f ≈ MOI.get(model, MOI.ConstraintFunction(), c2) atol=atol rtol=rtol - if config.solve - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) + MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 2.25 atol=atol rtol=rtol + @test MOI.get(model, MOI.ObjectiveValue()) ≈ 2.25 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), [x, y]) ≈ [0.5, 1.75] atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), [x, y]) ≈ [0.5, 1.75] atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), c1) ≈ [5/4, 9/4] atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), c2) ≈ 2 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), c1) ≈ [5/4, 9/4] atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), c2) ≈ 2 atol=atol rtol=rtol - if config.duals - @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - # The dual constraint gives - # [-1, -1] = [-λ_c1[1] + λ_c1[2], λ_c1[1] + λ_c1[2]] + λ_c2 * ([2 0; 0 0] * x + [0, 1]) - # = [-λ_c1[1] + λ_c1[2] + λ_c2, λ_c1[1] + λ_c1[2] + λ_c2] - # By complementary slackness, we have `λ_c1 = [0, 0]` so - # `λ_c2 = -1`. - @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ [0, 0] atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintDual(), c2) ≈ -1 atol=atol rtol=rtol - end - end + # @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + # # The dual constraint gives + # # [-1, -1] = [-λ_c1[1] + λ_c1[2], λ_c1[1] + λ_c1[2]] + λ_c2 * ([2 0; 0 0] * x + [0, 1]) + # # = [-λ_c1[1] + λ_c1[2] + λ_c2, λ_c1[1] + λ_c1[2] + λ_c2] + # # By complementary slackness, we have `λ_c1 = [0, 0]` so + # # `λ_c2 = -1`. + # @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ [0, 0] atol=atol rtol=rtol + # @test MOI.get(model, MOI.ConstraintDual(), c2) ≈ -1 atol=atol rtol=rtol # try delete quadratic constraint and go back to linear @@ -585,20 +548,19 @@ function qcp1test_mod(model::MOI.ModelLike, config::MOIT.Config) # # MOI.optimize!(model) # - # @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + # @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL # # @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT # # @test MOI.get(model, MOI.ObjectiveValue()) ≈ 0.0 atol=atol rtol=rtol end -function qcp2test_mod(model::MOI.ModelLike, config::MOIT.Config) +function qcp2test_mod(model::MOI.ModelLike, config) atol = config.atol rtol = config.rtol # Max x # s.t. x^2 <= 2 (c) - @test MOIU.supports_default_copy_to(model, #=copy_names=# false) @test MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) @test MOI.supports_constraint(model, MOI.ScalarQuadraticFunction{Float64},MOI.LessThan{Float64}) @@ -631,42 +593,36 @@ function qcp2test_mod(model::MOI.ModelLike, config::MOIT.Config) MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE - if config.query - @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) atol=atol rtol=rtol - end + @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) atol=atol rtol=rtol - if config.solve - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) + MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ObjectiveValue()) ≈ √2 atol=atol rtol=rtol + @test MOI.get(model, MOI.ObjectiveValue()) ≈ √2 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), x) ≈ √2 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ √2 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ 2 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ 2 atol=atol rtol=rtol + + # @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + # # The dual constraint gives + # # -1 = λ * (2 * x) = λ * 2√2 + # # hence λ = -1/(2√2). + # @test MOI.get(model, MOI.ConstraintDual(), c) ≈ -1 / (2 * √2) atol=atol rtol=rtol - if config.duals - @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - # The dual constraint gives - # -1 = λ * (2 * x) = λ * 2√2 - # hence λ = -1/(2√2). - @test MOI.get(model, MOI.ConstraintDual(), c) ≈ -1 / (2 * √2) atol=atol rtol=rtol - end - end end -function qcp3test_mod(model::MOI.ModelLike, config::MOIT.Config) +function qcp3test_mod(model::MOI.ModelLike, config) atol = config.atol rtol = config.rtol # Min -x # s.t. x^2 <= 2 - @test MOIU.supports_default_copy_to(model, #=copy_names=# false) @test MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) @test MOI.supports_constraint(model, MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}) @@ -700,47 +656,39 @@ function qcp3test_mod(model::MOI.ModelLike, config::MOIT.Config) MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE - if config.query - @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) atol=atol rtol=rtol - end + @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) atol=atol rtol=rtol - if config.solve - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) + MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - if config.duals - @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - end + # @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ObjectiveValue()) ≈ -√2 atol=atol rtol=rtol + @test MOI.get(model, MOI.ObjectiveValue()) ≈ -√2 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), x) ≈ √2 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ √2 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ 2 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ 2 atol=atol rtol=rtol + + # @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + # # The dual constraint gives + # # -1 = λ * (2 * x) = λ * 2√2 + # # hence λ = -1/(2√2). + # @test MOI.get(model, MOI.ConstraintDual(), c) ≈ -1 / (2 * √2) atol=atol rtol=rtol - if config.duals - @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - # The dual constraint gives - # -1 = λ * (2 * x) = λ * 2√2 - # hence λ = -1/(2√2). - @test MOI.get(model, MOI.ConstraintDual(), c) ≈ -1 / (2 * √2) atol=atol rtol=rtol - end - end end -function _qcp4test_mod(model::MOI.ModelLike, config::MOIT.Config, less_than::Bool) +function _qcp4test_mod(model::MOI.ModelLike, config, less_than::Bool) atol = config.atol rtol = config.rtol # Max x # s.t. x^2 + x * y + y^2 <= 3 (c) # y == 1 - @test MOIU.supports_default_copy_to(model, #=copy_names=# false) @test MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) quad_set = less_than ? MOI.LessThan(3.0) : MOI.GreaterThan(-3.0) @test MOI.supports_constraint(model, MOI.ScalarQuadraticFunction{Float64}, typeof(quad_set)) @@ -782,45 +730,40 @@ function _qcp4test_mod(model::MOI.ModelLike, config::MOIT.Config, less_than::Boo MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE - if config.query - @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) atol=atol rtol=rtol - @test quad_set == MOI.get(model, MOI.ConstraintSet(), c) - end + @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c) atol=atol rtol=rtol + @test quad_set == MOI.get(model, MOI.ConstraintSet(), c) - if config.solve - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) + MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 1.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ObjectiveValue()) ≈ 1.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), y) ≈ 1.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), y) ≈ 1.0 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ MOI.constant(quad_set) atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintPrimal(), vc) ≈ 1.0 atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), c) ≈ MOI.constant(quad_set) atol=atol rtol=rtol + @test MOI.get(model, MOI.ConstraintPrimal(), vc) ≈ 1.0 atol=atol rtol=rtol + + # @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + # # The dual constraint gives + # # [-1, 0] = λ_c * [2 1; 1 2] * x + [0, λ_vc] + # # = [3λ_c, 3λ_c + λ_vc] + # # hence `λ_c = -1/3` and `λ_vc = 1`. + # λ_c = less_than ? -1/3 : 1/3 + # @test MOI.get(model, MOI.ConstraintDual(), c) ≈ λ_c atol=atol rtol=rtol + # @test MOI.get(model, MOI.ConstraintDual(), vc) ≈ 1 atol=atol rtol=rtol - if config.duals - @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - # The dual constraint gives - # [-1, 0] = λ_c * [2 1; 1 2] * x + [0, λ_vc] - # = [3λ_c, 3λ_c + λ_vc] - # hence `λ_c = -1/3` and `λ_vc = 1`. - λ_c = less_than ? -1/3 : 1/3 - @test MOI.get(model, MOI.ConstraintDual(), c) ≈ λ_c atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintDual(), vc) ≈ 1 atol=atol rtol=rtol - end - end end -qcp4test_mod(model::MOI.ModelLike, config::MOIT.Config) = _qcp4test_mod(model, config, true) -qcp5test_mod(model::MOI.ModelLike, config::MOIT.Config) = _qcp4test_mod(model, config, false) +qcp4test_mod(model::MOI.ModelLike, config) = _qcp4test_mod(model, config, true) +qcp5test_mod(model::MOI.ModelLike, config) = _qcp4test_mod(model, config, false) -function socp1test_mod(model::MOI.ModelLike, config::MOIT.Config) +function socp1test_mod(model::MOI.ModelLike, config) atol = config.atol rtol = config.rtol # min t @@ -828,7 +771,6 @@ function socp1test_mod(model::MOI.ModelLike, config::MOIT.Config) # x^2 + y^2 <= t^2 (c2) # t >= 0 (bound) - @test MOIU.supports_default_copy_to(model, #=copy_names=# false) @test MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) @test MOI.supports_constraint(model, MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}) @test MOI.supports_constraint(model, MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) @@ -877,37 +819,32 @@ function socp1test_mod(model::MOI.ModelLike, config::MOIT.Config) MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE - if config.query - @test c1f ≈ MOI.get(model, MOI.ConstraintFunction(), c1) + @test c1f ≈ MOI.get(model, MOI.ConstraintFunction(), c1) - @test c2f ≈ MOI.get(model, MOI.ConstraintFunction(), c2) - end + @test c2f ≈ MOI.get(model, MOI.ConstraintFunction(), c2) - if config.solve - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) + MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 1/√2 atol=atol rtol=rtol + @test MOI.get(model, MOI.ObjectiveValue()) ≈ 1/√2 atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), [x,y,t]) ≈ [0.5,0.5,1/√2] atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), [x,y,t]) ≈ [0.5,0.5,1/√2] atol=atol rtol=rtol - @test MOI.get(model, MOI.VariablePrimal(), [t,x,y,t]) ≈ [1/√2,0.5,0.5,1/√2] atol=atol rtol=rtol + @test MOI.get(model, MOI.VariablePrimal(), [t,x,y,t]) ≈ [1/√2,0.5,0.5,1/√2] atol=atol rtol=rtol + + # @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + # # The dual constraint gives + # # [1, 0, 0] = [λ_bound, λ_c1, λ_c1] + λ_c2 * [-2 0 0; 0 2 0; 0 0 2] * x + # # = [λ_bound - √2*λ_c2, λ_c1 + λ_c2, λ_c1 + λ_c2] + # # and since `bound` is not active, `λ_bound = 0`. + # # This gives `λ_c2 = -1/√2` and `λ_c1 = 1/√2`. + # @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ 1/√2 atol=atol rtol=rtol + # @test MOI.get(model, MOI.ConstraintDual(), c2) ≈ -1/√2 atol=atol rtol=rtol + # @test MOI.get(model, MOI.ConstraintDual(), bound) ≈ 0 atol=atol rtol=rtol - if config.duals - @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT - # The dual constraint gives - # [1, 0, 0] = [λ_bound, λ_c1, λ_c1] + λ_c2 * [-2 0 0; 0 2 0; 0 0 2] * x - # = [λ_bound - √2*λ_c2, λ_c1 + λ_c2, λ_c1 + λ_c2] - # and since `bound` is not active, `λ_bound = 0`. - # This gives `λ_c2 = -1/√2` and `λ_c1 = 1/√2`. - @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ 1/√2 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintDual(), c2) ≈ -1/√2 atol=atol rtol=rtol - @test MOI.get(model, MOI.ConstraintDual(), bound) ≈ 0 atol=atol rtol=rtol - end - end end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index f4e6a21..319970c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,30 +2,31 @@ using QuadraticToBinary, Test, MathOptInterface const MOI = MathOptInterface const MOIU = MOI.Utilities -const MOIT = MOI.DeprecatedTest const MOIB = MOI.Bridges optimizers = [] +optimizers_high_tol = [] # include("solvers/gurobi.jl") -include("solvers/cbc.jl") +# include("solvers/xpress.jl") +# include("solvers/cbc.jl") +include("solvers/highs.jl") + +@assert length(optimizers) > 0 +@assert length(optimizers_high_tol) > 0 include("moi.jl") -const CONFIG_LOW_TOL = MOIT.Config{Float64}(atol = 1e-3, rtol = 1e-2, duals = false, infeas_certificates = false) -const CONFIG_VERY_LOW_TOL = MOIT.Config{Float64}(atol = 5e-3, rtol = 5e-2, duals = false, infeas_certificates = false) -const CONFIG = MOIT.Config{Float64}(duals = false, infeas_certificates = false) +const CONFIG_LOW_TOL = (atol = 1e-3, rtol = 1e-2) +const CONFIG_VERY_LOW_TOL = (atol = 5e-3, rtol = 5e-2) -function test_runtests() - optimizer = MOI.instantiate(Cbc.Optimizer, with_bridge_type = Float64) +function test_runtests(optimizer) model = QuadraticToBinary.Optimizer{Float64}(optimizer) MOI.set(model, MOI.Silent(), true) MOI.Test.runtests( model, MOI.Test.Config( - atol = 1e-3, rtol = 1e-2, + atol = 1e-3, rtol = 1e-2, # LOW_TOL exclude = Any[ - # MOI.DualStatus, - # Cbc limitations MOI.ConstraintDual, MOI.DualObjectiveValue, MOI.ConstraintBasisStatus, @@ -33,7 +34,6 @@ function test_runtests() ] ), exclude = String[ - # require bounds on variables: "test_quadratic_nonhomogeneous", "test_quadratic_nonconvex_constraint_integration", "test_quadratic_nonconvex_constraint_basic", @@ -49,30 +49,19 @@ function test_runtests() "test_objective_qp_ObjectiveFunction_edge_cases", "test_constraint_qcp_duplicate_off_diagonal", "test_constraint_qcp_duplicate_diagonal", - # TODO: broken in Cbc: - # https://github.com/jump-dev/Cbc.jl/blob/master/test/MOI_wrapper.jl - # TODO(odow): bug in Cbc.jl - "test_model_copy_to_UnsupportedAttribute", - "test_model_ModelFilter_AbstractConstraintAttribute", - # TODO(odow): bug in MOI - "test_model_LowerBoundAlreadySet", - "test_model_UpperBoundAlreadySet", - # TODO(odow): upstream bug in Cbc - "_Indicator_", - "test_linear_SOS1_integration", - "test_linear_SOS2_integration", - "test_solve_SOS2_add_and_delete", - # Can't prove infeasible. - "test_conic_NormInfinityCone_INFEASIBLE", - "test_conic_NormOneCone_INFEASIBLE", - "test_solve_TerminationStatus_DUAL_INFEASIBLE", ], ) #= - Hard + bounds required =# - optimizer = MOI.instantiate(Cbc.Optimizer, with_bridge_type = Float64) - model = QuadraticToBinary.Optimizer{Float64}(optimizer) + config = MOI.Test.Config( + atol = 1e-3, + rtol = 1e-2, + exclude = Any[ + MOI.ConstraintDual, + MOI.DualObjectiveValue + ], + ) MOI.set(model, QuadraticToBinary.FallbackUpperBound(), +10.0) MOI.set(model, QuadraticToBinary.FallbackLowerBound(), -10.0) MOI.set(model, QuadraticToBinary.GlobalVariablePrecision(), 1e-5) @@ -80,14 +69,7 @@ function test_runtests() MOI.set(model, MOI.TimeLimitSec(), 20.0) MOI.Test.runtests( model, - MOI.Test.Config( - atol = 5e-3, - rtol = 5e-2, - exclude = Any[ - MOI.ConstraintDual, - MOI.DualObjectiveValue - ], - ), + config, include = String[ "test_quadratic_nonhomogeneous", "test_quadratic_nonconvex_constraint_integration", @@ -101,44 +83,32 @@ function test_runtests() "test_objective_qp_ObjectiveFunction_edge_cases", "test_constraint_qcp_duplicate_off_diagonal", "test_constraint_qcp_duplicate_diagonal", + "test_quadratic_SecondOrderCone_basic", + "test_quadratic_integration", + # "test_quadratic_duplicate_terms", ] ) - #= - more precise bounds - =# - MOI.set(model, QuadraticToBinary.FallbackUpperBound(), 1.0) - MOI.set(model, QuadraticToBinary.FallbackLowerBound(), 0.0) - MOI.set(model, QuadraticToBinary.GlobalVariablePrecision(), 1e-5) - MOI.set(model, MOI.Silent(), true) - MOI.set(model, MOI.TimeLimitSec(), 80.0) + config = MOI.Test.Config( + atol = 5e-3, + rtol = 5e-2, + exclude = Any[ + MOI.ConstraintDual, + MOI.DualObjectiveValue + ], + ) MOI.Test.runtests( model, - MOI.Test.Config( - atol = 5e-3, - rtol = 5e-2, - optimal_status = MOI.OPTIMAL, - exclude = Any[ - MOI.ConstraintDual, - MOI.DualObjectiveValue - ], - ), + config, include = String[ - "test_quadratic_SecondOrderCone_basic", - "test_quadratic_integration", "test_quadratic_duplicate_terms", ] ) - return end -@testset "MOI Unit" begin - @time test_runtests() -end -@testset "basic_constraint_tests" begin - for opt in optimizers - MODEL = QuadraticToBinary.Optimizer{Float64}(opt) - MOIT.basic_constraint_tests(MODEL, CONFIG) +@testset "MOI Unit" begin + for opt in optimizers_high_tol + @time test_runtests(opt) end end @@ -180,71 +150,6 @@ end end end -@testset "MOI Deprecated Unit" begin - for opt in optimizers - MODEL = QuadraticToBinary.Optimizer{Float64}(opt) - MOIT.unittest(MODEL, CONFIG, [ - "number_threads", # FIXME implement `MOI.NumberOfThreads` - # INFEASIBLE_OR_UNBOUNDED instead of DUAL_INFEASIBLEÇ - "solve_unbounded_model", - # No quadratics (because of no bounds): - "delete_soc_variables", - "solve_qcp_edge_cases", - "solve_qp_edge_cases", - "solve_qp_zero_offdiag", - ]) - end -end - -@testset "ModelLike" begin - # @test MOI.get(CACHED, MOI.SolverName()) == "COIN Branch-and-Cut (Cbc)" - for opt in optimizers - MODEL = QuadraticToBinary.Optimizer{Float64}(opt) - @testset "default_objective_test" begin - MOIT.default_objective_test(MODEL) - end - @testset "default_status_test" begin - MOIT.default_status_test(MODEL) - end - @testset "nametest" begin - MOIT.nametest(MODEL) - end - @testset "validtest" begin - MOIT.validtest(MODEL) - end - @testset "emptytest" begin - MOIT.emptytest(MODEL) - end - @testset "orderedindicestest" begin - MOIT.orderedindicestest(MODEL) - end - @testset "copytest" begin - # Requires VectorOfVariables - # MOIT.copytest(MODEL, MOIU.CachingOptimizer( - # ModelForCachingOptimizer{Float64}(), - # Cbc.Optimizer() - # )) - end - end -end - -@testset "Continuous Linear" begin - for opt in optimizers - MODEL = QuadraticToBinary.Optimizer{Float64}(opt) - MOIT.contlineartest(MODEL, CONFIG) - end -end - -@testset "Integer Linear" begin - for opt in optimizers - MODEL = QuadraticToBinary.Optimizer{Float64}(opt) - MOIT.intlineartest(MODEL, CONFIG, [ - "indicator1", "indicator2", "indicator3", "indicator4", - "int2", - ]) - end -end - @testset "From MOI ncqcp" begin for opt in optimizers MODEL = QuadraticToBinary.Optimizer{Float64}(opt) @@ -263,7 +168,6 @@ end qcp3test_mod(MODEL, CONFIG_LOW_TOL) qcp4test_mod(MODEL, CONFIG_LOW_TOL) qcp5test_mod(MODEL, CONFIG_LOW_TOL) - # MOI.set(MODEL, QuadraticToBinary.GlobalVariablePrecision(), 1e-5) socp1test_mod(MODEL, CONFIG_LOW_TOL) end @@ -276,6 +180,13 @@ end qp1test_mod(MODEL, CONFIG_LOW_TOL) MOI.set(MODEL, QuadraticToBinary.GlobalVariablePrecision(), 1e-4) qp2test_mod(MODEL, CONFIG_VERY_LOW_TOL) + end +end + +@testset "From MOI qp" begin + for opt in optimizers_high_tol + MODEL = QuadraticToBinary.Optimizer{Float64}(opt) + MOI.set(MODEL, QuadraticToBinary.GlobalVariablePrecision(), 1e-6) qp3test_mod(MODEL, CONFIG_LOW_TOL) end end diff --git a/test/solvers/highs.jl b/test/solvers/highs.jl new file mode 100644 index 0000000..bf93322 --- /dev/null +++ b/test/solvers/highs.jl @@ -0,0 +1,14 @@ +using HiGHS + +highs = MOI.instantiate(HiGHS.Optimizer, with_bridge_type = Float64) +MOI.set(highs, MOI.Silent(), true) +push!(optimizers, highs) + +highs_high_tol = MOI.instantiate(HiGHS.Optimizer, with_bridge_type = Float64) +MOI.set(highs_high_tol, MOI.Silent(), true) +tol = 1e-9 +# MOI.set(highs_high_tol, MOI.RawOptimizerAttribute("small_matrix_value"), 1e-12) +MOI.set(highs_high_tol, MOI.RawOptimizerAttribute("mip_feasibility_tolerance"), tol) +MOI.set(highs_high_tol, MOI.RawOptimizerAttribute("primal_feasibility_tolerance"), tol) +# MOI.set(highs_high_tol, MOI.RawOptimizerAttribute("presolve"), "off") +push!(optimizers_high_tol, highs_high_tol) \ No newline at end of file diff --git a/test/solvers/xpress.jl b/test/solvers/xpress.jl new file mode 100644 index 0000000..0c4b063 --- /dev/null +++ b/test/solvers/xpress.jl @@ -0,0 +1,7 @@ +using Xpress + +XPRESS_OPTIMIZER = MOI.Bridges.full_bridge_optimizer( + Xpress.Optimizer(), Float64 +) +push!(optimizers_high_tol, XPRESS_OPTIMIZER) +push!(optimizers, XPRESS_OPTIMIZER)