Previous Up Next

7.2  Problem Modelling and Solving

GFD provides facilities to model and solve problems over the finite integer domain, with Gecode as the solver. It supports the constraints provided by Gecode – and Gecode supports a large set of constraints. The search to solve the problem can be done at the ECLiPSe level (with support from Gecode for variable and value selections), or the whole search can be performed by Gecode itself using one of its search engines.

Implementation-level differences (like Gecode’s re-computation based model vs. ECLiPSe’s backtracking model) are largely invisible to the user, as GFD automatically maintains the Gecode computational state to match ECLiPSe’s.

7.2.1  Usage

To load the GFD library into your program, simply add the following directive at an appropriate point in your code.

:- lib(gfd).

7.2.2  Integer domain variables

An (integer) domain variable is a variable which can be instantiated only to a value from a given finite set of integer values.

A variable becomes a domain variable when it first appears in a (GFD) constraint. If the constraint is a domain constraint, then the variable will be given the domain specified by the constraint. Otherwise, the variable will be given a default domain, which should be large enough for most problem instances.

The default domain is an interval, and the maximum and minimum values of this interval can be changed using gfd_set_default/2 (with the options interval_max and interval_min for the maximum and minimum values, respectively). You can also obtain the current values of interval_max and interval_min using gfd_get_default/2.

The Gecode documentation suggests that domain variables should be given as small a domain as possible, and requires the user to explicitly specify a domain for all domain variables. While this is not required by GFD, following Gecode’s convention is still a good idea, since overly large domains can negatively affect performance. It is therefore recommended to make use of domain constraints, and specify the domain for variables before using them in other constraints.

A domain variable is mapped into a Gecode IntVar.

7.2.3  Constraints

GFD supports the (integer finite domain) constraints implemented by Gecode. Some of these constraints correspond to those in ECLiPSe’s native finite domain solvers (IC and FD), but many do not. For those that are implemented in IC and/or FD, the same name and syntax is used by GFD (although details may differ, such as the types allowed for arguments). For many constraints, Gecode supports a choice of consistency levels. In GFD, these are provided as alternative implementations of these constraints in one of three modules:

gfd_gac
Domain consistent (Generalised Arc-Consistent), maps to ICL_DOM in Gecode.
gfd_bc
Bound consistent, maps to ICL_BND in Gecode.
gfd_vc
Value consistent, maps to ICL_VAL in Gecode.

Constraints can either be imported from one of these modules, or explicitly qualified with the module name, e.g.

gfd_gac:alldifferent(Xs)

Posting a constraint at a particular consistency level is supported only if that consistency level is implemented for that constraint in Gecode – see the individual documentation for the constraints for details. Note that the three consistency modules are implicitly created when library(gfd) is loaded, and do not need to be loaded explicitly. Posting a constraint unqualified (or qualified with gfd) means posting the constraint at the default consistency level (ICL_DEF).

Even constraints that involve expressions may be posted at specific consistency levels. However, it is possible that some of the sub-constraints and sub-expressions inside the expressions are not supported at the given consistency level. In such cases, these sub-expressions will be posted at the default consistency level.

For example, the N-Queens example can post domain consistent versions of the #\=/2 constraints:

:- lib(gfd).

