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 f25b4f1b..c53fbe09 100644 --- a/test/test_algorithm.jl +++ b/test/test_algorithm.jl @@ -1043,3 +1043,74 @@ end @test isapprox(objective_value(m), 13; atol = 1e-4) @test MOI.get(m, Alpine.NumberOfIterations()) == 0 end + +@testset "Test integer variable support via IntegerToZeroOneBridge" begin + test_solver = optimizer_with_attributes( + Alpine.Optimizer, + "nlp_solver" => IPOPT, + "mip_solver" => HIGHS, + "minlp_solver" => JUNIPER, + ) + 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, + "nlp_solver" => IPOPT, + "mip_solver" => HIGHS, + "minlp_solver" => JUNIPER, + ) + 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, + "nlp_solver" => IPOPT, + "mip_solver" => HIGHS, + "minlp_solver" => JUNIPER, + ) + 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, + "nlp_solver" => IPOPT, + "mip_solver" => HIGHS, + "minlp_solver" => JUNIPER, + ) + 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