Sending Constraints to Multiple Solvers

## 17.4 Sending Constraints to Multiple Solvers

- Syntax and Motivation
- Handling Booleans with Linear Constraints
- Handling Disjunctions
- A More Realistic Example

### 17.4.1 Syntax and Motivation

Because of the cooperation between solvers, it is often useful to send constraints to multiple solvers. A linear constraint, such as X+2 ≥ Y, can be posted to `eplex` by the code `eplex: (X+2 $>= Y)`. The same constraint can be posted to `ic` by the code `ic: (X+2 $>= Y)`. The constraint can be sent to both solvers by the code

>        [ic,eplex]: (X+2 $>= Y)

By sending constraints to both solvers, where possible, we can improve search algorithms for solving constraint problems. Through enhanced constraint reasoning at each node of the search tree we can:

- prune the search tree, thus improving efficiency
- render the algorithm less sensitive to search heuristics

The second advantage is a particular benefit of combining different solvers, as opposed to enhancing the reasoning power of a single solver. See [22] and [23] for experimental results and application examples using multiple solvers in this way.

### 17.4.2 Handling Booleans with Linear Constraints

The `overlap` constraint example above is disjunctive and therefore non-linear, and is only handled by `ic`. However as soon as the boolean variable is labelled to 1, during search, the constraint becomes linear.
The cooperation between the `eplex` and `ic` solvers could therefore be improved by passing the resulting linear constraint to `eplex` as soon as the boolean is labelled to 1. This could be achieved using a constraint handling rule (see CHR) or a suspended goal (see chapter 14).
However the same improved cooperation can be achieved by a well known mathematical programming technique (see e.g. [30]) that builds the boolean variable into a linear constraint that can be sent to `eplex` even before the boolean is instantiated. This linear constraint effectively enforces the overlap constraint if the boolean is instantiated to 1, but does not enforce it if the boolean is instantiated to 0.
To achieve this we introduce sufficiently big multipliers, that when the boolean is set to 0 the constraint is satisfied for all values within the variables' bounds. This method is known as the bigM transformation.
It is illustrated in the following encoding of `pos_overlap`:

>     pos_overlap(Start,Duration,Time,Bool) :-
>             % if Bool is 1 then the task with start time Start and
>             % duration Duration overlaps time point Time
>             Max1 is maxdiff(Start,Time),
>             Max2 is maxdiff(Time,Start+Duration-1),
>             eplex: (Time+(1-Bool)*Max1 $>= Start),              % lin1
>             eplex: (Time $=< Start+Duration-1+(1-Bool)*Max2).   % lin2

>     maxdiff(Expr1,Expr2,MaxDiff) :-
>             % the maximum diffrence between Expr1 and Expr2 is the max val
>             % of (Expr1 - Expr2)
>             MaxDiff is max_val(Expr1 - Expr2).

>     max_val(Expr, Max) :-
>             % the maximum value of a variable is its upper bound
>             var(Expr),!,
>             get_var_bounds(Expr, _, Max).
>     max_val(Expr, Max) :-
>             % the maximum value of a number is itself
>             number(Expr),!,
>             Max = Expr.
>     max_val(Expr1 + Expr2, Max) :-
>             % the maximum value of (Exrp1 + Expr2) is the maximum value of
>             % Expr1 plus the maximum value of Expr2
>             Max is max_val(Expr1) + max_val(Expr2).
>     max_val(Expr1 - Expr2, Max) :-
>             % the maximum value of (Exrp1 - Expr2) is the maximum value of
>             % Expr1 minus the minimum value of Expr2
>             Max is max_val(Expr1) - min_val(Expr2).

>     min_val(Expr, Min) :-
>             % the minimum value of a variable is its lower bound
>             var(Expr),!,
>             get_var_bounds(Expr, Min, _).
>     min_val(Expr, Min) :-
>             % the minimum value of a number is itself
>             number(Expr),!,
>             Min = Expr.
>     min_val(Expr1 + Expr2, Max) :-
>             % the minimum value of (Exrp1 + Expr2) is the minimum value of
>             % Expr1 plus the minimum value of Expr2
>             Max is min_val(Expr1) + min_val(Expr2).
>     min_val(Expr1 - Expr2, Max) :-
>             % the minimum value of (Exrp1 - Expr2) is the minimum value of
>             % Expr1 minus the maximum value of Expr2
>             Max is min_val(Expr1) - max_val(Expr2).

