Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nonlinear constraints with Boolean operators #21

Merged
merged 3 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/MiniZinc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
3 changes: 3 additions & 0 deletions src/optimize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 91 additions & 0 deletions src/write.jl
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,89 @@ 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 isinteger(x)
print(io, round(Int, x))
else
print(io, x)
end
return
end

function Base.write(io::IO, model::Model{T}) where {T}
MOI.FileFormats.create_unique_variable_names(
model,
Expand All @@ -478,6 +561,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;")
Expand Down
93 changes: 93 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,99 @@ 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)), 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)
@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_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)))),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chriscoey nested expressions like this work, for both binary and integer variables.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice!! 🤩

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll merge so that you can experiment with it more easily. It'll have some rough edges (assertion errors) when things go wrong, but we can fix those in time if there is interest.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome yes i plan to experiment with it in the next few days. thanks @odow!

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)
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)
@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

function test_model_solver_name()
solver = MiniZinc.Optimizer{Int}(MiniZinc.Chuffed())
@test MOI.get(solver, MOI.SolverName()) == "MiniZinc"
return
end

end

TestMiniZinc.runtests()