| # toulbar2 ## An exact solver for cost function networks

### Tutorials

- Weighted n-queen problem
- Weighted latin square problem
- Radio link frequency assignment problem
- Frequency assignment problem with polarization
- Mendelian error detection problem
- Block modeling problem
- Airplane landing problem
- Learning to play the Sudoku
- Renault car configuration system: learning user preferences

## Weighted n-queen problem

##### Brief description

The problem consists in assigning N queens on a NxN chessboard with random weights in (1..N) associated to every cell such that each queen does not attack another queen and the sum of the weights of queen's selected cells is minimized.

##### CFN model

A solution must have only one queen per column and per row. We create N variables for every column with domain size N to represent the selected row for each queen. A clique of binary constraints is used to express that two queens cannot be on the same row. Forbidden assignments have cost k=N**2+1. Two other cliques of binary constraints are used to express that two queens do not attack each other on a lower/upper diagonal.

##### Example for N=4 in JSON .cfn format

    {
    	problem: { "name": "4-queen", "mustbe": "<17" },
    	variables: {"Q0":["Row0", "Row1", "Row2", "Row3"], "Q1":["Row0", "Row1", "Row2", "Row3"],
    		    "Q2":["Row0", "Row1", "Row2", "Row3"], "Q3":["Row0", "Row1", "Row2", "Row3"]},
    	functions: {
    		{scope: ["Q0", "Q1"], "costs": [17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17]},
    		{scope: ["Q0", "Q2"], "costs": [17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17]},
    		{scope: ["Q0", "Q3"], "costs": [17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17]},
    		{scope: ["Q1", "Q2"], "costs": [17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17]},
    		{scope: ["Q1", "Q3"], "costs": [17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17]},
    		{scope: ["Q2", "Q3"], "costs": [17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17]},

    		{scope: ["Q0", "Q1"], "costs": [0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0]},
    		{scope: ["Q0", "Q2"], "costs": [0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0]},
    		{scope: ["Q0", "Q3"], "costs": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0]},
    		{scope: ["Q1", "Q2"], "costs": [0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0]},
    		{scope: ["Q1", "Q3"], "costs": [0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0]},
    		{scope: ["Q2", "Q3"], "costs": [0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0]},

    		{scope: ["Q0", "Q1"], "costs": [0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0]},
    		{scope: ["Q0", "Q2"], "costs": [0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0]},
    		{scope: ["Q0", "Q3"], "costs": [0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]},
    		{scope: ["Q1", "Q2"], "costs": [0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0]},
    		{scope: ["Q1", "Q3"], "costs": [0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0]},
    		{scope: ["Q2", "Q3"], "costs": [0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0, 17, 0, 0, 0, 0]},

    		{scope: ["Q0"], "costs": [4, 4, 3, 4]},
    		{scope: ["Q1"], "costs": [4, 3, 4, 4]},
    		{scope: ["Q2"], "costs": [2, 1, 3, 2]},
    		{scope: ["Q3"], "costs": [1, 2, 3, 4]}
    }

Optimal solution with cost 11 for the 4-queen example

##### Python model generator

The following code using python3 interpreter will generate the previous example if called without argument. Otherwise the first argument is the number of queens N (e.g. "python3 queens.py 8").

Notice that the first lines of code (import and functions flatten and cfn) are needed by all the other tutorial examples.

