next up previous index
Next: Current Known Restrictions and Up: The Finite Domains Library Previous: Example of Defining a   Index

Subsections

Program Examples

In this section we present some FD programs that show various aspects of the library usage. More examples can be found on the ECLiPSe web site http://www.icparc.ic.ac.uk/eclipse/.

Constraining Variable Pairs

The finite domain library gives the user the possibility to impose constraints on the value of a variable. How, in general, is it possible to impose constraints on two or more variables? For example, let us assume that we have a set of colours and we want to define that some colours fit with each other and others do not. This should work in such a way as to propagate possible changes in the domains as soon as this becomes possible.

Let us assume we have a symmetric relation that defines which colours fit with each other:

% The basic relation
fit(yellow, blue).
fit(yellow, red).
fit(blue, yellow).
fit(red, yellow).
fit(green, orange).
fit(orange, green).

The predicate nice_pair(X, Y) is a constraint and any change of the possible values of X or Y is propagated to the other variable. There are many ways in which this pairing can be defined in ECLiPSe. They are different solutions with different properties, but they yield the same results.

User-Defined Constraints

We use more or less directly the low-level primitives to handle finite domain variables. We collect all consistent values for the two variables, remove all other values from their domains and then suspend the predicate until one of its arguments is updated:
nice_pair(A, B) :-
 % get the domains of both variables
dvar_domain(A, DA),         
    dvar_domain(B, DB),         
 % make a list of respective matching colours
setof(Y, X^(dom_member(X, DA), fit(X, Y)), BL),
    setof(X, Y^(dom_member(Y, DB), fit(X, Y)), AL),
 % convert the lists to domains
sorted_list_to_dom(AL, DA1),
    sorted_list_to_dom(BL, DB1),
 % intersect the lists with the original domains
dom_intersection(DA, DA1, DA_New, _),
    dom_intersection(DB, DB1, DB_New, _),
 % and impose the result on the variables
dvar_update(A, DA_New),
    dvar_update(B, DB_New),
 % unless one variable is already instantiated, suspend
% and wake as soon as any element of the domain is removed
    (var(A), var(B) ->
        suspend(nice_pair(A, B), 2, [A,B]->any)
    ;
        true
    ).

% Declare the domains
colour(A) :-
    findall(X, fit(X, _), L),
    A :: L.

After defining the domains, we can state the constraints:

[eclipse 5]: colour([A,B,C]), nice_pair(A, B), nice_pair(B, C), A #\= green.

B = B{[blue, green, red, yellow]}
C = C{[blue, orange, red, yellow]}
A = A{[blue, orange, red, yellow]}
 
Delayed goals:
  nice_pair(A{[blue, orange, red, yellow]}, B{[blue, green, red, yellow]})
  nice_pair(B{[blue, green, red, yellow]}, C{[blue, orange, red, yellow]})

This way of defining new constraints is often the most efficient one, but usually also the most tedious one.

Using the element Constraint

In this case we use the available primitive in the fd library. Whenever it is necessary to associate a fd variable with some other fd variable, the element/3 constraint is a likely candidate. Sometimes it is rather awkward to use, because additional variables must be used, but it gives enough power:

nice_pair(A, B) :-
    element(I, [yellow, yellow, blue, red, green, orange], A),
    element(I, [blue, red, yellow, yellow, orange, green], B).

We define a new variable I which is a sort of index into the clauses of the fit predicate. The first colour list contains colours in the first argument of fit/2 and the second list contains colours from the second argument. The propagation is similar to that of the previous one.

When element/3 can be used, it is usually faster than the previous approach, because element/3 is partly implemented in C.

Using Evaluation Constraints

We can also encode directly the relations between elements in the domains of the two variables:

nice_pair(A, B) :-
    np(A, B),
    np(B, A).