queens_list(N, Board) :-
    length(Board, N),
    Board :: 1..N,
    (fromto(Board, [Q1|Cols], Cols, []) do
        ( foreach(Q2, Cols), param(Q1), count(Dist,1,_) do
            gfd_gac: (Q2 #\= Q1),
            gfd_gac: (Q2 - Q1 #\= Dist),
            gfd_gac: (Q1 - Q2 #\= Dist)
        )
    ),
    label(Board).

In this particular example, using the stronger propagation actually results in a reduction in performance, as there is no reduction in search space from the stronger propagation, but an increase in the cost of doing the propagation,

Gecode requires an explicit command to perform propagation. In GFD, this command is implemented as a delayed goal at priority 9. When a constraint is posted, GFD first adds any new domain variables in the constraint to Gecode, and then adds the constraint to Gecode, without performing any explicit propagation. The call to propagate is then scheduled for execution. It is thus possible to post multiple constraints without propagation by posting the constraints at a more urgent (i.e. numerically smaller) priority than 9 (see call_priority/2). This could reduce the cost of performing the propagation.

Several constraints involve the use of indices. In ECLiPSe, indices starts from 1, while Gecode, like C++ (the programming language it is implemented in), indices starts from 0. For compatibility with ECLiPSe, “normal” GFD constraints also have indices that starts from 1. These constraints are mapped to the Gecode native indices in various ways, depending of the constraint, with the aim of minimising the overhead. GFD also supports versions of these constraints that uses Gecode’s native indices, i.e. starting from 0, and these have an additional _g in their name (e.g. bin_packing_g/3 is the Gecode native index version of bin_packing/3). These versions of the constraint do not have the overhead of converting the index value, but may be incompatible with the rest of ECLiPSe.

Domain constraints

The following domain constraints are supported by GFD:

?Vars #:: ++Domain
Constrains Vars to have the domain Domain. A reified version is also available. ::/2,3 are also supported as aliases.

Arithmetic and logical expressions

GFD supports expressions as arguments for relational and logical connective constraints. Expressions can either evaluate to an integer (integer expression) or a truth value (constraint expression).

Relational Constraints
These specify an arithmetic relationship between two integer expressions. Constraint expressions are allowed as arguments of relational constraints, with the truth value of the expression treated as the integer value 1 (true) or 0 (false).

All relational constraints have their reified counterparts, which has an extra boolean argument that specify if the constraint is entailed or not.

The relational constraints are: #</2,3 (less than), #=/2,3 (equal), #=</2,3 (less than or equal to), #>/2,3 (greater than), #>=/2,3 (greater than or equal to), #\=/2,3 (not equal to).

Logical Connective constraints
Specifies a logical connection between constraint expression(s). All logical connectives have their reified counterparts. The available connectives are:

<=>/2,3 (equivalent), =>/2,3 (implies), and/2,3 (and), or/2,3 (or), xor/2,3 (exclusive or), neg/1,2 (negation).

Constraints which can be reified can occur as an argument of a logical connective, i.e. as a constraint expression, evaluating to the reified truth value.

As relational constraints can be reified, and truth values of constraint expressions can be evaluated as integer values, #=/2 can be used instead of <=>/2 and #\=/2 can be used instead of xor. This is also provided for compatibility with IC.

The syntax for the expressions closely follows that in IC. The following can be used inside expressions:

X
Variables. If X is not yet a domain variable, it is turned into one.
123
Integer constants.
+Expr
Identity.
-Expr
Sign change.
abs(Expr)
The absolute value of Expr.
E1+E2
Addition.
E1-E2
Subtraction.
E1*E2
Multiplication.
E1//E2
Integer division, truncating towards zero.
E1/E2
Integer division, defined only if E2 evenly divides E1.
E1 rem E2
Integer remainder, same sign as E1.
Expr^2
Square. Equivalent to sqr(Expr) (alias for compatibility).
min(E1,E2)
Minimum.
max(E1,E2)
Maximum.
sqr(Expr)
Square. Logically equivalent to Expr*Expr.
isqrt(Expr)
Square root (always positive). Truncated towards zero.
sqrt(Expr)
Square root, defined only if Expr is the square of an integer.
sum(ExprCol)
Sum of a collection of expressions.
sum(IntCol*ExprCol)
Scalar product of a collection of integers and expressions. IntCol and ExprCol must be the same size.
min(ExprCol)
Minimum of a collection of expressions.
max(ExprCol)
Maximum of a collection of expressions.
and
Reified constraint conjunction. e.g. X #> 3 and Y #< 8. These are restricted to the top-level of an expression, and for reifiable expressions only,
or
Reified constraint disjunction. e.g. X #> 3 or Y #< 8. These are restricted to the top-level of an expression, and for reifiable expressions only,
xor
Reified constraint exclusive disjunction. e.g. X #> 3 xor Y #< 8. These are restricted to the top-level of an expression, and for reifiable expressions only,
=>
Reified constraint implication. e.g. X #> 3 => Y #< 8. These are restricted to the top-level of an expression, and for reifiable expressions only,
neg
Reified constraint negation. e.g. neg X #> 3 These are restricted to the top-level of an expression, and for reifiable expressions only,
<=>
Reified constraint equivalence. e.g. X #> 3 <=> Y #< 8. This is similar to #= used in an expression context. These are restricted to the top-level of an expression, and for reifiable expressions only,
#>, #>=, #=, #=<, #<, #\=

Posted as a constraint, both the left- and right- hand arguments are expressions.

Within the expression context, the constraint evaluates to its reified truth value. If the constraint is entailed by the state of the constraint store then the (sub-)expression evaluates to 1. If it is dis-entailed by the state of the constraint store then it evaluates to 0. If its reified status is unknown then it evaluates to an integral variable 0..1.

Note: The simple cases (e.g. Bool #= (X #> 5)) are equivalent to directly calling the reified forms of the basic constraints (e.g. #>(X, 5, Bool)).

eval(Expr)
Logically equivalent to Expr. Should be used when Expr is a compile-time variable which may get instantiated to an expression (rather than an integer) at runtime.
Functional/reified constraints
Reified constraints (whose last argument is a 0/1 variable) and functional constraints (whose last argument is an integer variable) can be written without their last argument within an expression context. The expression then effectively evaluates to the value of the missing (unwritten) argument.

The expressions allowed by GFD are a super-set of the expressions supported by Gecode (briefly, Gecode does not support functional and reified constraints in expressions, and collections of expressions are not supported). When an expression is posted, it is parsed and broken down into expressions and/or logical connectives supported by Gecode (more specifically, Gecode’s MiniModel’s IntRel and BoolExpr, along with any constraints). This is done to allow the user greater freedom in the code they write, and also to provide better compatibility with IC.

Note that posting of complex expressions is relatively expensive: they are first parsed at the ECLiPSe level by GFD to extract the sub-expressions and any new domain variables, and these sub-expressions (in the form of ECLiPSe structures) are then parsed again at the GFD C++ level to convert them to the appropriate Gecode data structures, which are then passed to Gecode. Gecode itself will then convert these data structures to the basic constraints that it supports.

Arithmetic constraints

These constraints impose some form of arithmetic relation between their arguments. Some of these constraints can occur inside expressions, while others are “primitive” versions of the constraint where the arguments are domain variables (or integers).

all_eq(?Collection,?Y)
Constrains each element of Collection to be equal to Y. Similar constraints for the other relations: all_ge/2) (greater than or equal to), all_gt/2 (greater than), all_le/2 (less than or equal to), all_lt/2 (less than), and all_ne/2 (not equal).
max(+Collection,?Max)
Constrains Max to be the maximum of the values in Collection. Similarly, min(+Collection,?Min) for minimum.
mem(+Vars,?Member [,?Bool])
Constrains Member to be the a member element in Vars. The reified version has the Bool argument.
scalar_product(++Coeffs,+Collection,+Rel,?Sum [,?Bool])
Constrains the scalar product of the elements of Coeffs and Collection to satisfy the relation sum(Coeffs*Collection) Rel P. Reified with Bool argument.
sum(+Collection,?Sum)
Constrains Sum to be the sum of the elements in Collection, or if the argument is of the form IntCollection*Collection, the scalar product of the commections.
sum(+Collection,+Rel,?Sum [,?Bool]
Constrains the sum of the elements of Collection to satisfy the relation sum(Collection) Rel Sum. Reified with Bool argument.

Ordering constraints

These constraints impose some form of ordering relation on their arguments.

lex_eq(+Collection1,+Collection2)
Constrains Collection1 to be lexicographically equal to Collection2. Constraints for the other lexicographic relations: lex_ge/2 (lexicographically greater or equal to), lex_gt/2 (lexicographically greater than), lex_le/2 (lexicographically less or equal to), Collection2. lex_lt/2, (lexicographically less than), lex_neq/2 (lexicographically not equal to).
ordered(+Relation,+Collection)
Constrains Collection to be ordered according to Relation.
precede(++Values,+Collection)
Constrains each value in Values to precede its succeeding value in Collection.
precede(+S,+T,+Collection)
Constrains S to precede T in Collection.
sorted(?Unsorted, ?Sorted)
Sorted is a sorted permutation of Unsorted.
sorted(?Unsorted, ?Sorted, ?Positions)
Sorted is a sorted permutation (described by Positions) of Unsorted.

Counting and data constraints

These constraints impose restrictions either on the number of values that can be taken in one or more collections of domain variables, and/or on the positions of values in the collection.

alldifferent(+Vars)
Constrains all elements of Vars are different.
alldifferent_cst(+Vars,++Offsets)
Constrains the values of each element plus corresponding offset to be pairwise different.
among(+Values, ?Vars, +Rel, ?N)
The number of occurrences (Occ) in Vars of values taken from the set of values specified in Values satisfies the relation Occ Rel N.
atleast(?N, +Vars, +V)
At least N elements of Vars have the value V. Similarly atmost(?N, +Vars, +V).
count(+Value, ?Vars, +Rel, ?N)
Constrains the number of occurrences of Value in Vars (Occ) to satisfy the relation Occ Rel N.
count_matches(+Values, ?Vars, +Rel, ?N)
The number of the elements in Vars that match their corresponding value in Values, Matches, satisfies the relation Matches Rel N.
element(?Index, +Collection, ?Value)
Constrains Value to be the Indexth element of the integer collection Collection.
gcc(+Bounds,+Vars)
Constrains the number of occurrences of each Value in Vars according to the specification in Bounds (global cardinality constraint).
nvalues(+Collection, +Rel, ?Limit)
Constrains N, the number of distinct values occurring in Collection to satisfy the relation N Rel Limit.
occurrences(+Value,+Vars,?N)
Constrains the value Value to occur N times in Vars.
sequence(+Low,+High,+K,+Vars,++Values)
The number of values taken from Values is between Low and High for all sequences of K variables in Vars. There is also a version for binary (0/1) variables: sequence(+Low,+High,+K,+ZeroOnes).

Resource and scheduling constraints

These constraints deal with scheduling and/or allocation of resources.

bin_packing(+Items,++ItemSizes,+BinLoads)
The one-dimensional bin packing constraint with loads: packing M items into N bins, each bin having a load specified in BinLoads.
bin_packing(+Items,++ItemSizes,+N,+BinSize)
The one-dimensional bin packing constraint: packing M items into N bins of size BinSize.
cumulative(+Starts,+Durations,+Usages,+ResourceLimit)
Single-resource cumulative task scheduling constraint. A version with optional tasks is also available: cumulative_optional(+StartTimes, +Durations, +Usages, +ResourceLimit, +Scheduled).
cumulatives(+Starts,+Durations,+Heights,+Assigned,+Capacities)
Multi-resource cumulatives constraint on specified tasks.
cumulatives_min(+Starts,+Durs,+Heights,+Assgn,+Mins)
Multi-resource cumulatives constraint on specified tasks with required minimum resource consumptions.
disjoint2(+Rectangles)
Constrains the position (and possibly size) of the rectangles in Rectangles so that none overlap. A version where placement of rectangles is optional is disjoint2_optional(+Rectangles).
disjunctive(+StartTimes, +Durations)
Constrains the tasks with specified start times and durations to not overlap in time. A version with optional tasks is also available: disjunctive_optional(+StartTimes, +Durations, +Scheduled).

Graph constraints

In these constraints, the arguments represent a graph, and the constraint imposes some form of relation on the graph.

circuit(+Succ)
Constrains elements in Succ to form a Hamiltonian circuit. A version allowing constant offsets is circuit_offset(+Succ,+Offset).
circuit(+Succ,++CostMatrix,?Cost)
Constrains elements in Succ to form a Hamiltonian circuit, with Cost being the cost of the circuit, based on the edge cost matrix CostMatrix. A version allowing constant offsets is circuit_offset(+Succ,+Offset,++CostMatrix,?Cost).
circuit(+Succ,++CostMatrix,+ArcCosts,?Cost)
Constrains elements in Succ to form a Hamiltonian circuit. ArcCosts are the costs of the individual hops, and Cost their sum, based on the edge cost matrix CostMatrix. A version with constant offsets is available as circuit_offset(+Succ,+Offset,++CostMatrix,+ArcCosts,?Cost),
ham_path(?Start,?End,+Succ)
Constrains elements in Succ to form a Hamiltonian path from Start to End. A version with constant offsets is available as ham_path_offset(?Start,?End,+Succ,+Offset).
ham_path(?Start,?End,+Succ,++CostMatrix,?Cost)
Constrains elements in Succ to form a Hamiltonian path from Start to End, with Cost being the cost of the path, based on the edge cost matrix CostMatrix. A version with constant offsets is available as ham_path_offset(?Start, ?End, +Succ, +Offset, ++CostMatrix, ?Cost).
ham_path(?Start,?End,+Succ,++CostMatrix,+ArcCosts,?Cost)
Constrains elements in Succ to form a Hamiltonian path from Start to End. ArcCosts are the costs of the individual hops, and Cost their sum, based on the edge cost matrix CostMatrix. A version with constant offsets is available as ham_path_offset(?Start, ?End, +Succ, +Offset, ++CostMatrix, +ArcCosts, ?Cost).
inverse(+Succ,+Pred)
Constrains elements of Succ to be the successors and Pred to be the predecessors of nodes in a digraph. A version with offsets is also available: inverse(+Succ,+SuccOffset,+Pred,+PredOffset).

Extensional constraints

These are “user defined constraints” (also known as ad-hoc constraints), i.e. the allowable tuples of values for a collection of domain variables is defined as part of the constraint. These predicate differs in the way the allowable values are specified.

regular(+Vars, ++RegExp)
Constrains Vars’ solutions to conform to that defined in the regular expression RegExp.
table(+Vars, ++Table)
Constrain Vars’ solutions to be those defined by the tuples in Table. The variant table(+Vars, ++Table, +Option) allows the specification of the algorithm used.
extensional(+Vars, ++Transitions, +Start, +Finals)
Constrain Vars’ solutions to conform to the finite-state automaton specified by Transitions with start state Start and final states Finals.

Other constraints

Constraints that don’t fit into the other categories.

bool_channeling(?Var, +DomainBools, +Min)
Channel the domain values of Vars to the 0/1 boolean variables in DomainBools.
integers(+Vars)
Pseudo constraint (i.e. no constraint will be posted in Gecode): Vars’ domain is the integer numbers (within default bounds).

7.2.4  Search Support

GFD allows search to be performed in two ways: completely encapsulated in the external Gecode solver, or in ECLiPSe, supported by GFD’s variable selection and value choice predicates.

Performing search completely inside Gecode

Search can be performed in Gecode using one of its search engines. In this case, the search to produce a solution appears as an atomic step at the ECLiPSe level, and backtracking into the search will produce the next solution (or fail if there are none), again as an atomic step.

This direct interface to Gecode’s search engines is provided by gfd:search/6, and uses a syntax similar to that of the generic search/6 predicates (in lib(gfd_search) (see below), lib(ic) and lib(fd_search)).

As the search is performed in Gecode, it should be more efficient than doing the search in ECLiPSe, where the system has to repeatedly switch between Gecode and ECLiPSe when doing the search. As the search is a single atomic step from the ECLiPSe level, it is not suitable if your code needs to interact with the search, e.g. if you are using constraints defined at the ECLiPSe level, and/or if you are using other solvers during the search.

On the other hand, GFD’s search/6 is less flexible than the generic search – you can only use the predefined variable selection and value choice methods, i.e. you cannot provide user-defined predicates for the Select and Choice arguments.

The search engine to use is specified by the Method argument in search/6. One method provided by Gecode is bb_min – finding a minimal solution using branch-and-bound, which is not provided by the generic search.

Instead, branch-and-bound in ECLiPSe is provided by lib(branch_and_bound), which can be used with generic search’s search/6 to provide a similar functionality as the bb_min method of GFD’s search/6. The ECLiPSe branch-and-bound is more flexible, but is likely to be slower. Note that lib(branch_and_bound) can be used in combination with GFD’s search/6, but this is probably not useful unless you are doing some search in your own code in addition to that done by search/6, or if you want to see the non-optimal solutions generated by the search.

There are some differences in how search is performed by Gecode and generic search; the most significant is that all the built-in choice-operators of the generic search library make repeated choices on one variable until it becomes ground, before proceeding and selecting the next variable. Gecode’s built-in strategies on the other hand always interleave value choices with variable selection.

Here is the N-Queens example using gfd’s search/6:

:- lib(gfd).

queens_list(N, Board) :-
    length(Board, N),
    Board :: 1..N,
    (fromto(Board, [Q1|Cols], Cols, []) do
        ( foreach(Q2, Cols), param(Q1), count(Dist,1,_) do
            Q2 #\= Q1,
            Q2 - Q1 #\= Dist,
            Q1 - Q2 #\= Dist
        )
    ),
    search(Board, 0, input_order, indomain_min, complete, []).

Search in ECLiPSe using GFD primitives

The built-in Gecode search is appropriate when the problem consists exclusively of GFD-variables and GFD-library-constraints, and when the built-in search methods and search heuristics are sufficient to solve the problem.

As soon as any user-defined constraints or any other ECLiPSe solvers are involved, then the top-level search control should be written in ECLiPSe, in order to allow non-gfd propagators to execute between the labelling steps. Also the implementation of problem-specific search heuristics will usually make it necessary to lift the top-level search control to the ECLiPSe level. To make this possible, GFD provides primitives to support variable selection and value choice heuristics.

gfd:select_var(-X, +Collection, -Rest, ++Arg, ++Select)
Select a domain variable from Collection according to one of Gecode’s pre-defined selection criteria. These include criteria not available in other ECLiPSe solvers, like accumulated failure count.
gfd_search:delete(-X, +Collection, -Rest, ++Arg, ++Select)
Select (and remove) a domain variable from Collection. This is the generic implementation, (compatible with IC and FD solvers), providing a different choice of selection options, but likely to be less efficient than select_var/5.
gfd:try_value(?Var, ++Method)
This value choice predicate makes a binary choice on the domain of a variable, according to Method. It creates two search alternatives, which reduce the variable domain in complementary ways. Because the variable is not necessarily instantiated, this must be combined with a variable selection method that does not delete the selected variable, such as select_var/5.
gfd:indomain(?Var)
Instantiate Var to elements in its domain, using a default method.
gfd_search:indomain(?Var, ++Method)
A flexible way to nondeterministically assign values to finite domain variables according to Method. On success, Var is always instantiated. This is the generic implementation, (compatible with IC and FD solvers), providing a different choice of methods, and likely to be less efficient than try_value/5.

To mimic Gecode’s built-in search (where a variable selection step is usually interleaved with a binary choice on the variable domain), select_var/5 and try_value/2 can be used in the following way:

labeling(Vars, Select, Choice) :-
        ( select_var(V, Vars, Rest, 0, Select) ->
            try_value(V, Choice),
            labeling(Rest, Select, Choice)
        ;
            true
        ).

The same effect can be achieved by using select_var/5 and try_value/2 together with the generic gfd_search:search/6 predicate:

gfd_search:search(Vars, 0,
        select_var(Select), try_value(Choice), complete, [])

Combining select_var/5 with indomain/2 results in a scheme where (possibly several) value choices on a variable are made until the variable is ground, before proceeding to select the next variable:

gfd_search:search(Vars, 0,
        select_var(Select), indomain(Choice), complete, [])

For even more complex user-defined heuristics, various properties associated with a variable and its domain can be obtained using predicates described in section 7.3.3. Note that these include properties that are not available in ECLiPSe’s solvers, such as weighted degree (a.k.a. accumulated failure count).


Previous Up Next