queens.py

    import sys
    from random import randint, seed
    seed(123456789)

    def flatten(x):
        result = []
        for el in x:
            if hasattr(el, "__iter__") and not isinstance(el, str) and not isinstance(el, tuple) and not isinstance(el, dict):
                result.extend(flatten(el))
            else:
                result.append(el)
        return result

    def cfn(problem, isMinimization, initPrimalBound, floatPrecision=0):
        globals_key_order = ["metric", "cost", "bounds", "vars1", "vars2", "nb_states", "starts", "ends", "transitions", "nb_symbols", "nb_values", "start", "terminals", "non_terminals", "min", "max", "values", "defaultcost", "tuples", "comparator", "to"]
        print('{')
        print('\tproblem: { "name": "%s", "mustbe": "%s%.*f" },' % (problem["name"], "<" if (isMinimization) else ">", floatPrecision, initPrimalBound))
        print('\tvariables: {', end='')
        for i,e in enumerate(flatten(problem["variables"])):
            if i > 0: print(', ', end='')
            print('"%s":' % e[0], end='')
            if isinstance(e[1], int):
                print(' %s' % e[1], end='')
            else:
                print('[', end='')
                for j,a in enumerate(e[1]):
                    if j > 0: print(', ', end='')
                    print('"%s"' % a, end='')
                print(']', end='')
        print('},')
        print( '\tfunctions: {')
        for i,e in enumerate(flatten(problem["functions"])):
            if i > 0: print(',')
            if e.get("name") is not None: print('\t\t"%s": {scope: [' % e.get("name"), end='')
            else: print('\t\t{scope: [', end='')
            scope = {}
            for j,x in enumerate(e.get("scope")):
                if (x in scope): sys.exit(str(e) + '\nError: scope of function ' + str(i) + ' with the same variable twice is forbidden!')
                if j > 0: print(', ', end='')
                print('"%s"' % x, end='')
                scope[x]=j
            print('], ', end='')
            if e.get("type") is not None:
                print('"type:" %s, ' % e.get("type"), end='')
            if e.get("params") is not None:
                if isinstance(e.get("params"), dict):
                    print('"params": {', end='')
                    first = True
                    for key in globals_key_order:
                        if key in e.get("params"):
                            if not first: print(', ', end='')
                            if isinstance(e.get("params")[key], str): print('"%s": "%s"' % (str(key),str(e.get("params")[key]).replace("'", '"')), end='')
                            else: print('"%s": %s' % (str(key),str(e.get("params")[key]).replace("'", '"')), end='')
                            first = False
                    print ('}', end='')
                else: print('"params": %s, ' % str(e.get("params")).replace("'",'"'), end='')
            if e.get("defaultcost") is not None:
                print('"defaultcost:" %s, ' % e.get("defaultcost"), end='')
            if e.get("costs") is not None:
                print('"costs": ', end='')
                if isinstance(e.get("costs"), str):
                    print('"%s"' % e.get("costs"), end='') # reuse future cost function by giving its name here
                else:
                    print('[', end='')
                    for j,c in enumerate(e.get("costs")):
                        if j > 0: print(', ', end='')
                        if isinstance(c, str) and not c.isdigit():
                            print('"%s"' % c, end='')
                        else:
                            print('%s' % c, end='')
                    print(']', end='')
            print('}', end='')
        print('}\n}')
    def model(N, k):
        Var = ["Q" + str(i) for i in range(N)]
        Queen = {
            "name": str(N) + "-queen",
            "variables": [(Var[i], ["Row" + str(j) for j in range(N)]) for i in range(N)],
            "functions":
                [
                 # permutation constraints expressed by a clique of binary constraints
                 [{"scope": [Var[i], Var[j]], "costs": [0 if a != b else k for a in range(N) for b in range(N)]} for i in range(N) for j in range(N) if (i < j)],
                 # upper diagonal constraints
                 [{"scope": [Var[i], Var[j]], "costs": [0 if a + i != b + j else k for a in range(N) for b in range(N)]} for i in range(N) for j in range(N) if (i < j)],
                 # lower diagonal constraints
                 [{"scope": [Var[i], Var[j]], "costs": [0 if a - i != b - j else k for a in range(N) for b in range(N)]} for i in range(N) for j in range(N) if (i < j)],
                 # random unary costs
                 [{"scope": [Var[i]], "costs": [randint(1,N) for a in range(N)]} for i in range(N)]
                ]
             }
        return Queen

    if __name__ == '__main__':
        # read parameters
        N = int(sys.argv[1]) if len(sys.argv) > 1 else 4
        # infinite cost
        k = N**2+1
        # dump problem into JSON .cfn format for minimization
        cfn(model(N, k), True, k)

##

## Weighted latin square problem

##### Brief description

The problem consists in assigning a value from 0 to N-1 to every cell of a NxN chessboard. Each row and each column must be a permutation of N values. For each cell, a random weight in (1...N) is associated to every domain value. The objective is to find a complete assignment where the sum of the weights associated to the selected values for the cells is minimized.

##### CFN model

We create NxN variables for all cells with domain size N. A hard AllDifferent global cost function is used to model a permutation for every row and every column. Random weights are generated for every cell and domain value. Forbidden assignments have cost k=N**3+1.

