From 81bc90034cf2b12173e56913814a1bb087286a89 Mon Sep 17 00:00:00 2001 From: Roman Lebedev Date: Thu, 27 Jun 2024 01:17:04 +0300 Subject: [PATCH 1/3] Add [missing] test for integer variable Refs. https://github.com/lanl-ansi/Alpine.jl/issues/233 Refs. https://github.com/jump-dev/MathOptInterface.jl/issues/2517 --- examples/MINLPs/milp.jl | 8 ++++++++ test/test_algorithm.jl | 11 +++++++++++ 2 files changed, 19 insertions(+) create mode 100644 examples/MINLPs/milp.jl diff --git a/examples/MINLPs/milp.jl b/examples/MINLPs/milp.jl new file mode 100644 index 00000000..488fbd57 --- /dev/null +++ b/examples/MINLPs/milp.jl @@ -0,0 +1,8 @@ +function milp(; solver = nothing) + m = JuMP.Model(solver) + + @variable(m, 0 <= objvar <= 20, Int) + @objective(m, Min, objvar) + + return m +end diff --git a/test/test_algorithm.jl b/test/test_algorithm.jl index f25b4f1b..58554d7d 100644 --- a/test/test_algorithm.jl +++ b/test/test_algorithm.jl @@ -1043,3 +1043,14 @@ end @test isapprox(objective_value(m), 13; atol = 1e-4) @test MOI.get(m, Alpine.NumberOfIterations()) == 0 end + +@testset "Test integer variables" begin + test_solver = optimizer_with_attributes( + Alpine.Optimizer, + "nlp_solver" => IPOPT, + "mip_solver" => HIGHS, + "minlp_solver" => JUNIPER, + ) + m = milp(solver = test_solver) + @test_throws "Alpine does not support MINLPs with generic integer (non-binary) variables yet!" JuMP.optimize!(m) +end From d2d9d1b0bc8978cb0c3fff67245f0aebe7a7cc7c Mon Sep 17 00:00:00 2001 From: Roman Lebedev Date: Thu, 27 Jun 2024 01:57:47 +0300 Subject: [PATCH 2/3] Don't falsely claim that `Integer` variables are supported They really are not supported, and the claim is only there to provide a nice diagnostic. But that prevents MOI `IntegerToZeroOneBridge` from triggering, and providing Alpine with transparent support for them. Fixes https://github.com/lanl-ansi/Alpine.jl/issues/244 --- examples/MINLPs/milp.jl | 8 ---- src/MOI_wrapper/MOI_wrapper.jl | 38 +++++++++++------ src/main_algorithm.jl | 4 -- test/test_algorithm.jl | 75 ++++++++++++++++++++++++++++++++-- 4 files changed, 98 insertions(+), 27 deletions(-) delete mode 100644 examples/MINLPs/milp.jl diff --git a/examples/MINLPs/milp.jl b/examples/MINLPs/milp.jl deleted file mode 100644 index 488fbd57..00000000 --- a/examples/MINLPs/milp.jl +++ /dev/null @@ -1,8 +0,0 @@ -function milp(; solver = nothing) - m = JuMP.Model(solver) - - @variable(m, 0 <= objvar <= 20, Int) - @objective(m, Min, objvar) - - return m -end diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index ed0103af..ef295726 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -322,18 +322,6 @@ function MOI.add_constraint(model::Optimizer, vi::MOI.VariableIndex, set::SCALAR return MOI.ConstraintIndex{typeof(vi),typeof(set)}(vi.value) end -function MOI.supports_constraint( - ::Optimizer, - ::Type{MOI.VariableIndex}, - ::Type{MOI.Integer}, -) - return true -end - -function MOI.add_constraint(model::Optimizer, f::MOI.VariableIndex, set::MOI.Integer) - model.var_type_orig[f.value] = :Int - return MOI.ConstraintIndex{typeof(f),typeof(set)}(f.value) -end function MOI.supports_constraint( ::Optimizer, ::Type{MOI.VariableIndex}, @@ -457,6 +445,32 @@ function MOI.is_valid(model::Alpine.Optimizer, vi::MOI.VariableIndex) return 1 <= vi.value <= model.num_var_orig end +function _get_bound_set(model::Alpine.Optimizer, vi::MOI.VariableIndex) + if !MOI.is_valid(model, vi) + throw(MOI.InvalidIndex(vi)) + end + return _bound_set(model.l_var_orig[vi.value], model.u_var_orig[vi.value]) +end + +function MOI.is_valid( + model::Alpine.Optimizer, + ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, +) where {S<:SCALAR_SET} + set = _get_bound_set(model, MOI.VariableIndex(ci.value)) + return set isa S +end + +function MOI.get( + model::Alpine.Optimizer, + ::MOI.ConstraintSet, + ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, +) where {S<:SCALAR_SET} + if !MOI.is_valid(model, ci) + throw(MOI.InvalidIndex(ci)) + end + return _get_bound_set(model, MOI.VariableIndex(ci.value)) +end + # Taken from MatrixOptInterface.jl @enum ConstraintSense EQUAL_TO GREATER_THAN LESS_THAN INTERVAL _sense(::Type{<:MOI.EqualTo}) = EQUAL_TO diff --git a/src/main_algorithm.jl b/src/main_algorithm.jl index 4a3cdda8..20d27595 100644 --- a/src/main_algorithm.jl +++ b/src/main_algorithm.jl @@ -97,10 +97,6 @@ function load!(m::Optimizer) # Populate data to create the bounding MIP model recategorize_var(m) # Initial round of variable re-categorization - :Int in m.var_type_orig && error( - "Alpine does not support MINLPs with generic integer (non-binary) variables yet!", - ) - # Solver-dependent detection _fetch_mip_solver_identifier(m) _fetch_nlp_solver_identifier(m) diff --git a/test/test_algorithm.jl b/test/test_algorithm.jl index 58554d7d..6400438e 100644 --- a/test/test_algorithm.jl +++ b/test/test_algorithm.jl @@ -1044,13 +1044,82 @@ end @test MOI.get(m, Alpine.NumberOfIterations()) == 0 end -@testset "Test integer variables" begin +@testset "Test integer variable support via IntegerToZeroOneBridge" begin test_solver = optimizer_with_attributes( Alpine.Optimizer, "nlp_solver" => IPOPT, "mip_solver" => HIGHS, "minlp_solver" => JUNIPER, ) - m = milp(solver = test_solver) - @test_throws "Alpine does not support MINLPs with generic integer (non-binary) variables yet!" JuMP.optimize!(m) + + m = JuMP.Model(test_solver) + + @variable(m, -10 <= objvar <= 20, Int) + + @objective(m, Min, objvar) + + JuMP.optimize!(m) + + @test termination_status(m) == MOI.OPTIMAL + @test isapprox(objective_value(m), -10; atol = 1e-4) + @test isapprox(value(objvar), -10, atol = 1E-6) + @test MOI.get(m, Alpine.NumberOfIterations()) == 0 +end +@testset "Test integer variable support 0" begin + test_solver = optimizer_with_attributes( + Alpine.Optimizer, + "nlp_solver" => IPOPT, + "mip_solver" => HIGHS, + "minlp_solver" => JUNIPER, + ) + + m = JuMP.Model(test_solver) + + @variable(m, objvar, Int) + @constraint(m, -10 <= objvar) + @constraint(m, objvar <= 20) + + @objective(m, Min, objvar) + + @test_throws "Unable to use IntegerToZeroOneBridge because the variable MOI.VariableIndex(1) has a non-finite domain" JuMP.optimize!( + m, + ) +end +@testset "Test integer variable support 1" begin + test_solver = optimizer_with_attributes( + Alpine.Optimizer, + "nlp_solver" => IPOPT, + "mip_solver" => HIGHS, + "minlp_solver" => JUNIPER, + ) + + m = JuMP.Model(test_solver) + + @variable(m, -10 <= objvar, Int) + @constraint(m, objvar <= 20) + + @objective(m, Min, objvar) + + @test_throws "Unable to use IntegerToZeroOneBridge because the variable MOI.VariableIndex(1) has a non-finite domain" JuMP.optimize!( + m, + ) +end +@testset "Test integer variable support 2" begin + test_solver = optimizer_with_attributes( + Alpine.Optimizer, + "nlp_solver" => IPOPT, + "mip_solver" => HIGHS, + "minlp_solver" => JUNIPER, + ) + + m = JuMP.Model(test_solver) + + @variable(m, objvar <= 20, Int) + @constraint(m, -10 <= objvar) + + @objective(m, Min, objvar) + + @test_throws "Unable to use IntegerToZeroOneBridge because the variable MOI.VariableIndex(1) has a non-finite domain" JuMP.optimize!( + m, + ) end From ee8673f20700f0619e611a6633079a32e837ac63 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 14 Aug 2024 16:27:19 +1200 Subject: [PATCH 3/3] Update test_algorithm.jl --- test/test_algorithm.jl | 79 +++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 44 deletions(-) diff --git a/test/test_algorithm.jl b/test/test_algorithm.jl index 6400438e..c53fbe09 100644 --- a/test/test_algorithm.jl +++ b/test/test_algorithm.jl @@ -1051,20 +1051,16 @@ end "mip_solver" => HIGHS, "minlp_solver" => JUNIPER, ) - - m = JuMP.Model(test_solver) - - @variable(m, -10 <= objvar <= 20, Int) - - @objective(m, Min, objvar) - - JuMP.optimize!(m) - - @test termination_status(m) == MOI.OPTIMAL - @test isapprox(objective_value(m), -10; atol = 1e-4) - @test isapprox(value(objvar), -10, atol = 1E-6) - @test MOI.get(m, Alpine.NumberOfIterations()) == 0 + model = JuMP.Model(test_solver) + @variable(model, -10 <= x <= 20, Int) + @objective(model, Min, x) + JuMP.optimize!(model) + @test termination_status(model) == MOI.OPTIMAL + @test isapprox(objective_value(model), -10; atol = 1e-4) + @test isapprox(value(x), -10, atol = 1e-6) + @test MOI.get(model, Alpine.NumberOfIterations()) == 0 end + @testset "Test integer variable support 0" begin test_solver = optimizer_with_attributes( Alpine.Optimizer, @@ -1072,19 +1068,17 @@ end "mip_solver" => HIGHS, "minlp_solver" => JUNIPER, ) - - m = JuMP.Model(test_solver) - - @variable(m, objvar, Int) - @constraint(m, -10 <= objvar) - @constraint(m, objvar <= 20) - - @objective(m, Min, objvar) - - @test_throws "Unable to use IntegerToZeroOneBridge because the variable MOI.VariableIndex(1) has a non-finite domain" JuMP.optimize!( - m, + model = JuMP.Model(test_solver) + @variable(model, x, Int) + @objective(model, Min, x) + @test_throws( + ErrorException( + "Unable to use IntegerToZeroOneBridge because the variable MOI.VariableIndex(1) has a non-finite domain", + ), + JuMP.optimize!(model), ) end + @testset "Test integer variable support 1" begin test_solver = optimizer_with_attributes( Alpine.Optimizer, @@ -1092,18 +1086,17 @@ end "mip_solver" => HIGHS, "minlp_solver" => JUNIPER, ) - - m = JuMP.Model(test_solver) - - @variable(m, -10 <= objvar, Int) - @constraint(m, objvar <= 20) - - @objective(m, Min, objvar) - - @test_throws "Unable to use IntegerToZeroOneBridge because the variable MOI.VariableIndex(1) has a non-finite domain" JuMP.optimize!( - m, + model = JuMP.Model(test_solver) + @variable(model, -10 <= x, Int) + @objective(model, Min, x) + @test_throws( + ErrorException( + "Unable to use IntegerToZeroOneBridge because the variable MOI.VariableIndex(1) has a non-finite domain", + ), + JuMP.optimize!(model), ) end + @testset "Test integer variable support 2" begin test_solver = optimizer_with_attributes( Alpine.Optimizer, @@ -1111,15 +1104,13 @@ end "mip_solver" => HIGHS, "minlp_solver" => JUNIPER, ) - - m = JuMP.Model(test_solver) - - @variable(m, objvar <= 20, Int) - @constraint(m, -10 <= objvar) - - @objective(m, Min, objvar) - - @test_throws "Unable to use IntegerToZeroOneBridge because the variable MOI.VariableIndex(1) has a non-finite domain" JuMP.optimize!( - m, + model = JuMP.Model(test_solver) + @variable(model, x <= 20, Int) + @objective(model, Min, x) + @test_throws( + ErrorException( + "Unable to use IntegerToZeroOneBridge because the variable MOI.VariableIndex(1) has a non-finite domain", + ), + JuMP.optimize!(model), ) end