np(A, B) :-
    [A,B] :: [yellow, blue, red, orange, green],
    A #= yellow #=> B :: [blue, red],
    A #= blue #=> B #= yellow,
    A #= red #=> B #= yellow,
    A #= green #=> B #= orange,
    A #= orange #=> B #= green.

This method is quite simple and does not need any special analysis; on the other hand it potentially creates a huge number of auxiliary constraints and variables.

Using Generalised Propagation

Propia is the first candidate to convert an existing relation into a constraint. One can simply use infers most to achieve the propagation:

nice_pair(A, B) :-
    fit(A, B) infers most.

Using Propia is usually very easy and the programs are short and readable, so that this style of constraints writing is quite useful e.g. for teaching. It is not as efficient as with user-defined constraints, but if the amount of propagation is more important that the efficiency of the constraint itself, it can yield good results, too.

Using Constraint Handling Rules

The domain solver in CHR can be used directly with the element/3 constraint as well, however it is also possible to define directly domains consisting of pairs:
:- lib(chr).
:- chr(lib(domain)).

nice_pair(A, B) :-
    setof(X-Y, fit(X, Y), L),
    A-B :: L.

The pairs are then constrained accordingly:

[eclipse 2]: nice_pair(A, B), nice_pair(B, C), A ne orange.

B = B
C = C
A = A

Constraints:
(9) A_g1484 - B_g1516 :: [blue - yellow, green - orange, red - yellow,
yellow - blue, yellow - red]
(10) A_g1484 :: [blue, green, red, yellow]
(12) B_g1516 - C_g3730 :: [blue - yellow, orange - green, red - yellow,
yellow - blue, yellow - red]
(13) B_g1516 :: [blue, orange, red, yellow]
(14) C_g3730 :: [blue, green, red, yellow]

Puzzles

Various kinds of puzzles can be easily solved using finite domains. We show here the classical Lewis Carrol's puzzle with five houses and a zebra:
Five men with different nationalities live in the first five houses
of a street.  They practise five distinct professions, and each of
them has a favourite animal and a favourite drink, all of them
different.  The five houses are painted in different colours.

The Englishman lives in a red house.
The Spaniard owns a dog.
The Japanese is a painter.
The Italian drinks tea.
The Norwegian lives in the first house on the left.
The owner of the green house drinks coffee.
The green house is on the right of the white one.
The sculptor breeds snails.
The diplomat lives in the yellow house.
Milk is drunk in the middle house.
The Norwegian's house is next to the blue one.
The violinist drinks fruit juice.
The fox is in a house next to that of the doctor.
The horse is in a house next to that of the diplomat.

Who owns a Zebra, and who drinks water?

One may be tempted to define five variables Nationality, Profession, Colour, etc. with atomic domains to represent the problem. Then, however, it is quite difficult to express equalities over these different domains. A much simpler solution is to define 5x5 integer variables for each mentioned item, to number the houses from one to five and to represent the fact that e.g. Italian drinks tea by equating Italian = Tea. The value of both variables represents then the number of their house. In this way, no special constraints are needed and the problem is very easily described:

:- lib(fd).

zebra([zebra(Zebra), water(Water)]) :-
    Sol = [Nat, Color, Profession, Pet, Drink],
    Nat = [English, Spaniard, Japanese, Italian, Norwegian],
    Color = [Red, Green, White, Yellow, Blue],
    Profession = [Painter, Sculptor, Diplomat, Violinist, Doctor],
    Pet = [Dog, Snails, Fox, Horse, Zebra],
    Drink = [Tea, Coffee, Milk, Juice, Water],

 % we specify the domains and the fact
% that the values are exclusive
    Nat :: 1..5,
    Color :: 1..5,
    Profession :: 1..5,
    Pet :: 1..5,
    Drink :: 1..5,
    alldifferent(Nat),
    alldifferent(Color),
    alldifferent(Profession),
    alldifferent(Pet),
    alldifferent(Drink),

 % and here follow the actual constraints
