From 0645849ebe75bdb96e60fb109a91ba578a97c093 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 26 Jan 2023 16:22:55 +1300 Subject: [PATCH 1/3] Support nonlinear constraints with Boolean operators --- src/MiniZinc.jl | 7 ++++ src/optimize.jl | 3 ++ src/write.jl | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 26 +++++++++++++++ 4 files changed, 121 insertions(+) diff --git a/src/MiniZinc.jl b/src/MiniZinc.jl index 852f887..41bf77e 100644 --- a/src/MiniZinc.jl +++ b/src/MiniZinc.jl @@ -71,6 +71,13 @@ function MOI.supports_constraint( return true end +MOI.supports(::Model, ::MOI.NLPBlock) = true + +function MOI.set(model::Model, ::MOI.NLPBlock, data::MOI.NLPBlockData) + model.ext[:nlp_block] = data + return +end + include("write.jl") include("optimize.jl") diff --git a/src/optimize.jl b/src/optimize.jl index f364f9d..a1f39c4 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -83,10 +83,13 @@ end # The MOI interface +MOI.get(model::Optimizer, ::MOI.SolverName) = "MiniZinc" + MOI.is_empty(model::Optimizer) = MOI.is_empty(model.inner) function MOI.empty!(model::Optimizer) MOI.empty!(model.inner) + empty!(model.inner.ext) model.has_solution = false empty!(model.primal_solution) return diff --git a/src/write.jl b/src/write.jl index aac5fb1..2f6132f 100644 --- a/src/write.jl +++ b/src/write.jl @@ -460,6 +460,83 @@ function _write_predicates(io, model) return end +function _write_constraint(io, model, variables, expr::Expr) + print(io, "constraint ") + if Meta.isexpr(expr, :call, 3) + @assert expr.args[1] in (:(<=), :(>=), :(<), :(>), :(==)) + _write_expression(io, model, variables, expr.args[2]) + rhs = expr.args[3] + if isone(rhs) + println(io, " $(expr.args[1]) true;") + else + @assert iszero(rhs) + println(io, " $(expr.args[1]) false;") + end + else + @assert Meta.isexpr(expr, :comparison, 5) + error("Two sided not supported") + end + return +end + +function _write_logical_expression(io, model, variables, expr) + ops = Dict(:|| => "\\/", :&& => "/\\") + op = get(ops, expr.head, nothing) + @assert op !== nothing + print(io, "(") + _write_expression(io, model, variables, expr.args[1]) + for i in 2:length(expr.args) + print(io, " ", op, " ") + _write_expression(io, model, variables, expr.args[i]) + end + print(io, ")") + return +end + +function _write_call_expression(io, model, variables, expr) + ops = Dict(:- => "-", :+ => "+") + op = get(ops, expr.args[1], nothing) + @assert op !== nothing + print(io, "(") + _write_expression(io, model, variables, expr.args[2]) + for i in 3:length(expr.args) + print(io, " ", op, " ") + _write_expression(io, model, variables, expr.args[i]) + end + print(io, ")") + return +end + +function _write_expression(io, model, variables, expr::Expr) + if Meta.isexpr(expr, :ref) + @assert expr.args[1] == :x + _write_expression(io, model, variables, expr.args[2]) + return + end + if Meta.isexpr(expr, :||) || Meta.isexpr(expr, :&&) + _write_logical_expression(io, model, variables, expr) + else + @assert Meta.isexpr(expr, :call) + _write_call_expression(io, model, variables, expr) + end + return +end + +function _write_expression(io, model, variables, x::MOI.VariableIndex) + print(io, _to_string(variables, x)) + return +end + +function _write_expression(io, model, variables, x::Real) + if isone(x) + print(io, "1") + else + @assert iszero(x) + print(io, "0") + end + return +end + function Base.write(io::IO, model::Model{T}) where {T} MOI.FileFormats.create_unique_variable_names( model, @@ -478,6 +555,14 @@ function Base.write(io::IO, model::Model{T}) where {T} end _write_constraint(io, model, variables, F, S) end + nlp_block = get(model.ext, :nlp_block, nothing) + if nlp_block !== nothing + MOI.initialize(nlp_block.evaluator, [:ExprGraph]) + for i in 1:length(nlp_block.constraint_bounds) + expr = MOI.constraint_expr(nlp_block.evaluator, i) + _write_constraint(io, model, variables, expr) + end + end sense = MOI.get(model, MOI.ObjectiveSense()) if sense == MOI.FEASIBILITY_SENSE println(io, "solve satisfy;") diff --git a/test/runtests.jl b/test/runtests.jl index f481ce5..e98d839 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1047,6 +1047,32 @@ function test_model_filename() return end +function test_model_nlp_boolean() + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Int}()) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.ZeroOne()) + nlp = MOI.Nonlinear.Model() + a, b = x + MOI.Nonlinear.add_constraint(nlp, :(($a || $b) - 1.0), MOI.EqualTo(0.0)) + MOI.Nonlinear.add_constraint(nlp, :(($a && $b) - 0.0), MOI.EqualTo(0.0)) + backend = MOI.Nonlinear.ExprGraphOnly() + evaluator = MOI.Nonlinear.Evaluator(nlp, backend, x) + MOI.set(model, MOI.NLPBlock(), MOI.NLPBlockData(evaluator)) + solver = MiniZinc.Optimizer{Int}(MiniZinc.Chuffed()) + MOI.set(solver, MOI.RawOptimizerAttribute("model_filename"), "test.mzn") + index_map, _ = MOI.optimize!(solver, model) + println(read("test.mzn", String)) + @test MOI.get(solver, MOI.TerminationStatus()) === MOI.OPTIMAL + @test MOI.get(solver, MOI.ResultCount()) >= 1 + y = [index_map[v] for v in x] + sol = round.(Bool, MOI.get(solver, MOI.VariablePrimal(), y)) + @test (sol[1] || sol[2]) == true + @test (sol[1] && sol[2]) == false + @test read("test.mzn", String) == "var bool: x1;\nvar bool: x2;\nconstraint ((x1 \\/ x2) - 1) == false;\nconstraint ((x1 /\\ x2) - 0) == false;\nsolve satisfy;\n" + rm("test.mzn") + return +end + end TestMiniZinc.runtests() From e6856aa12dfda22c15106c4efe28bf90a22ba28c Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 26 Jan 2023 16:57:12 +1300 Subject: [PATCH 2/3] Add tests --- test/runtests.jl | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index e98d839..ce46c5b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1048,6 +1048,33 @@ function test_model_filename() end function test_model_nlp_boolean() + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Int}()) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.ZeroOne()) + nlp = MOI.Nonlinear.Model() + a, b = x + MOI.Nonlinear.add_constraint(nlp, :(($a || $b)), MOI.EqualTo(1.0)) + MOI.Nonlinear.add_constraint(nlp, :(($a && $b)), MOI.EqualTo(0.0)) + backend = MOI.Nonlinear.ExprGraphOnly() + evaluator = MOI.Nonlinear.Evaluator(nlp, backend, x) + MOI.set(model, MOI.NLPBlock(), MOI.NLPBlockData(evaluator)) + solver = MiniZinc.Optimizer{Int}(MiniZinc.Chuffed()) + MOI.set(solver, MOI.RawOptimizerAttribute("model_filename"), "test.mzn") + index_map, _ = MOI.optimize!(solver, model) + println(read("test.mzn", String)) + @test MOI.get(solver, MOI.TerminationStatus()) === MOI.OPTIMAL + @test MOI.get(solver, MOI.ResultCount()) >= 1 + y = [index_map[v] for v in x] + sol = round.(Bool, MOI.get(solver, MOI.VariablePrimal(), y)) + @test (sol[1] || sol[2]) == true + @test (sol[1] && sol[2]) == false + @test read("test.mzn", String) == + "var bool: x1;\nvar bool: x2;\nconstraint (x1 \\/ x2) == true;\nconstraint (x1 /\\ x2) == false;\nsolve satisfy;\n" + rm("test.mzn") + return +end + +function test_model_nlp_boolean_jump() model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Int}()) x = MOI.add_variables(model, 2) MOI.add_constraint.(model, x, MOI.ZeroOne()) @@ -1068,11 +1095,18 @@ function test_model_nlp_boolean() sol = round.(Bool, MOI.get(solver, MOI.VariablePrimal(), y)) @test (sol[1] || sol[2]) == true @test (sol[1] && sol[2]) == false - @test read("test.mzn", String) == "var bool: x1;\nvar bool: x2;\nconstraint ((x1 \\/ x2) - 1) == false;\nconstraint ((x1 /\\ x2) - 0) == false;\nsolve satisfy;\n" + @test read("test.mzn", String) == + "var bool: x1;\nvar bool: x2;\nconstraint ((x1 \\/ x2) - 1) == false;\nconstraint ((x1 /\\ x2) - 0) == false;\nsolve satisfy;\n" rm("test.mzn") return end +function test_model_solver_name() + solver = MiniZinc.Optimizer{Int}(MiniZinc.Chuffed()) + @test MOI.get(solver, MOI.SolverName()) == "MiniZinc" + return +end + end TestMiniZinc.runtests() From ca90a3a571cc111cda8df0e63c8dc568271919a6 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 27 Jan 2023 12:26:26 +1300 Subject: [PATCH 3/3] Add more tests --- src/write.jl | 16 +++++++++++----- test/runtests.jl | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/write.jl b/src/write.jl index 2f6132f..c9f0e6b 100644 --- a/src/write.jl +++ b/src/write.jl @@ -494,7 +494,14 @@ function _write_logical_expression(io, model, variables, expr) end function _write_call_expression(io, model, variables, expr) - ops = Dict(:- => "-", :+ => "+") + ops = Dict( + :- => "-", + :+ => "+", + :(<) => "<", + :(>) => ">", + :(<=) => "<=", + :(>=) => ">=", + ) op = get(ops, expr.args[1], nothing) @assert op !== nothing print(io, "(") @@ -528,11 +535,10 @@ function _write_expression(io, model, variables, x::MOI.VariableIndex) end function _write_expression(io, model, variables, x::Real) - if isone(x) - print(io, "1") + if isinteger(x) + print(io, round(Int, x)) else - @assert iszero(x) - print(io, "0") + print(io, x) end return end diff --git a/test/runtests.jl b/test/runtests.jl index ce46c5b..dab4183 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1061,7 +1061,6 @@ function test_model_nlp_boolean() solver = MiniZinc.Optimizer{Int}(MiniZinc.Chuffed()) MOI.set(solver, MOI.RawOptimizerAttribute("model_filename"), "test.mzn") index_map, _ = MOI.optimize!(solver, model) - println(read("test.mzn", String)) @test MOI.get(solver, MOI.TerminationStatus()) === MOI.OPTIMAL @test MOI.get(solver, MOI.ResultCount()) >= 1 y = [index_map[v] for v in x] @@ -1074,6 +1073,41 @@ function test_model_nlp_boolean() return end +function test_model_nlp_boolean_nested() + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Int}()) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.ZeroOne()) + y = MOI.add_variable(model) + MOI.add_constraint(model, y, MOI.Integer()) + MOI.add_constraint(model, y, MOI.Interval(0, 10)) + nlp = MOI.Nonlinear.Model() + a, b = x + # a || (b && (y < 5)) + MOI.Nonlinear.add_constraint( + nlp, + :(($a || ($b && ($y < 5)))), + MOI.EqualTo(1.0), + ) + MOI.Nonlinear.add_constraint(nlp, :($a < 1), MOI.EqualTo(1.0)) + backend = MOI.Nonlinear.ExprGraphOnly() + evaluator = MOI.Nonlinear.Evaluator(nlp, backend, x) + MOI.set(model, MOI.NLPBlock(), MOI.NLPBlockData(evaluator)) + solver = MiniZinc.Optimizer{Int}(MiniZinc.Chuffed()) + MOI.set(solver, MOI.RawOptimizerAttribute("model_filename"), "test.mzn") + index_map, _ = MOI.optimize!(solver, model) + @test MOI.get(solver, MOI.TerminationStatus()) === MOI.OPTIMAL + @test MOI.get(solver, MOI.ResultCount()) >= 1 + sol_x = [index_map[v] for v in [x; y]] + sol = round.(Int, MOI.get(solver, MOI.VariablePrimal(), sol_x)) + @test sol[1] == 0 + @test sol[2] == 1 + @test sol[3] < 5 + @test read("test.mzn", String) == + "var bool: x1;\nvar bool: x2;\nvar 0 .. 10: x3;\nconstraint (x1 \\/ (x2 /\\ (x3 < 5))) == true;\nconstraint (x1 < 1) == true;\nsolve satisfy;\n" + rm("test.mzn") + return +end + function test_model_nlp_boolean_jump() model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Int}()) x = MOI.add_variables(model, 2) @@ -1088,7 +1122,6 @@ function test_model_nlp_boolean_jump() solver = MiniZinc.Optimizer{Int}(MiniZinc.Chuffed()) MOI.set(solver, MOI.RawOptimizerAttribute("model_filename"), "test.mzn") index_map, _ = MOI.optimize!(solver, model) - println(read("test.mzn", String)) @test MOI.get(solver, MOI.TerminationStatus()) === MOI.OPTIMAL @test MOI.get(solver, MOI.ResultCount()) >= 1 y = [index_map[v] for v in x]