Advanced Usage

This page provides examples of usages of MiniZinc Python other than solving just a basic model instance.

Custom Output Type

You can change the type in which MiniZinc Python will provide its solutions. By default the output type will automatically be generated for every mode, but it can be changed by setting the output_type attribute of a model or instance. This can be useful if you require the data in a particular format for later use. The following example solves a task assignment problem and its result will store the solutions in a class with an additional method to check that the tasks in the solution are scheduled uniquely.

from dataclasses import InitVar, dataclass
from typing import List

from minizinc import Instance, Model, Solver


@dataclass
class TaskAssignment:
    task: List[int]
    objective: int
    __output_item: InitVar[str] = None

    def check(self) -> bool:
        return len(set(self.task)) == len(self.task)


gecode = Solver.lookup("gecode")
model = Model()
model.add_string(
    """
    int: n;
    set of int: DOM = 1..n;
    int: m;
    set of int: COD = 1..m;
    array[DOM,COD] of int: profit;

    array[DOM] of var COD: task;

    include "all_different.mzn";
    constraint all_different(task);

    solve maximize sum(w in DOM)
                (profit[w,task[w]]);
    """
)
model.output_type = TaskAssignment

inst = Instance(gecode, model)
inst["n"] = 4
inst["m"] = 5
inst["profit"] = [[7, 1, 3, 4, 6], [8, 2, 5, 1, 4], [4, 3, 7, 2, 5], [3, 1, 6, 3, 6]]


sol = inst.solve().solution
assert type(sol) == TaskAssignment

if sol.check:
    print("A valid assignment!")
else:
    print("A bad assignment!")

Defining Meta-Heuristics

Modellers will sometimes require the use of meta-heuristics to more efficiently solve their problem instances. MiniZinc Python can assist in the formatting of Meta-Heuristics by the use of the Instance.branch() method. This method allows you to make incremental (temporary) changes to a Instance object. This could, for example allow you to explore a different part of the search space.

The following example shows a manual implementation of the branch-and-bound algorithm used in various solvers. It first looks for any solution. Once a solution is found, a new constraint is added to ensure that the next solution has a higher objective value. The second step is repeated until no solutions can be found.

from minizinc import Instance, Model, Result, Solver, Status

gecode = Solver.lookup("gecode")
m = Model()
m.add_string(
    """
    array[1..4] of var 1..10: x;
    var int: obj;

    constraint obj = sum(x);
    output ["\\(obj)"];
    """
)
inst = Instance(gecode, m)

res: Result = inst.solve()
print(res.solution)
while res.status == Status.SATISFIED:
    with inst.branch() as child:
        child.add_string(f"constraint obj > {res['obj']};")
        res = child.solve()
        if res.solution is not None:
            print(res.solution)

Note that all constraints added to the child instance are removed once the with-context ends. For branch-and-bound the added constraints are complementary and do not have to be retracted. For other search algorithms this is not the case. The following example performs a simple Large Neighbourhood search. After finding an initial solution, the search will randomly fix 70% of its variables and try and find a better solution. If no better solution is found in the last 3 iterations, it will stop.

import random

from minizinc import Instance, Model, Result, Solver, Status

gecode = Solver.lookup("gecode")
m = Model()
m.add_string(
    """
    array[1..10] of var 1..10: x;
    var int: obj;

    constraint obj = sum(x);
    output ["\\(obj)"]
    """
)
inst = Instance(gecode, m)

res: Result = inst.solve()
incumbent = res.solution
i = 0
print(incumbent)
while i < 10:
    with inst.branch() as child:
        for i in random.sample(range(10), 7):
            child.add_string(f"constraint x[{i + 1}] == {incumbent.x[i]};\n")
        child.add_string(f"solve maximize obj;\n")
        res = child.solve()
        if res.solution is not None and res["obj"] > incumbent.obj:
            i = 0
            incumbent = res.solution
            print(incumbent)
        else:
            i += 1

Concurrent Solving

MiniZinc Python provides asynchronous methods for solving MiniZinc instances. These methods can be used to concurrently solve an instances and/or use some of pythons other functionality. The following code sample shows a MiniZinc instance that is solved by two solvers at the same time. The solver that solves the instance the fastest is proclaimed the winner!

import asyncio
import minizinc

# Lookup solvers to compete
chuffed = minizinc.Solver.lookup("chuffed")
gecode = minizinc.Solver.lookup("gecode")

# Create model
model = minizinc.Model(["nqueens.mzn"])
model["n"] = 16


async def solver_race(model, solvers):
    tasks = set()
    for solver in solvers:
        # Create an instance of the model for every solver
        inst = minizinc.Instance(solver, model)

        # Create a task for the solving of each instance
        task = asyncio.create_task(inst.solve_async())
        task.solver = solver.name
        tasks.add(task)

    # Wait on the first task to finish and cancel the other tasks
    done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
    for t in pending:
        t.cancel()

    # Declare the winner
    for t in done:
        print("{} finished solving the problem first!".format(t.solver))


asyncio.run(solver_race(model, [chuffed, gecode]))

Concurrent Solutions

MiniZinc Python provides an asynchronous generator to receive the generated solutions. The generator allows users to process solutions as they come in. The following example solves the n-queens problem and displays a board with the letter Q on any position that contains a queen.

import asyncio
import minizinc


async def show_queens(n):
    # Create model
    model = minizinc.Model(["nqueens.mzn"])
    model["n"] = n
    # Lookup solver
    gecode = minizinc.Solver.lookup("gecode")
    instance = minizinc.Instance(gecode, model)

    async for result in instance.solutions(all_solutions=True):
        if result.solution is None:
            continue
        queens = result["q"]

        for row in range(len(queens)):
            # Print line
            print("".join(["--" for _ in range(len(queens))]) + "-")
            print("|", end="")
            for col in range(len(queens)):
                if queens[row] == col:
                    print("Q|", end="")
                else:
                    print(" |", end="")
            print("")

        print("".join(["--" for _ in range(len(queens))]) + "-")

        print("\n --------------------------------- \n")


