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:
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.
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
pos_overlap
is a linear relaxation of the disjunctive
constraint), and
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.
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). |
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.