The linear constraints, which will enforce the overlap condition when the variable `Bool` is set to 1, are labelled lin1 and lin2. If the variable `Bool` is instantiated to 0, then the variables (or values) `Start`, `Time` and `Duration` are free to take any value in their respective domains.
Notice that `pos_overlap` is logically weaker than `overlap` because

- it does not enforce the integrality of the boolean variable, (i.e. `pos_overlap` is a linear relaxation of the disjunctive constraint), and
- it does not enforce the negation of overlap in case the boolean is set to 0.

The tighter cooperation is achieved simply by adding the `pos_overlap` constraint to the original encoding:

>     eplex_constraints_2(Time,S1,S2,S3,B1,B2) :-
>             % task 1 with duration 3 and task 2 with duration 5 are both
>             % completed before the start time of task 3
>             before(S1,3,S3),
>             before(S2,5,S3),
>             % task 1 with duration 3 overlaps time point Time if B1 = 1
>             pos_overlap(S1,3,Time,B1),
>             % task 2 with duration 5 overlaps time point Time if B2 = 1
>             pos_overlap(S2,5,Time,B2).

>     hybrid3(Time, [S1,S2,S3], End) :-
>             % give the eplex cost variable some default bounds
>             ic:(End $:: -1.0Inf..1.0Inf),
>             % we must give the start time of task 3 ic bounds in order to
>             % suspend on changes to them
>             ic: (S3::1..20),
>             % setup the problem constraints
>             ic_constraints(Time,S1,S2,B1,B2),
>             eplex_constraints(Time,S1,S2,S3,B1,B2),
>             % perform the optimisation
>             both_opt(labeling([B1,B2,S1,S2]),min(S3),End).

Although it may at first glance seem better to enforce the integerality of all variables in the linear solver as well, this is in fact counter-productive for variables that will be explicitly labelled during search in hybrid algorithms. The external solver would then perform its own branch-and-bound search in addition to the branch-and-bound search being performed within the ECLiPSe program.

### 17.4.3 Handling Disjunctions

The same technique, of introducing boolean variables and sufficiently large multipliers, can be used to translate any disjunction of linear constraints into linear constraints (and integrality constraints on the booleans) which can be handled by `eplex`.
Consider the negation of the overlap constraints above: if a task does not overlap a time point then it is either completed before the time point or starts after the timepoint. This disjunction can be expressed in `eplex` using two further boolean variables:

>     neg_overlap(Start,Duration,Time,Bool1,Bool2) :-
>             % if Bool1 is 1 then the task with start time Start and duration
>             % Duration starts after time point Time
>             Max1 is maxdiff(Time,Start-1),
>             eplex:(Time $=< Start-1+(1-Bool1)*Max1),
>             % if Bool2 is 1 then the task with start time Start and duration
>             % Duration is completed before time point Time
>             Max2 is maxdiff(Start+Duration,Time),
>             eplex:(Time+(1-Bool2)*Max2 $>= Start+Duration).

>     eplex_constraints_3(T,S1,S2,S3,B1,N1B1,N2B1,B2,N1B2,N2B2) :-
>             % task 1 with duration 3 and task 2 with duration 5 are both
>             % completed before the start time of task 3
>             before(S1,3,S3),
>             before(S2,5,S3),
>             % task 1 with duration 3 either overlaps time point Time,
>             % starts after it or is completed before it
>             pos_overlap(S1,3,T,B1),
>             neg_overlap(S1,3,T,N1B1,N2B1),
>             eplex:(N1B1+N2B1 $= 1-B1),
>             % task 2 with duration 5 either overlaps time point Time,
>             % starts after it or is completed before it
>             pos_overlap(S2,5,T,B2),
>             neg_overlap(S2,5,T,N1B2,N2B2),
>             eplex:(N1B2+N2B2 $= 1-B2),
>             % exactly one of task 1 with duration 3 and task 2 with
>             % duration 5 overlaps time point Time
>             eplex:(B1+B2 $= 1).

