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.
To load the GFD library into your program, simply add the following directive at an appropriate point in your code.
:- lib(gfd).
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
.
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:
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.
The following domain constraints are supported by GFD:
::/2,3
are also supported as aliases.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).
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).
<=>/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
is not yet a domain variable, it is turned
into one.Expr*Expr
.IntCol
and ExprCol
must be the same size.X #> 3 and Y #< 8
.
These are restricted to the top-level of an expression,
and for reifiable expressions only,X #> 3 or Y #< 8
.
These are restricted to the top-level of an expression,
and for reifiable expressions only,X #> 3 xor Y #< 8
.
These are restricted to the top-level of an expression,
and for reifiable expressions only,X #> 3 => Y #< 8
.
These are restricted to the top-level of an expression,
and for reifiable expressions only,neg X #> 3
These are restricted to the top-level of an expression,
and for reifiable expressions only,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)
).
Expr
.
Should be used when Expr
is a compile-time variable
which may get instantiated to an expression (rather than an
integer) at runtime.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.
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).
Bool
argument.Bool
argument.Bool
argument.These constraints impose some form of ordering relation on their arguments.
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.
These constraints deal with scheduling and/or allocation of resources.
In these constraints, the arguments represent a graph, and the constraint imposes some form of relation on the graph.
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.
Constraints that don’t fit into the other categories.
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.
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, []).
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.
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).