##### Example for N=4 in JSON .cfn format

    {
    	problem: { "name": "LatinSquare4", "mustbe": "<65" },
    	variables: {"X0_0": 4, "X0_1": 4, "X0_2": 4, "X0_3": 4, "X1_0": 4, "X1_1": 4, "X1_2": 4, "X1_3": 4, "X2_0": 4, "X2_1": 4, "X2_2": 4, "X2_3": 4, "X3_0": 4, "X3_1": 4, "X3_2": 4, "X3_3": 4},
    	functions: {
    		{scope: ["X0_0", "X0_1", "X0_2", "X0_3"], "type:" salldiff, "params": {"metric": "var", "cost": 65}},
    		{scope: ["X1_0", "X1_1", "X1_2", "X1_3"], "type:" salldiff, "params": {"metric": "var", "cost": 65}},
    		{scope: ["X2_0", "X2_1", "X2_2", "X2_3"], "type:" salldiff, "params": {"metric": "var", "cost": 65}},
    		{scope: ["X3_0", "X3_1", "X3_2", "X3_3"], "type:" salldiff, "params": {"metric": "var", "cost": 65}},

    		{scope: ["X0_0", "X1_0", "X2_0", "X3_0"], "type:" salldiff, "params": {"metric": "var", "cost": 65}},
    		{scope: ["X0_1", "X1_1", "X2_1", "X3_1"], "type:" salldiff, "params": {"metric": "var", "cost": 65}},
    		{scope: ["X0_2", "X1_2", "X2_2", "X3_2"], "type:" salldiff, "params": {"metric": "var", "cost": 65}},
    		{scope: ["X0_3", "X1_3", "X2_3", "X3_3"], "type:" salldiff, "params": {"metric": "var", "cost": 65}},

    		{scope: ["X0_0"], "costs": [4, 4, 3, 4]},
    		{scope: ["X0_1"], "costs": [4, 3, 4, 4]},
    		{scope: ["X0_2"], "costs": [2, 1, 3, 2]},
    		{scope: ["X0_3"], "costs": [1, 2, 3, 4]},
    		{scope: ["X1_0"], "costs": [3, 1, 3, 3]},
    		{scope: ["X1_1"], "costs": [4, 1, 1, 1]},
    		{scope: ["X1_2"], "costs": [4, 1, 1, 3]},
    		{scope: ["X1_3"], "costs": [4, 4, 1, 4]},
    		{scope: ["X2_0"], "costs": [1, 3, 3, 2]},
    		{scope: ["X2_1"], "costs": [2, 1, 3, 1]},
    		{scope: ["X2_2"], "costs": [3, 4, 2, 2]},
    		{scope: ["X2_3"], "costs": [2, 3, 1, 3]},
    		{scope: ["X3_0"], "costs": [3, 4, 4, 2]},
    		{scope: ["X3_1"], "costs": [3, 2, 4, 4]},
    		{scope: ["X3_2"], "costs": [4, 1, 3, 4]},
    		{scope: ["X3_3"], "costs": [4, 4, 4, 3]}
    }

Optimal solution with cost 35 for the latin 4-square example (in red, weights associated to the selected values)

##### Python model generator

The following code using python3 interpreter will generate the previous example if called without argument. Otherwise the first argument is the dimension N of the chessboard (e.g. "python3 latinsquare.py 6").

latinsquare.py

    def model(N, k):
        Var = {(i,j): "X" + str(i) + "_" + str(j) for i in range(N) for j in range(N)}
        LatinSquare = {
            "name": "LatinSquare" + str(N),
            "variables": [(Var[(i,j)], N) for i in range(N) for j in range(N)],
            "functions":
                [# permutation constraints on rows
                    [{"scope": [Var[(i,j)] for j in range(N)], "type": "salldiff", "params": {"metric": "var", "cost": k}} for i in range(N)],
                 # permutation constraints on columns
                 [{"scope": [Var[(i,j)] for i in range(N)], "type": "salldiff", "params": {"metric": "var", "cost": k}} for j in range(N)],
                  # random unary costs on every cell
                 [{"scope": [Var[(i,j)]], "costs": [randint(1, N) for a in range(N)]} for i in range(N) for j in range(N)]
                ]
        }
        return LatinSquare

    if __name__ == '__main__':
        # read parameters
        N = int(sys.argv[1]) if len(sys.argv) > 1 else 4
        # infinite cost
        k = N**3+1
        # dump problem into JSON .cfn format for minimization
        cfn(model(N, k), True, k)

##

## Radio link frequency assignment problem

##### Brief description

The problem consists in assigning frequencies to radio communication links in such a way that no interferences occurs. Domains are set of integers (non necessarily consecutives). Two types of constraints occur: (I) the absolute difference between two frequencies should be greater than a given number d_i (|x-y|>d_i), or (II) the absolute difference between two frequencies should exactly be equal to a given number d_i (|x-y|=d_i). Different deviations d_i, i in 0..4, may exist for the same pair of links. d_0 corresponds to hard constraints while higher deviations are soft constraints that can be violated with an associated cost a_i. Moreover, pre-assigned frequencies may be known for some links which are either hard or soft preferences (mobility cost b_i, i in 0..4). The goal is to minimize the weighted sum of violated constraints. Cabon, B., de Givry, S., Lobjois, L., Schiex, T., Warners, J.P. Constraints (1999) 4: 79.

##### CFN model

We create N variables for every radio link with a given integer domain. Hard and soft binary cost functions express interference constraints with possible deviations. Unary cost functions are used to model mobility costs.

##### Data

Original data files can be download from the cost function library FullRLFAP. Their format is described here. You can try a small example CELAR6-SUB1 (var.txt, dom.txt, ctr.txt, cst.txt) with optimum value equal to 2669.

##### Python model generator

The following code using python3 interpreter will generate the corresponding cost function network (e.g. "python3 rlfap.py var.txt dom.txt ctr.txt cst.txt").

rlfap.py

    class Data:
        def __init__(self, var, dom, ctr, cst):
            self.var = list()
            self.dom = {}
            self.ctr = list()
            self.cost = {}
            self.nba = {}
            self.nbb = {}
            self.top = 0

            stream = open(var)
            for line in stream:
                if len(line.split())>=4:
                    (varnum, vardom, value, mobility) = line.split()[:4]
                    self.var.append((int(varnum), int(vardom), int(value), int(mobility)))
                    self.nbb["b" + str(mobility)] = self.nbb.get("b" + str(mobility), 0) + 1
                else:
                    (varnum, vardom) = line.split()[:2]
                    self.var.append((int(varnum), int(vardom)))

            stream = open(dom)
            for line in stream:
                domain = line.split()[:]
                self.dom[int(domain[0])] = [int(f) for f in domain[2:]]

            stream = open(ctr)
            for line in stream:
                (var1, var2, dummy, operand, deviation, weight) = line.split()[:6]
                self.ctr.append((int(var1), int(var2), operand, int(deviation), int(weight)))
                self.nba["a" + str(weight)] = self.nba.get("a" + str(weight), 0) + 1

            stream = open(cst)
            for line in stream:
                if len(line.split()) == 3:
                    (aorbi, eq, cost) = line.split()[:3]
                    if (eq == "="):
                        self.cost[aorbi] = int(cost)
                        self.top += int(cost) * self.nba.get(aorbi, self.nbb.get(aorbi, 0))

    def model(data):
        Var = {e[0]: "X" + str(e[0]) for e in data.var}
        Domain = {e[0]: e[1] for e in data.var}
        RLFAP = {
            "name": "RLFAP",
            "variables": [(Var[e[0]], ["f" + str(f) for f in data.dom[e[1]]]) for e in data.var],
            "functions":
                [# hard and soft interference
                 [{"scope": [Var[var1], Var[var2]], "costs": [0 if ((operand==">" and abs(a - b)>deviation) or (operand=="=" and abs(a - b)==deviation))
                                                                else data.cost.get("a"+str(weight),data.top)
                                                              for a in data.dom[Domain[var1]] for b in data.dom[Domain[var2]]]}
                  for (var1, var2, operand, deviation, weight) in data.ctr],
                 # mobility costs
                 [{"scope": [Var[e[0]]], "defaultcost": data.cost.get("b"+str(e[3]),data.top), "costs": ["f" + str(e[2]), 0] if e[2] in data.dom[e[1]] else []}
                  for e in data.var if len(e)==4]
                ]
        }
        return RLFAP

    if __name__ == '__main__':
        # read parameters
        if len(sys.argv) < 5: exit('Command line arguments are filenames: var.txt dom.txt ctr.txt cst.txt')
        data = Data(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
        # dump problem into JSON .cfn format for minimization
        cfn(model(data), True, data.top)

##

## Frequency Assignment Problem with Polarization

##### Brief description

The previously-described radio link frequency assignment problem has been extended to take into account polarization constraints and user-defined relaxation of electromagnetic compatibility constraints. The problem is to assign a pair (frequency,polarization) to every radio communication link (also called a path). Frequencies are integer values taken in finite domains. Polarizations are in {-1,1}. Constraints are:

- (I) two paths must use equal or different frequencies (f_i=f_j or f_i<>f_j),
- (II) the absolute difference between two frequencies should exactly be equal or different to a given number e (|f_i-f_j|=e or |f_i-f_j|<>e),
- (III) two paths must use equal or different polarizations (p_i=p_j or p_i<>p_j),
- (IV) the absolute difference between two frequencies should be greater at a relaxation level l (0 to 10) than a given number g_l (resp. d_l) if polarization are equal (resp. different) (|f_i-f_j|>=g_l if p_i=p_j else |f_i-f_j|>=d_l), with g_(l-1)>g_l, d_(l-1)>d_l, and usually g_l>d_l.

Constraints (I) to (III) are mandatory constraints, while constraints (IV) can be relaxed. The goal is to find a feasible assignment with the smallest relaxation level l and which minimizes the number of violations of (IV) at lower levels. See ROADEF Challenge 2001.

##### CFN model

In order to benefit from soft local consistencies on binary cost functions, we create a single variable to represent a pair (frequency,polarization) for every radio link.

##### Data

Original data files can be download from ROADEF or fapp. Their format is described here. You can try a small example exemple1.in (resp. exemple2.in) with optimum 523 at relaxation level 3 with 1 violation at level 2 and 3 below (resp. 13871 at level 7 with 1 violation at level 6 and 11 below). See ROADEF Challenge 2001 results.

##### Python model generator

The following code using python3 interpreter will generate the corresponding cost function network (e.g. "python3 fapp.py exemple1.in 3"). You can also compile fappeval.c using "gcc -o fappeval fappeval.c" and download sol2fapp.awk in order to evaluate the solutions (e.g., "python3 fapp.py exemple1.in 3 | toulbar2 --stdin=cfn -s=3 | awk -f ./sol2fapp.awk - exemple1").

fapp.py

    class Data:
        def __init__(self, filename, k):
            self.var = list()
            self.dom = {}
            self.ctr = list()
            self.softeq = list()
            self.softne = list()
            self.nbsoft = 0
            self.top = 1
            self.cst = 0

            stream = open(filename)
            for line in stream:
                if len(line.split())==3 and line.split()[0]=="DM":
                    (DM, dom, freq) = line.split()[:3]
                    if self.dom.get(int(dom)) is None:
                        self.dom[int(dom)] = [int(freq)]
                    else:
                        self.dom[int(dom)].append(int(freq))

                if len(line.split()) == 4 and line.split()[0]=="TR":
                    (TR, route, dom, polarisation) = line.split()[:4]
                    if int(polarisation) is 0:
                        self.var.append((int(route), [(f,0) for f in self.dom[int(dom)]] + [(f,1) for f in self.dom[int(dom)]]))
                    if int(polarisation) is -1:
                        self.var.append((int(route), [(f,0) for f in self.dom[int(dom)]]))
                    if int(polarisation) is 1:
                        self.var.append((int(route), [(f,1) for f in self.dom[int(dom)]]))

                if len(line.split())==6 and line.split()[0]=="CI":
                    (CI, route1, route2, vartype, operator, deviation) = line.split()[:6]
                    self.ctr.append((int(route1), int(route2), vartype, operator, int(deviation)))

                if len(line.split())==14 and line.split()[0]=="CE":
                    (CE, route1, route2, s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10) = line.split()[:14]
                    self.softeq.append((int(route1), int(route2), [int(s) for s in [s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10]]))
                    self.nbsoft += 1

                if len(line.split())==14 and line.split()[0]=="CD":
                    (CD, route1, route2, s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10) = line.split()[:14]
                    self.softne.append((int(route1), int(route2), [int(s) for s in [s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10]]))

            self.cst = 10*k*self.nbsoft**2
            self.top += self.cst
            self.top += 10*self.nbsoft**2

    def model(data, k):
        Var = {e[0]: "X" + str(e[0]) for e in data.var}
        Domain = {e[0]: e[1] for e in data.var}
        FAPP = {
            "name": "FAPP",
            "variables": [(Var[e[0]], ["f" + str(f) + "p" + str(p) for (f,p) in e[1]]) for e in data.var],
            "functions":
                [# hard constraints (I) to (III)
                 [{"scope": [Var[route1], Var[route2]], "costs": [0 if ((operand=="I" and abs((f1 if vartype=="F" else p1) - (f2 if vartype=="F" else p2)) != deviation)
                                                                        or (operand=="E" and abs((f1 if vartype=="F" else p1) - (f2 if vartype=="F" else p2)) == deviation))
                                                                    else data.top
                                                                  for (f1,p1) in Domain[route1] for (f2,p2) in Domain[route2]]}
                  for (route1, route2, vartype, operand, deviation) in data.ctr],
                 # soft equality constraints (IV)
                 [{"scope": [Var[route1], Var[route2]], "costs": [0 if p1!=p2 or abs(f1 - f2) >= deviations[i]
                                                                    else (data.top if i>=k
                                                                                   else (1 if i < k-1
                                                                                           else 10*data.nbsoft))
                                                                  for (f1, p1) in Domain[route1] for (f2, p2) in Domain[route2]]}
                    for i in range(11) for (route1, route2, deviations) in data.softeq],
                 # soft inequality constraints (IV)
                 [{"scope": [Var[route1], Var[route2]], "costs": [0 if p1==p2 or abs(f1 - f2) >= deviations[i]
                                                                    else (data.top if i>=k
                                                                                   else (1 if i < k-1
                                                                                           else 10*data.nbsoft))
                                                                  for (f1, p1) in Domain[route1] for (f2, p2) in Domain[route2]]}
                    for i in range(11) for (route1, route2, deviations) in data.softne],
                 # constant cost to be added corresponding to the relaxation level
                 {"scope": [], "defaultcost": data.cst, "costs": []}
                 ]
        }
        return FAPP

    if __name__ == '__main__':
        # read parameters
        if len(sys.argv) < 2: exit('Command line argument is problem-data-filename and relaxation-level')
        k = int(sys.argv[2])
        data = Data(sys.argv[1], k)
        # dump problem into JSON .cfn format for minimization
        cfn(model(data, k), True, data.top)

##

## Mendelian error detection problem

##### Brief description

The problem is to detect marker genotyping incompatibilities (Mendelian errors only) in complex pedigrees. The input is a pedigree data with partial observed genotyping data at a single locus. The problem is to assign genotypes (unordered pairs of alleles) to all individuals such that they are compatible with the Mendelian law of heredity and with the maximum number of genotyping data. Sanchez, M., de Givry, S. and Schiex, T. Constraints (2008) 13:130.

##### CFN model

We create N variables for every individual genotype with domain being all possible unordered pairs of existing alleles. Hard ternary cost functions express mendelian law of heredity. Unary cost functions are used to model potential genotyping errors.

##### Data

Original data files can be download from the cost function library pedigree. Their format is described here. You can try a small example simple.pre (simple.pre) with optimum value equal to 1.

##### Python model generator

The following code using python3 interpreter will generate the corresponding cost function network (e.g. "python3 mendel.py simple.pre").

mendel.py

    class Data:
        def __init__(self, ped):
            self.id = list()
            self.father = {}
            self.mother = {}
            self.alleles = {}
            self.freq = {}
            self.obs = 0

            stream = open(ped)
            for line in stream:
                (locus, id, father, mother, sex, allele1, allele2) = line.split()[:]
                self.id.append(int(id))
                self.father[int(id)] = int(father)
                self.mother[int(id)] = int(mother)
                self.alleles[int(id)] = (int(allele1), int(allele2)) if int(allele1) < int(allele2) else (int(allele2), int(allele1))
                if int(allele1) != 0: self.freq[int(allele1)] = self.freq.get(int(allele1), 0) + 1
                if int(allele2) != 0: self.freq[int(allele2)] = self.freq.get(int(allele2), 0) + 1
                if int(allele1) != 0 or int(allele2) != 0: self.obs += 1

    def model(data, k):
        Var = {g: "g" + str(g) for g in data.id}
        Domain = ["a" + str(a1) + "a" + str(a2)  for a1 in data.freq for a2 in data.freq if a1 <= a2]
        Mendel = {
            "name": "Mendel",
            "variables": [(Var[g], Domain) for g in data.id],
            "functions":
                [# mendelian law of heredity
                 [{"scope": [Var[data.father[g]], Var[data.mother[g]], Var[g]],
                   "costs": [0 if (a1 in (p1,p2) and a2 in (m1,m2)) or (a2 in (p1,p2) and a1 in (m1,m2)) else k
                             for p1 in data.freq for p2 in data.freq
                             for m1 in data.freq for m2 in data.freq
                             for a1 in data.freq for a2 in data.freq if p1 <= p2 and m1 <= m2 and a1 <= a2]}
                  for g in data.id if data.father.get(g, 0) != 0 and data.mother.get(g, 0) != 0],
                 # observation costs
                 [{"scope": [Var[g]],
                   "costs": [0 if (a1,a2) == data.alleles[g] else 1 for a1 in data.freq for a2 in data.freq if a1 <= a2]}
                  for g in data.id if data.alleles[g][0] != 0 and data.alleles[g][1] != 0]
                ]
        }
        return Mendel

    if __name__ == '__main__':
        # read parameters
        if len(sys.argv) < 2: exit('Command line arguments are PEDFILE filename: simple.pre')
        data = Data(sys.argv[1])
        # dump problem into JSON .cfn format for minimization
        cfn(model(data, data.obs + 1), True, data.obs + 1)

##

## Block modeling problem

##### Brief description

This is a clustering problem, occuring in social network analysis. The problem is to divide a given graph G into k clusters such that the interactions between clusters can be summarized by a k*k 0/1 matrix M: if M[i,j]=1 then all the nodes in cluster i should be connected to all the nodes in cluster j in G, else if M[i,j]=0 then there should be no edge between the nodes in G. The goal is to find a k-clustering and the associated matrix M minimizing the number of erroneous edges. A Mattenet, I Davidson, S Nijssen, P Schaus. Generic Constraint-Based Block Modeling Using Constraint Programming. CP 2019, pp656-673, Stamford, CT, USA.

##### CFN model

We create N variables for every node of the graph with domain size k. We add k*k Boolean variables for representing M. For all triplets of two nodes u, v, and one matrix cell M[i,j], we have a ternary cost function which returns a cost of 1 if node u is assigned to cluster i, v to j, and M[i,j]=1 but (u,v) is not in G, or M[i,j]=0 and (u,v) in G. In order to break symmetries, we constrain the first k-1 node variables to be assigned to cluster index less than or equal to their index

##### Data

You can try a small example simple.mat (simple.mat) with optimum value equal to 0 for 3 clusters. See other examples (*.mat) such as PoliticalActor in TUTORIALS.

##### Python model generator

The following code using python3 interpreter will generate the corresponding cost function network (e.g. "python3 simple.mat 3").

blockmodel.py

    class Data:
        def __init__(self, filename, k):
            lines = open(filename).readlines()
            self.n = len(lines)
            self.matrix = [[int(e) for e in l.split(' ')] for l in lines]
            self.top = 1 + self.n*self.n

    def model(data, K):
        Var = [(chr(65 + i) if data.n < 28 else "x" + str(i)) for i in range(data.n)] # Political actor or any instance
        BlockModeling = {
            "name": "BlockModel_N" + str(data.n) + "_K" + str(K),
            "variables": [[("M_" + str(u) + "_" + str(v), 2) for u in range(K) for v in range(K)],
                          [(Var[i], K)  for i in range(data.n)]],
            "functions":
                [
                    # objective function
                    [{"scope": ["M_" + str(u) + "_" + str(v), Var[i], Var[j]],
                      "costs": [1 if (u == k and v == l and data.matrix[i][j] != m)
                                else 0
                                for m in range(2)
                                for k in range(K)
                                for l in range(K)]}
                     for u in range(K) for v in range(K) for i in range(data.n) for j in range(data.n) if i != j],

                    # self-loops
                    [{"scope": ["M_" + str(u) + "_" + str(u), Var[i]],
                      "costs": [1 if (u == k and data.matrix[i][i] != m)
                                else 0
                                for m in range(2)
                                for k in range(K)]}
                     for u in range(K) for i in range(data.n)],

                    # breaking partial symmetries by fixing first (K-1) domain variables to be assigned to cluster less than or equal to their index
                    [{"scope": [Var[l]],
                     "costs": [data.top if k > l else 0 for k in range(K)]}
                     for l in range(K-1)]
                ]
        }
        return BlockModeling

    if __name__ == '__main__':
        # read parameters
        if len(sys.argv) < 2: exit('Command line argument is problem data filename and number of blocks')
        K = int(sys.argv[2])
        data = Data(sys.argv[1], K)
        # dump problem into JSON .cfn format for minimization
        cfn(model(data, K), True, data.top)

We improve the previous model by sorting node variables by decreasing out degree and removing the lower triangular matrix of M if the input graph is symmetric.

blockmodel2.py

    def model(data, K):
        symmetric = all([data.matrix[i][j] == data.matrix[j][i] for i in range(data.n) for j in range(data.n) if j>i])
        Var = [(chr(65 + i) if data.n < 28 else "x" + str(i)) for i in range(data.n)]

        # sort node variables by decreasing out degree
        degree = [(i, sum(data.matrix[i])) for i in range(data.n)]
        degree.sort(key=lambda tup: -tup[1])
        indexes = [e[0] for e in degree]

        BlockModeling = {
            "name": "BlockModel_N" + str(data.n) + "_K" + str(K) + "_Sym" + str(symmetric),
            # order node variables before matrix M variables
            # order matrix M variables starting from the main diagonal and moving away progressively
            # if input graph is symmetric then keep only the upper triangular matrix of M
            "variables": [[("M_" + str(u) + "_" + str(u), 2) for u in range(K)],
                          [("M_" + str(u) + "_" + str(v), 2) for d in range(K) for u in range(K) for v in range(K)
                           if u != v and (not symmetric or u < v) and abs(u - v) == d],
                          [(Var[indexes[i]], K)  for i in range(data.n)]],
            "functions":
                [
                    # objective function
                    # if input graph is symmetric then cost tables are also symmetric wrt node variables
                    [{"scope": ["M_" + str(u) + "_" + str(v), Var[indexes[i]], Var[indexes[j]]],
                      "costs": [1 if (((u == k and v == l) or (symmetric and u == l and v == k))
                                      and data.matrix[indexes[i]][indexes[j]] != m)
                                else 0
                                for m in range(2)
                                for k in range(K)
                                for l in range(K)]}
                     for u in range(K) for v in range(K) for i in range(data.n) for j in range(data.n)
                     if i != j and (not symmetric or u <= v)],

                    # self-loops
                    [{"scope": ["M_" + str(u) + "_" + str(u), Var[indexes[i]]],
                      "costs": [1 if (u == k and data.matrix[indexes[i]][indexes[i]] != m)
                                else 0
                                for m in range(2)
                                for k in range(K)]}
                     for u in range(K) for i in range(data.n)],

                    # breaking partial symmetries by fixing first (K-1) domain variables to be assigned to cluster less than or equal to their index
                    [{"scope": [Var[indexes[l]]],
                      "costs": [data.top if k > l else 0 for k in range(K)]}
                     for l in range(K-1)]
                ]
        }
        return BlockModeling

##

## Airplane landing problem

##### Brief description (from CHOCO-SOLVER)

Given a set of planes and runways, the objective is to minimize the total weighted deviation from the target landing time for each plane. We consider only a single runway. There are costs associated with landing either earlier or later than a target landing time for each plane. Each plane has to land within its predetermined time window such that separation times between all pairs of planes are satisfied. J.E. Beasley, M. Krishnamoorthy, Y.M. Sharaiha and D. Abramson. Scheduling aircraft landings - the static case. Transportation Science, vol.34, 2000.

##### CFN model

We create N variables for every plane landing time. Binary cost functions express separation times between pairs of planes. Unary cost functions represent the weighted deviation for each plane.

##### Data

Original data files can be download from the cost function library airland. Their format is described here. You can try a small example airland1.txt with optimum value equal to 700.

##### Python model and solve

The following code uses the python3 module of toulbar2 and its CFN.py interface to generate the cost function network and solve it (e.g. "python3 airland.py airland1.txt"). Compile toulbar2 with "cmake -DPYTB2=ON . ; make" an copy the module in your current directory "cp lib/Linux/pytoulbar2.cpython* ."

airland.py

    import sys
    import CFN

    f = open(sys.argv[1], 'r').readlines()

    tokens = []
    for l in f:
        tokens += l.split()

    pos = 0

    def token():
       global pos, tokens
       if (pos == len(tokens)):
          return None
       s = tokens[pos]
       pos += 1
       return int(float(s))

    N = token()
    token() # skip freeze time

    LT = []
    PC = []
    ST = []

    for i in range(N):
       token()  # skip appearance time
    # Times per plane: {earliest landing time, target landing time, latest landing time}
       LT.append([token(), token(), token()])

    # Penalty cost per unit of time per plane:
    # [for landing before target, after target]
       PC.append([token(), token()])

    # Separation time required after i lands before j can land
       ST.append([token() for j in range(N)])

    top = 99999

    Problem = CFN.CFN(top)
    for i in range(N):
        Problem.AddVariable('x' + str(i), range(LT[i][0],LT[i][2]+1))

    for i in range(N):
        Problem.AddFunction([i], [PC[i][0]*abs(a-LT[i][1]) for a in range(LT[i][0], LT[i][2]+1)])

    for i in range(N):
        for j in range(i+1,N):
            Problem.AddFunction([i, j], [top*(a+ST[i][j]>b and b+ST[j][i]>a) for a in range(LT[i][0], LT[i][2]+1) for b in range(LT[j][0], LT[j][2]+1)])

    Problem.Dump('airplane.cfn')
    Problem.NoPreprocessing()
    Problem.Solve()