English = Red,
    Spaniard = Dog,
    Japanese = Painter,
    Italian = Tea,
    Norwegian = 1,
    Green = Coffee,
    Green #= White + 1,
    Sculptor = Snails,
    Diplomat = Yellow,
    Milk = 3,
    Dist1 #= Norwegian - Blue, Dist1 :: [-1, 1],
    Violinist = Juice,
    Dist2 #= Fox - Doctor, Dist2 :: [-1, 1],
    Dist3 #= Horse - Diplomat, Dist3 :: [-1, 1],

    flatten(Sol, List),
    labeling(List).

Bin Packing

In this type of problems the goal is to pack a certain amount of different things into the minimal number of bins under specific constraints. Let us solve an example given by Andre Vellino in the Usenet group comp.lang.prolog, June 93:

To solve this problem, it is not enough to state constraints on some variables and to start a labeling procedure on them. The variables are namely not known, because we don't know how many bins we should take. One possibility would be to take a large enough number of bins and to try to find a minimum number. However, usually it is better to generate constraints for an increasing fixed number of bins until a solution is found.

The predicate solve/1 returns the solution for this particular problem, solve_bin/2 is the general predicate that takes an amount of components packed into a cont/5 structure and it returns the solution.

solve(Bins) :-
    solve_bin(cont(1, 2, 1, 3, 2), Bins).

solve_bin/2 computes the sum of all components which is necessary as a limit value for various domains, calls bins/4 to generate a list Bins with an increasing number of elements and finally it labels all variables in the list:

solve_bin(Demand, Bins) :-
    Demand = cont(G, P, S, W, C),
    Sum is G + P + S + W + C,
    bins(Demand, Sum, [Sum, Sum, Sum, Sum, Sum, Sum], Bins),
    label(Bins).

The predicate to generate a list of bins with appropriate constraints works as follows: first it tries to match the amount of remaining components with zero and the list with nil. If this fails, a new bin represented by a list

[Colour, Glass, Plastic, Steel, Wood, Copper]

is added to the bin list, appropriate constraints are imposed on all the new bin's variables, its contents is subtracted from the remaining number of components, and the predicate calls itself recursively:

bins(cont(0, 0, 0, 0, 0), 0, _, []).
bins(cont(G0, P0, S0, W0, C0), Sum0, LastBin, [Bin|Bins]) :-
    Bin = [_Col, G, P, S, W, C],
    bin(Bin, Sum),
    G2 #= G0 - G,
    P2 #= P0 - P,
    S2 #= S0 - S,
    W2 #= W0 - W,
    C2 #= C0 - C,
    Sum2 #= Sum0 - Sum,
    ordering(Bin, LastBin),
    bins(cont(G2, P2, S2, W2, C2), Sum2, Bin, Bins).
The ordering/2 constraints are strictly necessary because this problem has a huge number of symmetric solutions.

The constraints imposed on a single bin correspond exactly to the problem statement:

bin([Col, G, P, S, W, C], Sum) :-
    Col :: [red, blue, green],
    [Capacity, G, P, S, W, C] :: 0..4,
    G + P + S + W + C #= Sum,
    Sum #> 0, % no empty bins
Sum #<= Capacity,
    capacity(Col, Capacity),
    contents(Col, G, P, S, W, C),
    requires(W, P),
    exclusive(G, C),
    exclusive(C, P),
    at_most(1, red, Col, W),
    at_most(2, green, Col, W).

We will code all of the special constraints with the maximum amount of propagation to show how this can be achieved. In most programs, however, it is not necessary to propagate all values everywhere which simplifies the code quite considerably. Often it is also possible to use some of the built-in symbolic constraints of ECLiPSe, e.g. element/3 or atmost/3.

Capacity Constraints

capacity(Color, Capacity) should instantiate the capacity if the colour is known, and reduce the colour values if the capacity is known to be greater than some values. If we use evaluation constraints, we can code the constraint directly, using equivalences:
capacity(Color, Capacity) :-
    Color #= blue #<=> Capacity #= 1,
    Color #= green #<=> Capacity #= 4,
    Color #= red #<=> Capacity #= 3.

