From 1c31d311af78ad74f385f71a6be43ce850817640 Mon Sep 17 00:00:00 2001 From: Simon Bowly Date: Fri, 29 Nov 2024 06:26:42 +1100 Subject: [PATCH] Track nonlinear resultant vars outside of MOI (#590) --- src/MOI_wrapper/MOI_nonlinear.jl | 37 +++++++---- src/MOI_wrapper/MOI_wrapper.jl | 12 +++- test/MOI/MOI_wrapper.jl | 103 +++++++++++++++++++++++++++++-- 3 files changed, 133 insertions(+), 19 deletions(-) diff --git a/src/MOI_wrapper/MOI_nonlinear.jl b/src/MOI_wrapper/MOI_nonlinear.jl index 33128c1c..82088027 100644 --- a/src/MOI_wrapper/MOI_nonlinear.jl +++ b/src/MOI_wrapper/MOI_nonlinear.jl @@ -324,15 +324,19 @@ function MOI.add_constraint( opcode = Cint[] data = Cdouble[] parent = Cint[] - sense, rhs = _sense_and_rhs(s) _process_nonlinear(model, f, opcode, data, parent) - # Add resultant variable - vi, ci = MOI.add_constrained_variable(model, s) - resvar_index = c_column(model, vi) + # Add resultant variable. We don't use MOI.add_constrained_variable because + # we don't want it to show up in the bound constraints, etc. + column = _get_next_column(model) + lb, ub = _bounds(s) + lb = something(lb, -Inf) + ub = something(ub, Inf) + ret = GRBaddvar(model, 0, C_NULL, C_NULL, 0.0, lb, ub, GRB_CONTINUOUS, "") + _check_ret(model, ret) ret = GRBaddgenconstrNL( model, C_NULL, - resvar_index, + column - 1, length(opcode), opcode, data, @@ -342,7 +346,7 @@ function MOI.add_constraint( _require_update(model, model_change = true) model.last_constraint_index += 1 model.nl_constraint_info[model.last_constraint_index] = - _NLConstraintInfo(length(model.nl_constraint_info) + 1, s, vi) + _NLConstraintInfo(length(model.nl_constraint_info) + 1, s, column) return MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,typeof(s)}( model.last_constraint_index, ) @@ -371,8 +375,13 @@ function MOI.delete( end delete!(model.nl_constraint_info, c.value) model.name_to_constraint_index = nothing - # Remove resultant variable - MOI.delete(model, info.resvar) + # Delete resultant variable from the Gurobi model. These are not tracked in + # model.variable_info but they do need to be accounted for in index + # adjustment. + del_cols = [Cint(info.resvar_index - 1)] + ret = GRBdelvars(model, length(del_cols), del_cols) + _check_ret(model, ret) + append!(model.columns_deleted_since_last_update, del_cols .+ 1) _require_update(model, model_change = true) return end @@ -389,9 +398,15 @@ function MOI.delete( info.row -= searchsortedlast(rows_to_delete, info.row - 1) end model.name_to_constraint_index = nothing - # Delete resultant variables - resvars = [_info(model, c).resvar for c in cs] - MOI.delete(model, resvars) + # Delete resultant variables from the Gurobi model for all removed + # constraints. These are not tracked in model.variable_info but they do + # need to be accounted for in index adjustment. + del_cols = [Cint(_info(model, c).resvar_index - 1) for c in cs] + ret = GRBdelvars(model, length(del_cols), del_cols) + _check_ret(model, ret) + append!(model.columns_deleted_since_last_update, del_cols .+ 1) + _require_update(model, model_change = true) + # Remove entries from nl constraint tracking cs_values = sort!(getfield.(cs, :value)) filter!(model.nl_constraint_info) do pair return isempty(searchsorted(cs_values, pair.first)) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index 51949d0c..09c85fe1 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -96,9 +96,9 @@ mutable struct _NLConstraintInfo # Storage for constraint names. Where possible, these are also stored in # the Gurobi model. name::String - resvar::MOI.VariableIndex - function _NLConstraintInfo(row::Int, set, resvar::MOI.VariableIndex) - return new(row, set, "", resvar) + resvar_index::Int + function _NLConstraintInfo(row::Int, set, resvar_index::Int) + return new(row, set, "", resvar_index) end end @@ -566,6 +566,12 @@ function _update_if_necessary( var_info.column, ) end + for nl_info in values(model.nl_constraint_info) + nl_info.resvar_index -= searchsortedlast( + model.columns_deleted_since_last_update, + nl_info.resvar_index, + ) + end model.next_column -= length(model.columns_deleted_since_last_update) empty!(model.columns_deleted_since_last_update) ret = GRBupdatemodel(model) diff --git a/test/MOI/MOI_wrapper.jl b/test/MOI/MOI_wrapper.jl index 3f9b347a..f8412679 100644 --- a/test/MOI/MOI_wrapper.jl +++ b/test/MOI/MOI_wrapper.jl @@ -65,15 +65,14 @@ function test_runtests() "_RotatedSecondOrderCone_", "_GeometricMeanCone_", # Shaky tests - "vector_nonlinear", - "VectorNonlinearFunction", - # Tests should be skipped due to RequirementsUnmet, but aren't - r"^test_nonlinear_expression_hs071$", - r"^test_nonlinear_expression_hs071_epigraph$", + "_multiobjective_vector_nonlinear", + # Timeouts r"^test_nonlinear_expression_hs109$", r"^test_nonlinear_expression_hs110$", + # MOI.get(MOI.ObjectiveValue()) fails for NL objectives r"^test_nonlinear_expression_quartic$", r"^test_nonlinear_expression_overrides_objective$", + # Nonlinear duals not computed r"^test_nonlinear_duals$", ], ) @@ -590,6 +589,72 @@ function test_add_constrained_variables() return end +function test_add_constrained_variable_greaterthan() + model = Gurobi.Optimizer(GRB_ENV) + MOI.set(model, MOI.Silent(), true) + set = MOI.GreaterThan{Float64}(1.2) + vi, ci = MOI.add_constrained_variable(model, set) + @test MOI.get(model, MOI.NumberOfVariables()) == 1 + @test MOI.get(model, MOI.ListOfConstraintTypesPresent()) == + [(MOI.VariableIndex, MOI.GreaterThan{Float64})] + @test MOI.get(model, MOI.ConstraintFunction(), ci) == vi + @test MOI.get(model, MOI.ConstraintSet(), ci) == set + # Force update and check correct bounds on the Gurobi model + MOI.optimize!(model) + valueP = Ref{Cdouble}() + ret = Gurobi.GRBgetdblattrelement(model, "LB", 0, valueP) + @test ret == 0 + @test valueP[] == 1.2 + ret = Gurobi.GRBgetdblattrelement(model, "UB", 0, valueP) + @test ret == 0 + @test valueP[] >= -1e30 + return +end + +function test_add_constrained_variable_lessthan() + model = Gurobi.Optimizer(GRB_ENV) + MOI.set(model, MOI.Silent(), true) + set = MOI.LessThan{Float64}(3.4) + vi, ci = MOI.add_constrained_variable(model, set) + @test MOI.get(model, MOI.NumberOfVariables()) == 1 + @test MOI.get(model, MOI.ListOfConstraintTypesPresent()) == + [(MOI.VariableIndex, MOI.LessThan{Float64})] + @test MOI.get(model, MOI.ConstraintFunction(), ci) == vi + @test MOI.get(model, MOI.ConstraintSet(), ci) == set + # Force update and check correct bounds on the Gurobi model + MOI.optimize!(model) + valueP = Ref{Cdouble}() + ret = Gurobi.GRBgetdblattrelement(model, "LB", 0, valueP) + @test ret == 0 + @test valueP[] <= -1e30 + ret = Gurobi.GRBgetdblattrelement(model, "UB", 0, valueP) + @test ret == 0 + @test valueP[] == 3.4 + return +end + +function test_add_constrained_variable_equalto() + model = Gurobi.Optimizer(GRB_ENV) + MOI.set(model, MOI.Silent(), true) + set = MOI.EqualTo{Float64}(5.2) + vi, ci = MOI.add_constrained_variable(model, set) + @test MOI.get(model, MOI.NumberOfVariables()) == 1 + @test MOI.get(model, MOI.ListOfConstraintTypesPresent()) == + [(MOI.VariableIndex, MOI.EqualTo{Float64})] + @test MOI.get(model, MOI.ConstraintFunction(), ci) == vi + @test MOI.get(model, MOI.ConstraintSet(), ci) == set + # Force update and check correct bounds on the Gurobi model + MOI.optimize!(model) + valueP = Ref{Cdouble}() + ret = Gurobi.GRBgetdblattrelement(model, "LB", 0, valueP) + @test ret == 0 + @test valueP[] == 5.2 + ret = Gurobi.GRBgetdblattrelement(model, "UB", 0, valueP) + @test ret == 0 + @test valueP[] == 5.2 + return +end + function _is_binary(x; atol = 1e-6) return isapprox(x, 0; atol = atol) || isapprox(x, 1; atol = atol) end @@ -1363,6 +1428,34 @@ function test_ConstrName_too_long() return end +function test_delete_nonlinear_index() + if !Gurobi._supports_nonlinear() + return + end + model = Gurobi.Optimizer(GRB_ENV) + x1 = MOI.add_variable(model) + x2 = MOI.add_variable(model) + MOI.add_constraint(model, x1, MOI.GreaterThan(-1.0)) + MOI.add_constraint(model, x1, MOI.LessThan(1.0)) + MOI.add_constraint(model, x2, MOI.GreaterThan(-1.0)) + MOI.add_constraint(model, x2, MOI.LessThan(1.0)) + g1 = MOI.ScalarNonlinearFunction( + :+, + Any[MOI.ScalarNonlinearFunction(:sin, Any[2.5*x1]), 1.0*x2], + ) + g2 = MOI.ScalarNonlinearFunction( + :+, + Any[MOI.ScalarNonlinearFunction(:cos, Any[2.5*x1]), 1.0*x2], + ) + c1 = MOI.add_constraint(model, g1, MOI.EqualTo(0.0)) + c2 = MOI.add_constraint(model, g2, MOI.EqualTo(0.0)) + # Delete in order: tests that the resvar index of the first constraint + # is correctly adjusted. + MOI.delete(model, c1) + MOI.delete(model, c2) + return +end + end # TestMOIWrapper TestMOIWrapper.runtests()