Skip to content

Commit 56f1743

Browse files
authored
Support nonlinear constraints with Boolean operators (#21)
1 parent e31983b commit 56f1743

File tree

4 files changed

+194
-0
lines changed

4 files changed

+194
-0
lines changed

src/MiniZinc.jl

+7
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ function MOI.supports_constraint(
7171
return true
7272
end
7373

74+
MOI.supports(::Model, ::MOI.NLPBlock) = true
75+
76+
function MOI.set(model::Model, ::MOI.NLPBlock, data::MOI.NLPBlockData)
77+
model.ext[:nlp_block] = data
78+
return
79+
end
80+
7481
include("write.jl")
7582
include("optimize.jl")
7683

src/optimize.jl

+3
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,13 @@ end
8383

8484
# The MOI interface
8585

86+
MOI.get(model::Optimizer, ::MOI.SolverName) = "MiniZinc"
87+
8688
MOI.is_empty(model::Optimizer) = MOI.is_empty(model.inner)
8789

8890
function MOI.empty!(model::Optimizer)
8991
MOI.empty!(model.inner)
92+
empty!(model.inner.ext)
9093
model.has_solution = false
9194
empty!(model.primal_solution)
9295
return

src/write.jl

+91
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,89 @@ function _write_predicates(io, model)
460460
return
461461
end
462462

463+
function _write_constraint(io, model, variables, expr::Expr)
464+
print(io, "constraint ")
465+
if Meta.isexpr(expr, :call, 3)
466+
@assert expr.args[1] in (:(<=), :(>=), :(<), :(>), :(==))
467+
_write_expression(io, model, variables, expr.args[2])
468+
rhs = expr.args[3]
469+
if isone(rhs)
470+
println(io, " $(expr.args[1]) true;")
471+
else
472+
@assert iszero(rhs)
473+
println(io, " $(expr.args[1]) false;")
474+
end
475+
else
476+
@assert Meta.isexpr(expr, :comparison, 5)
477+
error("Two sided not supported")
478+
end
479+
return
480+
end
481+
482+
function _write_logical_expression(io, model, variables, expr)
483+
ops = Dict(:|| => "\\/", :&& => "/\\")
484+
op = get(ops, expr.head, nothing)
485+
@assert op !== nothing
486+
print(io, "(")
487+
_write_expression(io, model, variables, expr.args[1])
488+
for i in 2:length(expr.args)
489+
print(io, " ", op, " ")
490+
_write_expression(io, model, variables, expr.args[i])
491+
end
492+
print(io, ")")
493+
return
494+
end
495+
496+
function _write_call_expression(io, model, variables, expr)
497+
ops = Dict(
498+
:- => "-",
499+
:+ => "+",
500+
:(<) => "<",
501+
:(>) => ">",
502+
:(<=) => "<=",
503+
:(>=) => ">=",
504+
)
505+
op = get(ops, expr.args[1], nothing)
506+
@assert op !== nothing
507+
print(io, "(")
508+
_write_expression(io, model, variables, expr.args[2])
509+
for i in 3:length(expr.args)
510+
print(io, " ", op, " ")
511+
_write_expression(io, model, variables, expr.args[i])
512+
end
513+
print(io, ")")
514+
return
515+
end
516+
517+
function _write_expression(io, model, variables, expr::Expr)
518+
if Meta.isexpr(expr, :ref)
519+
@assert expr.args[1] == :x
520+
_write_expression(io, model, variables, expr.args[2])
521+
return
522+
end
523+
if Meta.isexpr(expr, :||) || Meta.isexpr(expr, :&&)
524+
_write_logical_expression(io, model, variables, expr)
525+
else
526+
@assert Meta.isexpr(expr, :call)
527+
_write_call_expression(io, model, variables, expr)
528+
end
529+
return
530+
end
531+
532+
function _write_expression(io, model, variables, x::MOI.VariableIndex)
533+
print(io, _to_string(variables, x))
534+
return
535+
end
536+
537+
function _write_expression(io, model, variables, x::Real)
538+
if isinteger(x)
539+
print(io, round(Int, x))
540+
else
541+
print(io, x)
542+
end
543+
return
544+
end
545+
463546
function Base.write(io::IO, model::Model{T}) where {T}
464547
MOI.FileFormats.create_unique_variable_names(
465548
model,
@@ -478,6 +561,14 @@ function Base.write(io::IO, model::Model{T}) where {T}
478561
end
479562
_write_constraint(io, model, variables, F, S)
480563
end
564+
nlp_block = get(model.ext, :nlp_block, nothing)
565+
if nlp_block !== nothing
566+
MOI.initialize(nlp_block.evaluator, [:ExprGraph])
567+
for i in 1:length(nlp_block.constraint_bounds)
568+
expr = MOI.constraint_expr(nlp_block.evaluator, i)
569+
_write_constraint(io, model, variables, expr)
570+
end
571+
end
481572
sense = MOI.get(model, MOI.ObjectiveSense())
482573
if sense == MOI.FEASIBILITY_SENSE
483574
println(io, "solve satisfy;")

test/runtests.jl

+93
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,99 @@ function test_model_filename()
10471047
return
10481048
end
10491049

1050+
function test_model_nlp_boolean()
1051+
model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Int}())
1052+
x = MOI.add_variables(model, 2)
1053+
MOI.add_constraint.(model, x, MOI.ZeroOne())
1054+
nlp = MOI.Nonlinear.Model()
1055+
a, b = x
1056+
MOI.Nonlinear.add_constraint(nlp, :(($a || $b)), MOI.EqualTo(1.0))
1057+
MOI.Nonlinear.add_constraint(nlp, :(($a && $b)), MOI.EqualTo(0.0))
1058+
backend = MOI.Nonlinear.ExprGraphOnly()
1059+
evaluator = MOI.Nonlinear.Evaluator(nlp, backend, x)
1060+
MOI.set(model, MOI.NLPBlock(), MOI.NLPBlockData(evaluator))
1061+
solver = MiniZinc.Optimizer{Int}(MiniZinc.Chuffed())
1062+
MOI.set(solver, MOI.RawOptimizerAttribute("model_filename"), "test.mzn")
1063+
index_map, _ = MOI.optimize!(solver, model)
1064+
@test MOI.get(solver, MOI.TerminationStatus()) === MOI.OPTIMAL
1065+
@test MOI.get(solver, MOI.ResultCount()) >= 1
1066+
y = [index_map[v] for v in x]
1067+
sol = round.(Bool, MOI.get(solver, MOI.VariablePrimal(), y))
1068+
@test (sol[1] || sol[2]) == true
1069+
@test (sol[1] && sol[2]) == false
1070+
@test read("test.mzn", String) ==
1071+
"var bool: x1;\nvar bool: x2;\nconstraint (x1 \\/ x2) == true;\nconstraint (x1 /\\ x2) == false;\nsolve satisfy;\n"
1072+
rm("test.mzn")
1073+
return
1074+
end
1075+
1076+
function test_model_nlp_boolean_nested()
1077+
model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Int}())
1078+
x = MOI.add_variables(model, 2)
1079+
MOI.add_constraint.(model, x, MOI.ZeroOne())
1080+
y = MOI.add_variable(model)
1081+
MOI.add_constraint(model, y, MOI.Integer())
1082+
MOI.add_constraint(model, y, MOI.Interval(0, 10))
1083+
nlp = MOI.Nonlinear.Model()
1084+
a, b = x
1085+
# a || (b && (y < 5))
1086+
MOI.Nonlinear.add_constraint(
1087+
nlp,
1088+
:(($a || ($b && ($y < 5)))),
1089+
MOI.EqualTo(1.0),
1090+
)
1091+
MOI.Nonlinear.add_constraint(nlp, :($a < 1), MOI.EqualTo(1.0))
1092+
backend = MOI.Nonlinear.ExprGraphOnly()
1093+
evaluator = MOI.Nonlinear.Evaluator(nlp, backend, x)
1094+
MOI.set(model, MOI.NLPBlock(), MOI.NLPBlockData(evaluator))
1095+
solver = MiniZinc.Optimizer{Int}(MiniZinc.Chuffed())
1096+
MOI.set(solver, MOI.RawOptimizerAttribute("model_filename"), "test.mzn")
1097+
index_map, _ = MOI.optimize!(solver, model)
1098+
@test MOI.get(solver, MOI.TerminationStatus()) === MOI.OPTIMAL
1099+
@test MOI.get(solver, MOI.ResultCount()) >= 1
1100+
sol_x = [index_map[v] for v in [x; y]]
1101+
sol = round.(Int, MOI.get(solver, MOI.VariablePrimal(), sol_x))
1102+
@test sol[1] == 0
1103+
@test sol[2] == 1
1104+
@test sol[3] < 5
1105+
@test read("test.mzn", String) ==
1106+
"var bool: x1;\nvar bool: x2;\nvar 0 .. 10: x3;\nconstraint (x1 \\/ (x2 /\\ (x3 < 5))) == true;\nconstraint (x1 < 1) == true;\nsolve satisfy;\n"
1107+
rm("test.mzn")
1108+
return
1109+
end
1110+
1111+
function test_model_nlp_boolean_jump()
1112+
model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Int}())
1113+
x = MOI.add_variables(model, 2)
1114+
MOI.add_constraint.(model, x, MOI.ZeroOne())
1115+
nlp = MOI.Nonlinear.Model()
1116+
a, b = x
1117+
MOI.Nonlinear.add_constraint(nlp, :(($a || $b) - 1.0), MOI.EqualTo(0.0))
1118+
MOI.Nonlinear.add_constraint(nlp, :(($a && $b) - 0.0), MOI.EqualTo(0.0))
1119+
backend = MOI.Nonlinear.ExprGraphOnly()
1120+
evaluator = MOI.Nonlinear.Evaluator(nlp, backend, x)
1121+
MOI.set(model, MOI.NLPBlock(), MOI.NLPBlockData(evaluator))
1122+
solver = MiniZinc.Optimizer{Int}(MiniZinc.Chuffed())
1123+
MOI.set(solver, MOI.RawOptimizerAttribute("model_filename"), "test.mzn")
1124+
index_map, _ = MOI.optimize!(solver, model)
1125+
@test MOI.get(solver, MOI.TerminationStatus()) === MOI.OPTIMAL
1126+
@test MOI.get(solver, MOI.ResultCount()) >= 1
1127+
y = [index_map[v] for v in x]
1128+
sol = round.(Bool, MOI.get(solver, MOI.VariablePrimal(), y))
1129+
@test (sol[1] || sol[2]) == true
1130+
@test (sol[1] && sol[2]) == false
1131+
@test read("test.mzn", String) ==
1132+
"var bool: x1;\nvar bool: x2;\nconstraint ((x1 \\/ x2) - 1) == false;\nconstraint ((x1 /\\ x2) - 0) == false;\nsolve satisfy;\n"
1133+
rm("test.mzn")
1134+
return
1135+
end
1136+
1137+
function test_model_solver_name()
1138+
solver = MiniZinc.Optimizer{Int}(MiniZinc.Chuffed())
1139+
@test MOI.get(solver, MOI.SolverName()) == "MiniZinc"
1140+
return
1141+
end
1142+
10501143
end
10511144

10521145
TestMiniZinc.runtests()

0 commit comments

Comments
 (0)