>     hybrid4(Time, [S1,S2,S3], End) :-
>             % give the eplex cost variable some default bounds
>             ic:(End $:: -1.0Inf..1.0Inf),
>             % we must give the start time of task 3 and the non-overlap
>             % booleans ic bounds in order to suspend on changes to them
>             ic:(S3::1..20),
>             ic:([N1B1,N2B1,N1B2,N2B2]::0..1),
>             % setup the problem constraints
>             ic_constraints(Time,S1,S2,B1,B2),
>             eplex_constraints_3(Time,S1,S2,S3,B1,N1B1,N2B1,B2,N1B2,N2B2),
>             % perform the optimisation
>             both_opt(labeling([B1,N1B1,N2B1,B2,N1B2,N2B2,S1,S2]),min(S3),End).

Now the negation of the overlap will be enforced whenever either of the non-overlap booleans is set to 1. Note that it is not strictly necessary to label the non-overlap booleans: whenever the start time of a task is labelled in such a way that the task falls to one side of the time point, the other non-overlap boolean will be forced to 0 by its linear non-overlap constraint. The constraint requiring a task to either overlap or fall to one side of the time point will then force the remaining non-overlap boolean to be 1.
In fact in this simple example we gain nothing by including the `neg_overlap` constraints on the “direction” of non-overlap. As soon as a labeling decision has been made as to whether one task overlaps the time point, the earliest possible start time of both tasks is updated in the linear solver. Since the problem cost is minimized by starting all tasks as early as possible, the relaxed `eplex` solution will conicide with the integer solution.
As another simple example consider a naive program to choose values for the elements of a finite list (of length `Length`) such that each pair of values differs by at least 2. The diff2 constraint on each pair `X` and `Y` of elements can be expressed as a disjunction in `ic`:

>     diff2ic(X,Y) :-
>             % X and Y must differ by at least 2
>             ic: ((X+2 $=< Y) or (Y+2 $=< X)).

>     list_diff2ic(List) :-
>             % each pair must differ by at least 2
>             (
>                 fromto(List, [X|Rest], Rest, [])
>             do
>                 (
>                     foreach(Y, Rest),
>                     param(X)
>                 do
>                     diff2ic(X,Y)
>                 )
>             ).

Alternatively it can be expressed in `eplex` using a boolean variable:

>     diff2eplex(X,Y,Length,B) :-
>             % if B is 1 then Y is at least 2 greater than X
>             eplex: (X+2+B*Length $=< Y+Length),
>             % if B is 0 then X is at least 2 greater than Y
>             eplex: (X+Length $>= Y+2+(1-B)*Length).

>     list_diff2eplex(List, Length, Bools) :-
>             % each pair must differ by at least 2
>             (
>                 fromto(List, [X|Rest], Rest, []),
>                 fromto(Bools, Out, In, []),
>                 param(Length)
>             do
>                 (
>                     foreach(Y, Rest),
>                     fromto(Out, [B|Bs], Bs, In),
>                     param(X, Length)
>                 do
>                     diff2eplex(X,Y,Length,B)
>                 )
>             ).

Suppose each element E of the list must take a value between 1 and 2*(Length−1), then any attempted labelling of the elements must fail. Sending the constraints to `ic` and labelling the elements of the list is inefficient.

>     ic_list(List) :-
>             length(List, Length),
>             Max is 2*(Length-1),
>             % each element must take a value between 1 and 2*(Length-1)
>             ic: (List::1..Max),
>             list_diff2ic(List),
>             labeling(List).

Sending the constraints to `eplex` and enforcing integrality of the booleans is more efficient.