A more efficient code would take into account the ordering on the capacities. Concretely, if the capacity is greater than 1, the colour cannot be blue and if it is greater than 3, it must be green:

capacity(Color, Capacity) :-
    var(Color),
    !,
    dvar_domain(Capacity, DC),
    dom_range(DC, MinC, _),
    (MinC > 1 ->
        Color #\= blue,
        (MinC > 3 ->
            Color = green
        ;
            suspend(capacity(Color, Capacity), 3, (Color, Capacity)->inst)
        )
    ;
        suspend(capacity(Color, Capacity), 3, [Color->inst, Capacity->min])
    ).
capacity(blue, 1).
capacity(green, 4).
capacity(red, 3).
Note that when suspended, the predicate waits for colour instantiation or for minimum of the capacity to be updated (except that 3 is one less than the maximum capacity and thus waiting for its instantiation is equivalent).

Containment Constraints

The containment constraints are stated as logical expressions and this is also the easiest way to medel them. The important point to remember is that a condition like red can contain glass, wood, copper actually means red cannot contain plastic or steel which can be written as

contents(Col, G, P, S, W, _) :-
    Col #= red #=> P #= 0 #/\ S #= 0,
    Col #= blue #=> P #= 0 #/\ W #= 0,
    Col #= green #=> G #= 0 #/\ S #= 0.

If we want to model the containment with low-level domain predicates, it is easier to state them in the equivalent conjugate form:

or in a further equivalent form that uses at most one bin colour:

contents(Col, G, P, S, W, _) :-
    not_contained_in(Col, G, green),
    contained_in(Col, P, green),
    contained_in(Col, S, blue),
    not_contained_in(Col, W, blue).

contained_in(Color, Component, In) states that if Color is different from In, there can be no such component in it, i.e. Component is zero:

contained_in(Col, Comp, In) :-
    nonvar(Col),
    !,
    (Col \== In ->
        Comp = 0
    ;
        true
    ).
contained_in(Col, Comp, In) :-
    dvar_domain(Comp, DM),
    dom_range(DM, MinD, _),
    (MinD > 0 ->
        Col = In
    ;
        suspend(contained_in(Col, Comp, In), 2, [Comp->min, Col->inst])
    ).

not_contained_in(Color, Component, In) states that if the bin is of the given colour, the component cannot be contained in it:

not_contained_in(Col, Comp, In) :-
    nonvar(Col),
    !,
    (Col == In ->
        Comp = 0
    ;
        true
    ).
not_contained_in(Col, Comp, In) :-
    dvar_domain(Comp, DM),
    dom_range(DM, MinD, _),
    (MinD > 0 ->
        Col #\= In
    ;
        suspend(not_contained_in(Col, Comp, In), 2, [Comp->min, Col->any])
    ).

As you can see again, modeling with the low-level domain predicates might give a faster and more precise programs, but it is much more difficult than using constraint expressions and evaluation constraints. A good approach is thus to start with constraint expressions and only if they are not efficient enough, to (stepwise) recode some or all constraints with the low-level predicates.

Requirement Constraints

The constraint `A requires B' is written as

requires(A, B) :-
    A #> 0 #=> B #> 0.

With low-level predicates, the constraint `A requires B' is woken as soon as some A is present or B is known:

requires(A, B) :-
    nonvar(B),
    !,
    ( B = 0 ->
        A = 0
    ;
        true
    ).
requires(A, B) :-
    dvar_domain(A, DA),
    dom_range(DA, MinA, _),
    ( MinA > 0 ->
        B #> 0
    ;
        suspend(requires(A, B), 2, [A->min, B->inst])
    ).

Exclusive Constraints

The exclusive constraint can be written as
exclusive(A, B) :-
    A #> 0 #=> B #= 0,
    B #> 0 #=> A #= 0.