asyncio.run(show_queens(6))

Using multiple MiniZinc versions

MiniZinc Python is designed to be flexible in its communication with MiniZinc. That is why it is possible to switch to a different version of MiniZinc, or even use multiple versions of MiniZinc at the same time. This can be useful to compare different versions of MiniZinc.

In MiniZinc Python a MiniZinc executable or shared library is represented by a Driver object. The Driver.find() function can help finding a compatible MiniZinc executable or shared library and create an associated Driver object. The following example shows how to load two versions of MiniZinc and sets one as the new default.

from minizinc import Driver, Instance, Solver, default_driver

print(default_driver.minizinc_version)

v23: Driver = Diver.find(["/minizinc/2.3.2/bin"])
print(v23.minizinc_version)
gecode = Solver.lookup("gecode", driver=v23)
v23_instance = Instance(gecode, driver=v23)

v24: Driver = Driver.find(["/minizinc/2.4.0/bin"])
print(v24.minizinc_version)
gecode = Solver.lookup("gecode", driver=v24)
v24_instance = Instance(gecode, driver=v24)

v24.make_default()
print(default_driver.minizinc_version)
gecode = Solver.lookup("gecode")  # using the new default_driver
instance = Instance(gecode)

Debugging the Python to MiniZinc connection

This package has been setup to contain useful logging features to find any potential issues in the connections from Python to MiniZinc. The logging is implemented using Python’s default logging package and is always enabled. To view this log one has to set the preferred output mode and event level before importing the MiniZinc package. The following example will view all logged events and write them to a file:

import logging

logging.basicConfig(filename="minizinc-python.log", level=logging.DEBUG)

import minizinc

model = minizinc.Model(["nqueens.mzn"])
solver = minizinc.Solver.lookup("gecode")
instance = minizinc.Instance(solver, model)
instance["n"] = 9
print(instance.solve())

The generated file will contain all remote calls to the MiniZinc executable:

DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --version", timeout None
DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --version", timeout None
DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --solvers-json", timeout None
DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --model-interface-only nqueens.mzn", timeout None
DEBUG:minizinc:CLIInstance:analyse -> output fields: [('q', typing.List[int]), ('_checker', <class 'str'>)]
DEBUG:asyncio:Using selector: KqueueSelector
DEBUG:minizinc:CLIDriver:create_process -> program: /Applications/MiniZincIDE.app/Contents/Resources/minizinc args: "--solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --output-mode json --output-time --output-objective --output-output-item -s nqueens.mzn"
DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --version", timeout None
DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --solvers-json", timeout None
DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --model-interface-only nqueens.mzn", timeout None
DEBUG:minizinc:CLIInstance:analyse -> output fields: [('q', typing.List[int]), ('_checker', <class 'str'>)]
DEBUG:asyncio:Using selector: KqueueSelector
DEBUG:minizinc:CLIDriver:create_process -> program: /Applications/MiniZincIDE.app/Contents/Resources/minizinc args: "--solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --output-mode json --output-time --output-objective --output-output-item -s nqueens.mzn /var/folders/gj/cmhh026j5ddb28sw1z95pygdy5kk20/T/mzn_datau1ss0gze.json"
DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --version", timeout None
DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --solvers-json", timeout None
DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --model-interface-only nqueens.mzn", timeout None
DEBUG:minizinc:CLIInstance:analyse -> output fields: [('q', typing.List[int]), ('_checker', <class 'str'>)]
DEBUG:asyncio:Using selector: KqueueSelector
DEBUG:minizinc:CLIDriver:create_process -> program: /Applications/MiniZincIDE.app/Contents/Resources/minizinc args: "--solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --output-mode json --output-time --output-objective --output-output-item -s nqueens.mzn /var/folders/gj/cmhh026j5ddb28sw1z95pygdy5kk20/T/mzn_datamnhikzwo.json"

If MiniZinc Python instances are instantiated directly from Python, then the CLI driver will generate temporary files to use with the created MiniZinc process. When something seems wrong with MiniZinc Python it is often a good idea to retrieve these objects to both check if the files are generated correctly and to recreate the exact MiniZinc command that is ran. For a CLIInstance object, you can use the files() method to generate the files. You can then inspect or copy these files:

import minizinc
from pathlib import Path

model = minizinc.Model(["nqueens.mzn"])
solver = minizinc.Solver.lookup("gecode")
instance = minizinc.Instance(solver, model)
instance["n"] = 9

with instance.files() as files:
    store = Path("./tmp")
    store.mkdir()
    for f in files:
        f.link_to(store / f.name)

Finally, if you are looking for bugs related to the behaviour of MiniZinc, solvers, or solver libraries, then it could be helpful to have a look at the direct MiniZinc output on stderr. The CLIInstance class now has experimental support for to write the output from stderr to a file and to enable verbose compilation and solving (using the command line -v flag). The former is enable by providing a pathlib.Path object as the debug_output parameter to any solving method. Similarly, the verbose flag is triggered by setting the verbose parameter to True. The following fragment shows capture the verbose output of a model that contains trace statements (which are also captured on stderr):

import minizinc
from pathlib import Path

gecode = minizinc.Solver.lookup("gecode")
instance = minizinc.Instance(gecode)
instance.add_string("""
    constraint trace("--- TRACE: This is a trace statement\\n");
""")

instance.solve(verbose=True, debug_output=Path("./debug_output.txt"))