>     eplex_list(List) :-
>             length(List, Length),
>             Max is 2*(Length-1),
>             % each element must take a value between 1 and 2*(Length-1)
>             eplex: (List::1..Max),
>             list_diff2eplex(List, Length, Bools),
>             % enforce Bools to be 0..1 and integer
>             eplex: integers(Bools),
>             eplex: (Bools::0..1),
>             % setup the eplex solver with a dummy objective function
>             eplex:eplex_solver_setup(min(0),Cost,[],[]),
>             % solve by linear solver
>             eplex:eplex_solve(Cost).

Better still is to post the constraints to both `ic` and `eplex`, and label the booleans.

>     hybrid_list(List) :-
>             % give the eplex cost variable some default bounds
>             ic:(Cost $:: -1.0Inf..1.0Inf),
>             length(List, Length),
>             Max is 2*(Length-1),
>             % each element must take a value between 1 and 2*(Length-1)
>             ic: (List::1..Max),
>             list_diff2ic(List),
>             list_diff2eplex(List, Length, Bools),
>             % enforce Bools to be 0..1 (but not integer in eplex)
>             ic: (Bools::0..1),
>             % setup the eplex solver with a dummy objective function
>             eplex:eplex_solver_setup(min(0),Cost,[sync_bounds(yes)],[ic:min,ic:max]),
>             % minimize Cost by branch-and-bound
>             minimize((labeling(Bools),eplex_get(cost,Cost)),Cost).

### 17.4.4 A More Realistic Example

For more complex applications, sending all “linearisable” constraints to both `ic` and `eplex` is rarely the best method. Sending too many constraints to `ic` can result in many wakings but little useful propagation. Sending too many constraints to `eplex` can cause a big growth in the size of the constraint store, which slows down constraint solving with little improvement in the relaxed optimum. If the extra variables are constrained to be integer, then the (MIP) solver may enter a deep search tree with disastrous consequences for efficiency. In this example we briefly illustrate the point, though there is no space to include the whole program, and complete supporting results.
Consider the problem of generating test networks for IP (internet protocol). To generate such networks, it is necessary to assign capacities to each line. We assume a routing algorithm that sends each message along a “cheapest” path, where the cost is dependent on the bandwidth. Messages from a particular start to end node are divided equally amongst all cheapest paths.
Path Flows
Given a total quantity `Qty` of messages, between a particular start and end node, it is necessary to compute the quantity of messages `QtyP` along each path P between the two nodes. The variable `CostP` represents the cost of this path, and the variable `MinCost` represents the cost of the cheapest path. The variable `Count` represents the number of cheapest paths (between which the messages were equally divided). A boolean variable BP records whether the current path is a cheapest path, and therefore whether `QtyP` is non-zero. The encoding in `ic` is as follows:

>     ic: '$>='(MinCost + 1, CostP,BP),    % con3
>     ic: (QtyP*Count $= BP*Qty)         % con4

Note that it is not possible to test for equality between `MinCost` and `CostP` because they are not integers but real number variables.
These constraints are very precise but propagate little until the variables `MinCost` and `CostP` have tight bounds.
The challenge is to find a combination of `ic` and `eplex` constraint handling that efficiently extract the maximum information from the constraints. Linearising `con3` so it can be handled by `eplex` does not help prune the search tree. Worse, it may significantly increase the size of the linear constraint store and the number of integer (boolean) variables, which impacts solver performance.
Once all the boolean variables are instantiated, the sum of `QtyP` for all the paths equals the total quantity `Qty` (because precisely Count paths have a non-zero PQty = Qty / Count). We therefore introduce a variable `Qties` constrained to be the sum of all the path quantities. If `QtyList` is a list of the path quantities, we can express the constraint thus `Qties $= sum(QtyList)`. We can now add a redundant constraint `Qty $= Qties`. The above constraints are both linear and can be handled by `eplex`.
In practice this encoding dramatically enhances the efficiency of the test network generation. Experimentation with this program revealed that posting the redundant constraints to `eplex` yields a much more significant improvement than just posting them to `ic`.

> It is easy to send a constraint to more than one solver. Even disjunctive constraints can be encoded in a form that enables them to be sent to both solvers. However for large applications it is best to send constraints only to those solvers that can extract useful information from them. This requires experimentation.

> Figure 17.3: Sending Constraints to Multiple Solvers