however a simple form with one disjunction is enough:
exclusive(A, B) :-
    A #= 0 #\/ B #= 0.

With low-level domain predicates, the exclusive constraint defines a suspension which is woken as soon as one of the two components is present:

exclusive(A, B) :-
    dvar_domain(A, DA),
    dom_range(DA, MinA, MaxA),
    ( MinA > 0 ->
        B = 0
    ; MaxA = 0 ->
 % A == 0
true
    ;
        dvar_domain(B, DB),
        dom_range(DB, MinB, MaxB),
        ( MinB > 0 ->
            A = 0
        ; MaxB = 0 ->
 % B == 0
true
        ;
            suspend(exclusive(A, B), 3, (A,B)->min)
        )
    ).

Atmost Constraints

at_most(N, In, Colour, Components) states that if Colour is equal to In, then there can be at most N Components and vice versa, if there are more than N Components, the colour cannot be In. With constraint expressions, this can be simply coded as
at_most(N, In, Col, Comp) :-
    Col #= In #=> Comp #<= N.

A low-level solution looks as follows:

at_most(N, In, Col, Comp) :-
    nonvar(Col),
    !,
    (In = Col ->
        Comp #<= N
    ;
        true
    ).
at_most(N, In, Col, Comp) :-
    dvar_domain(Comp, DM),
    dom_range(DM, MinM, _),
    (MinM > N ->
        Col #\= In
    ;
        suspend(at_most(N, In, Col, Comp), 2, [In->inst, Comp->min])
    ).

Ordering Constraints

To filter out symmetric solutions we can e.g. impose a lexicographic ordering on the bins in the list, i.e. the second bin must be lexicographically greater or equal than the first one etc. As long as the corresponding most significant variables in two consecutive bins are not instantiated, we cannot constrain the following ones and thus we suspend the ordering on the inst lists:

ordering([], []).
ordering([Val1|Bin1], [Val2|Bin2]) :-
    Val1 #<= Val2,
    (integer(Val1) ->
        (integer(Val2) ->
            (Val1 = Val2 ->
                ordering(Bin1, Bin2)
            ;
                true
            )
        ;
            suspend(ordering([Val1|Bin1], [Val2|Bin2]), 2, Val2->inst)
        )
    ;
        suspend(ordering([Val1|Bin1], [Val2|Bin2]), 2, Val1->inst)
    ).

There is a problem with the representation of the colour: If the colour is represented by an atom, we cannot apply the #<=/2 predicate on it. To keep the ordering predicate simple and still have a symbolic representation of the colour in the program, we can define input macros that transform the colour atoms into integers:

:- define_macro(no_macro_expansion(blue)/0, tr_col/2, []).
:- define_macro(no_macro_expansion(green)/0, tr_col/2, []).
:- define_macro(no_macro_expansion(red)/0, tr_col/2, []).

tr_col(no_macro_expansion(red), 1).
tr_col(no_macro_expansion(green), 2).
tr_col(no_macro_expansion(blue), 3).

Labeling

A straightforward labeling would be to flatten the list with the bins and use e.g. deleteff/3 to label a variable out of it. However, for this example not all variables have the same importance -- the colour variables propagate much more data when instantiated. Therefore, we first filter out the colours and label them before all the component variables:
label(Bins) :-
    colours(Bins, Colors, Things),
    flatten(Things, List),
    labeleff(Colors),
    labeleff(List).

colours([], [], []).
colours([[Col|Rest]|Bins], [Col|Cols], [Rest|Things]) :-
    colours(Bins, Cols, Things).

labeleff([]).
labeleff(L) :-
    deleteff(V, L, Rest),
    indomain(V),
    labeleff(Rest).

Note also that we need a special version of flatten/3 that works with nonground lists.


next up previous index
Next: Current Known Restrictions and Up: The Finite Domains Library Previous: Example of Defining a   Index
Warwick Harvey
2004-08-07