Tutorial¶
In this tutorial we will cover the basics of how to use PyMzn to solve MiniZinc problems inside Python scripts. Bare in mind that this tutorial is not meant to be an introduction to MiniZinc, so if you are new to MiniZinc go check out the official MiniZinc tutorial and then come back here to learn about how to use MiniZinc in Python.
Let’s start with a simple coloring problem of Australia (which happens to be the same of the MiniZinc tutorial):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | int: nc = 3;
var 1 .. nc: wa; var 1 .. nc: nt; var 1 .. nc: sa; var 1 .. nc: q;
var 1 .. nc: nsw; var 1 .. nc: v; var 1 .. nc: t;
constraint wa != nt;
constraint wa != sa;
constraint nt != sa;
constraint nt != q;
constraint sa != q;
constraint sa != nsw;
constraint sa != v;
constraint q != nsw;
constraint nsw != v;
solve satisfy;
output [
"wa=\(wa)\t nt=\(nt)\t sa=\(sa)\n",
"q=\(q)\t nsw=\(nsw)\t v=\(v)\n",
"t=", show(t), "\n"
];
|
Using PyMzn we can call MiniZinc to solve the above problem from a Python script and get the result as a Python object. Calling PyMzn is just as easy as:
import pymzn
solns = pymzn.minizinc('aust.mzn')
print(solns)
which will return a solution like:
[{'wa': 3, 'nt': 2, 'sa': 1, 'q': 3, 'nsw': 2, 'v': 3, 't': 1}]
The pymzn.minizinc
function takes as input the path to the MiniZinc file,
takes care of launching MiniZinc for you and parses the solution stream to get
back solutions as Python dictionaries. Internally, PyMzn takes care of how
solutions should be returned by MiniZinc in order to be able to parse them. By
default it will ignore the output statement on your MiniZinc model, so you can
leave it there, just in case. If instead you want to get solutions as strings
formatted according to the output statement present in your MiniZinc model, you
can use the option output_mode='item'
with the pymzn.minizinc
function:
solns = pymzn.minizinc('aust.mzn', output_mode='item')
print(solns[0])
which will print:
wa=3 nt=2 sa=1
q=3 nsw=2 v=3
t=1
There are other options for the output_mode
flag, namely dzn
, json
and raw
. The former two return strings formatted as dzn and json
respectively, while the latter does not return a list of solutions but rather
the entire solution stream as a single string.
As you may have noticed, the pymzn.minizinc
function returns an object of
type Solutions
which in most cases may be addressed and iterated as a list:
1 2 3 4 5 6 7 8 9 10 | solns = pymzn.minizinc('aust.mzn')
print(type(solns).__name__)
# Solutions
print(len(solns))
# 1
print(type(iter(solns)))
# list_iterator
|
The Solutions
object returned by the pymzn.minizinc
function has also
few other useful attributes, such as the status
attribute:
solns = pymzn.minizinc('aust.mzn')
print(solns.status)
# Status.INCOMPLETE
The status
attribute is an instance of a pymzn.Status
enum, which
represents the status of the solution stream, i.e. wheter it is complete or not,
whether the problem is unsatisfiable or there has been an error with the solver.
In this case the status is incomplete because it is a satisfiability problem and
the solver returned only one of the feasible solution.
To get all the solutions of the problem, we can use the all_solutions
flag:
1 2 3 4 5 6 7 | solns = pymzn.minizinc('aust.mzn', all_solutions=True)
print(len(solns))
# 18
print(solns.status)
# Status.COMPLETE
|
To be able to get all the solutions for a satisfiability problem, this feature needs to be supported by the solver. The default solver used by PyMzn is Gecode, which does support this feature.
Data¶
Let us now move on to another problem, a simple 0-1 knapsack encoded with MiniZinc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | int: n; % number of objects
set of int: OBJ = 1..n;
array[OBJ] of int: profit; % the profit of each object
array[OBJ] of int: size; % the size of each object
int: capacity; % the capacity of the knapsack
var set of OBJ: x;
constraint sum(i in x)(size[i]) <= capacity;
var int: obj = sum(i in x)(profit[i]);
solve maximize obj;
output [
"knapsack = ", show(x), "\n",
"objective = ", show(obj)
];
|
As you can see, in the above problem some data is missing, so we need to specify it via a data file or via inline data. Here is a data file for the above problem:
1 2 3 | n = 5;
profit = [10, 3, 9, 4, 8];
size = [14, 4, 10, 6, 9];
|
In the above file we are still missing the capacity
parameter, which we can
specify as inline data. To solve the above problem with the provided data we
use:
import pymzn
solns = pymzn.minizinc('knapsack01.mzn', 'knapsack01.dzn', data={'capacity': 20})
print(solns)
# [{'x': {3, 5}}]
The second argument we passed to the pymzn.minizinc
function is the data
file specified above. We also passed the keyword argument data
as a
dictionary assigning the value 20
to the variable capacity
. PyMzn will
automatically convert the dictionary into an appropriate dzn representation and
pass it to MiniZinc as inline data. PyMzn does so by using the function
pymzn.dict2dzn
which you can use yourself:
dzn = pymzn.dict2dzn({'capacity': 20})
print(dzn)
which will return a list of dzn assignments, one for each variable, in this case:
['capacity = 20;']
Conveniently, you can also save the dzn assignments directly on a file:
pymzn.dict2dzn({'capacity': 20}, fout='capacity.dzn')
This file can then be used in subsequent calls to the pymzn.minizinc
function. We can actually specify as many data files we need as additional
positional argument to the pymzn.minizinc
function, so the above problem can
now be solved with:
solns = pymzn.minizinc('knapsack01.mzn', 'knapsack01.dzn', 'capacity.dzn')
print(solns)
# [{'x': {3, 5}}]
If you want to get back the content of a data file into a Python dictionary,
PyMzn also offers the inverse function pymzn.dzn2dict
, which accepts the
path to a .dzn
file as input and returns the content of the file converted
to Python objects:
data = pymzn.dzn2dict('capacity.dzn')
print(data)
# {'capacity': 20}
The pymzn.dzn2dict
function also accept dzn content directly:
data = pymzn.dzn2dict('capacity = 20;')
print(data)
# {'capacity': 20}
You can read more about converting data to and from dzn format in the Dzn files section.
Solver arguments¶
Usually, solvers provide arguments that can be used to modify their behavior.
You can specify arguments to pass to the solver as additional keyword arguments
in the minizinc
function. When using Gecode, for example, the fzn_flags
option is available, which can take any flag described when executing
fzn-gecode --help
. The pymzn.minizinc
can take fzn_flags
as a
keyword argument, which will then be passed to the solver, e.g.:
pymzn.minizinc('test.mzn', fzn_flags=['-restart linear', '-restart-base 1.5'])
More details can be found in the Solvers section.