Chapter 2 Building Blocks
2.1 Domains
Finite domains of integers are created, accessed and handled with
functions of module Domain
(described exhaustively in section
4.4). They are represented as functional objects of (abstract)
type Domain.t
and can therefore be shared. Domains are build with
different functions according to the domain property:
-
Domain.empty
is the empty domain;
-
Domain.create
is the most general constructor and builds a domain from a
list of integers, possibly unsorted and with duplicates;
-
Domain.interval
is a shorthand when domains are continuous;
-
Domain.boolean
is a shorthand for create [0;1]
;
-
Domain.int
is the largest (well, at least very large) domain.
Domains can be conveniently printed on an output channel with
Domain.fprint
and are displayed as lists of non-overlapping intervals and
single integers
[inf
1-sup
1;val
2;inf
3-sup
3;...]
in increasing order:
let discontinuous = Domain.create [4;7;2;4;-1;3];;
val discontinuous : Facile.Domain.t = <abstr>
Domain.fprint stdout discontinuous;;
[-1;2-4;7]- : unit = ()
let range = Domain.interval 4 12;;
val range : Facile.Domain.t = <abstr>
Domain.fprint stdout range;;
[4-12]- : unit = ()
Various functions allow access to properties of domains like, among
others (see 4.4), Domain.is_empty
, Domain.min
,
Domain.max
whose names are self-explanatory:
Domain.is_empty range;;
- : bool = false
Domain.max range;;
- : int = 12
Domain.member 3 discontinuous;;
- : bool = true
Domain.values range;;
- : int list = [4; 5; 6; 7; 8; 9; 10; 11; 12]
Operators are provided as well to handle domains and perform
easily set operations like Domain.intersection
, Domain.union
,
Domain.difference
and domain reduction like Domain.remove
,
Domain.remove_up
, Domain.remove_low
etc (see 4.4):
Domain.fprint stdout (Domain.intersection discontinuous range);;
[4;7]- : unit = ()
Domain.fprint stdout (Domain.union discontinuous range);;
[-1;2-12]- : unit = ()
Domain.fprint stdout (Domain.remove_up 3 discontinuous);;
[-1;2-3]- : unit = ()
Domain.fprint stdout (Domain.remove_closed_inter 7 10 range);;
[4-6;11-12]- : unit = ()
2.2 Variables
FaCiLe variables are attributed objects[3] which
maintain their current domain and can be backtracked during execution of search
goals.
Creation
FaCiLe finite domain constrained variables are build and handled by
functions of module Var.Fd
(described exhaustively in section
4.13). Variables are objects of type Fd.t
created by a call to one of the following functions of module Var.Fd
:
-
create d
takes a domain d
as argument.
-
interval inf sup
yields a variable whose domain ranges the
interval [inf..sup]
. It is equivalent to
create (Domain.interval inf sup)
.
-
array n inf sup
creates an array of n
``interval''
variables. Equivalent to Array.init n
~f:(fun _ -> Fd.interval inf sup)
.
-
int n
returns a variable already bound to n
.
The omnipresent fprint
function writes a variable on an output channel:
let vd = Fd.create discontinuous;;
val vd : Facile.Var.Fd.t = <abstr>
Fd.fprint stdout vd;;
_4{[-1;2-4;7]}- : unit = ()
Attribute
A FaCiLe variable can be regarded as either in one of the two following
states:
-
uninstantiated or unbound, such that an ``attribute''
containing the current domain (of size strictly greater than one) is
attached to the variable;
- instantiated or bound, such that merely an integer
is attached to the variable.
So an unbound variable is associated with an attribute of type
Var.Attr.t
holding its current domain, a unique integer
identifier and various data useless for the end-user. Functions to access
attributes data are gathered in module Var.Attr
:
-
dom
returns the current domain of an attribute;
- the mapping of
fprint
, min
, max
, size
,
member
of module Domain
applied on the embedded domain of
an attribute (e.g. min a
is equivalent to Domain.min (dom a)
);
-
id
to get the identifier of an attribute;
-
constraints_number
returns the number of ``active''
constraints still attached to a variable.
Although variables are of abstract type Fd.t
, function
Fd.value v
returns a concrete view of type
Var.concrete_fd = Unk of Attr.t | Val of int
1
of a variable v
,
such that a control structure that depends on the instantiation of a variable
will typically look like:
match Fd.value v with
Val n -> f_bound n
| Unk attr -> f_unbound attr
An alternative boolean function Fd.is_var
returns the current state
of a variable, sparing the ``match'' construct.
let v1 = Fd.create (Domain.create [1]) (* equivalent to Fd.int 1 *);;
val v1 : Facile.Var.Fd.t = <abstr>
Fd.is_var v1;;
- : bool = false
Fd.fprint stdout v1;;
1- : unit = ()
Domain reduction
Module Fd
provides three functions to perform
backtrackable domain reductions on variables, typically used within
instantiation goals and filtering of user-defined constraints:
-
subst v n
tries to instantiate variable v
to integer
n
. subst v n
fails whenever n
does not belong to
the domain of v
. v
must be unbound otherwise an exception
is raised.
let vr = Fd.interval 2 6;;
val vr : Facile.Var.Fd.t = <abstr>
Fd.subst vr 7;;
Uncaught exception: Fcl_stak.Fail("Var.Fd.subst").
Fd.subst vr 5;;
- : unit = ()
Fd.fprint stdout vr;;
5- : unit = ()
Fd.subst v1 1;;
Uncaught exception:
Failure
"Fatal error: Fd.subst: bound variable (use Fd.unify on possible bound variable
)".
-
unify
is equivalent to subst
but can be called on
instantiated variables. If v
is bound to v_value
, then
unify v n
fails if n
is different from v_value
.
Fd.unify v1 2;;
Uncaught exception: Fcl_stak.Fail("Var.Fd.unify").
Fd.unify v1 1;;
- : unit = ()
-
refine v dom
reduces the domain of v
to dom
.
dom
must be included in the current domain of v
otherwise
an assert failure is raised with the byte code library facile.cma
or the system will be corrupted with the optimized native code library
facile.cmxa
.
Fd.fprint stdout vd;;
_4{[-1;2-4;7]}- : unit = ()
match Fd.value vd with
Val n -> () (* Do nothing *)
| Unk attr -> (* Remove every value > 2 *)
let new_dom = Domain.remove_up 2 (Var.Attr.dom attr) in
Fd.refine vd new_dom;;
- : unit = ()
Fd.fprint stdout vd;;
_4{[-1;2]}- : unit = ()
Whenever the domain of a variable becomes empty, a failure occurs (see
2.5 for more explanations about failure):
match Fd.value vd with
Val n -> () (* Do nothing *)
| Unk attr -> (* Remove every value < 4 *)
let new_dom = Domain.remove_low 4 (Var.Attr.dom attr) in
Fd.refine vd new_dom;;
Uncaught exception: Fcl_stak.Fail("Var.Fd.refine").
Access
Besides Fd.value
and Fd.is_var
which access the state of
a variable, module Fd
provides the mapping of module Domain
functions like Fd.size
, Fd.min
, Fd.max
, Fd.values
,
Fd.iter
and Fd.member
, and they return meaningful values whatever
the state (bound or unbound) of the variable may be:
let vr = Fd.interval 5 8;;
val vr : Facile.Var.Fd.t = <abstr>
Fd.size vr;;
- : int = 4
let v12 = Fd.int 12;;
val v12 : Facile.Var.Fd.t = <abstr>
Fd.member v12 12;;
- : bool = true
Function Fd.id
, unlike the previous ones, returns a unique identifier
for the variable only if it is uninstantiated, otherwise an exception is
raised. An order based on these identifiers is defined by function
Fd.compare
2 as well as an equality function Fd.equal
, observing
the two following rules:
-
bound variables are smaller than unbound variables;
- unbound variables are compared according to their identifiers.
Fd.id vr;;
- : int = 6
Fd.id v12;;
Uncaught exception: Failure "Fatal error: Fd.id: bound variable".
Fd.compare v12 (Fd.int 11);;
- : int = 1
Fd.compare vr v12;;
- : int = 1
Fd.id vd;;
- : int = 4
Fd.compare vd vr;;
- : int = -1
2.3 Arithmetic expressions
Arithmetic expressions and constraints over finite domain variables
are build with functions and operators of module Arith
(see
4.2).
Creation and access
Arithmetic expressions are objects of abstract type Arith.t
which
contain a representation of an arithmetic term over finite domain variables.
An expression is ground when all the variables used to build it
are bound; in such a state an expression can be ``evaluated'' with
function Arith.eval
which returns its unique integral value.
A call to Arith.eval
with an expression that is not ground raises
the exception Invalid_argument
. However, any expression can be printed
on an output channel with function Arith.fprint
.
A variable of type Fd.t
or an OCaml integer of type int
are not arithmetic expressions and cannot therefore be mixed up
with the latter. ``Conversion'' functions are provided by module
Arith
to build an expression from variables and integers :
-
Arith.i2e n
returns an expression which evaluates to
integer n
;
-
Arith.fd2e v
returns an expression which evaluates to n
when v
is instantiated and
Var.Fd.value v
evaluates to Val n
.
Handily enough, opening module Easy
allows direct access to most
useful functions of module Arith
, including i2e
and fd2e
:
let v1 = Fd.interval 2 5;;
val v1 : Facile.Var.Fd.t = <abstr>
let exp1 = fd2e v1;;
val exp1 : Facile.Arith.t = <abstr>
Arith.fprint stdout exp1;;
_7{[2-5]}- : unit = ()
Arith.eval exp1;;
Uncaught exception: Invalid_argument "Arith.eval: not ground".
Fd.unify v1 4;;
- : unit = ()
Arith.eval exp1;;
- : int = 4
Arith.fprint stdout (i2e 2);;
2- : unit = ()
Maximal and minimal values of expressions can be accessed by functions
Arith.max_of_expr
and Arith.min_of_expr
:
let exp2 = fd2e (Fd.interval (-3) 12);;
val exp2 : Facile.Arith.t = <abstr>
Arith.min_of_expr exp2;;
- : int = -3
Arith.max_of_expr exp2;;
- : int = 12
An arithmetic expression can also be transformed into a variable thanks
to function Arith.e2fd
which creates a new variable constrained
to be equal to its argument (see 2.4.2).
Operators
Module Arith
provides classic linear and non-linear arithmetic
operators to build complex expressions. They can be directly accessed
through the opening of module Easy
, which considerably ligthen
the writting of equation, especially for binary infix ones.
-
+~
, -~
, *~
, /~
: addition, substraction,
multiplication and division (a failure occurs whenever its second
argument evaluates to 0).
-
e **~ n
raises e
to the n
th power. n
is an integer.
-
x %~ y
:
modulo, i.e. x -~ (x /~ y) *~ y
. A failure occurs whenever
y
evaluates to 0.
-
Arith.abs
: absolute value.
let vx = Fd.interval 3 6 and vy = Fd.interval 4 12;;
let exp1 = i2e 2 *~ fd2e vx -~ fd2e vy +~ i2e 3;;
val exp1 : Facile.Arith.t = <abstr>
Arith.fprint stdout exp1;;
(((2*_9{[3-6]})+-(_10{[4-12]}))+3)- : unit = ()
Arith.min_of_expr exp1;;
- : int = -3
Arith.max_of_expr exp1;;
- : int = 11
Global arithmetic operators working on array of expressions are provided
as well:
-
Arith.sum exps
builds the sum of all the elements of
the array of expressions exps
.
-
Arith.scalprod ints exps
builds the scalar products of
an array of integers by an array of expressions. Arith.scalprod
raises Invalid_argument
if the two arrays have not the same
length.
-
Arith.prod exps
builds the product of all the elements of
the array of expressions exps
.
Their variable counterparts where the array of expressions is replaced by
an array of variables are defined as well: Arith.sum_fd
,
Arith.scalprod_fd
, Arith.prod_fd
.
let size = 5;;
val size : int = 5
let coefs = Array.init size ~f:(fun i -> i+1);;
val coefs : int array = [|1; 2; 3; 4; 5|]
let vars = Fd.array size 0 9;;
val vars : Facile.Var.Fd.t array =
[|<abstr>; <abstr>; <abstr>; <abstr>; <abstr>|]
let pscal_exp = Arith.scalprod_fd coefs vars;;
val pscal_exp : Facile.Arith.t = <abstr>
Arith.fprint stdout pscal_exp;;
(((((0+(_11{[0-9]}*1))+(_12{[0-9]}*2))+(_13{[0-9]}*3))+(_14{[0-9]}*4))+(_15{[0-9
]}*5))- : unit = ()
Arith.min_of_expr pscal_exp;;
- : int = 0
Arith.max_of_expr pscal_exp;;
- : int = 135
2.4 Constraints
2.4.1 Creation and Use
A constraint in FaCiLe is a value of type Cstr.t
. It can be
created by a built-in function (arithmetic, global constraints) or
user-defined (see 3.3). A constraint must be posted with the function Cstr.post
to be taken into account,
i.e. added to the constraints store.
When a constraint is posted, it is attached to the involved variables
and activated: propagation occurs as soon as the constraint is
posted. The constraint is also stored in a global state accessible
by the Cstr.active_store
function which returns the list of
all constraints still ``unsolved'', i.e. not yet globally consistent.
Constraints basically perform domain reductions on their involved
variables, first when posted and then each time that a particular
``event'' occurs on their variables. An event corresponds to a domain
reduction on a variable: the minimal or maximal value has changed,
the size of the domain has decreased or the variable has been bound.
To all these kinds of reduction are associated different events
that will trigger the ``awakening'' of the appropriate constraints.
See 3.2.1 for a more precise description of this event-driven
mechanism.
Constraints can also be printed on an output channel with function
Cstr.fprint
which usually yields useful information about
the variables involved and/or the name of the constraint.
2.4.2 Arithmetic Constraints
Simplest and standard constraints are relations on arithmetic
expressions (c.f. 2.3):
-
equality
=~
- strict and non-strict inequality
<~
, >~
, <=~
,
>=~
- disequality
<>~
FaCiLe provides them as infix operators
suffixed with the ~
character, similarly to expression operators. These
operators are declared in the Easy
module and don't need
module prefix notation whenever Easy
is opened. The small example
below uses the equality operator =~
and points out the effect on
the variables domains of posting the constraint equation
:
(* 0<=x<=10, 0<=y<=10, 0<=z<=10 *)
let x = Fd.interval 0 10 and y = Fd.interval 0 10 and z = Fd.interval 0 10;;
let equation = (* x*y - 2*z >= 90 *)
fd2e x *~ fd2e y -~ i2e 2 *~ fd2e z >=~ i2e 90;;
val equation : Facile.Cstr.t = <abstr>
(* before propagation has occured *)
Cstr.fprint stdout equation;;
+2._18{[0-10]} -1._19{[0-100]} <= -90- : unit = ()
Cstr.post equation;;
- : unit = ()
(* after propagation has occured *)
Cstr.fprint stdout equation;;
+2._18{[0-5]} -1._19{[90-100]} <= -90- : unit = ()
Notice that the output of the Cstr.fprint
function does not
look exactly like the stated inequation but shows how the two operands
of the main sum are internally reduced into new single variables
constrained to be equal to the latters. This mechanism is of course
hidden to the user and is only unfolded when calling Cstr.fprint
.
FaCiLe compiles and simplifies (``normalizes'') arithmetic constraints
as much as possible such that variables and integers may be scattered inside
an expression with no loss of efficiency. Therefore the constraint
ineq1
:
let x = Fd.interval (-2) 6 and y = Fd.interval 4 12;;
val x : Facile.Var.Fd.t = <abstr>
val y : Facile.Var.Fd.t = <abstr>
let xe = fd2e x and ye = fd2e y;;
val xe : Facile.Arith.t = <abstr>
val ye : Facile.Arith.t = <abstr>
let ineq1 = i2e 3 *~ ye +~ i2e 2 *~ xe *~ ye *~ i2e 5 *~ xe +~ ye >=~ i2e 4300;;
val ineq1 : Facile.Cstr.t = <abstr>
Cstr.fprint stdout ineq1;;
-10._26{[0-432]} -4._23{[4-12]} <= -4300- : unit = ()
which ensures 3y+(2xy×5x)+y ³ 4300, i.e. 10x2y+4y ³ 4300,
is equivalent to ineq2
:
let ineq2 = i2e 10 *~ (xe **~ 2) *~ ye +~ i2e 4 *~ ye >=~ i2e 4300;;
val ineq2 : Facile.Cstr.t = <abstr>
Cstr.fprint stdout ineq2;;
-10._31{[0-432]} -4._23{[4-12]} <= -4300- : unit = ()
Once posted, ineq1
or ineq2
incidentally yield a single
solution:
Printf.printf "x=%a y=%a\\n" Fd.fprint x Fd.fprint y;;
x=_22{[-2-6]} y=_23{[4-12]}
- : unit = ()
Cstr.post ineq1;;
- : unit = ()
Printf.printf "x=%a y=%a\\n" Fd.fprint x Fd.fprint y;;
x=6 y=12
- : unit = ()
It is also worthy to mention that arithmetic constraints involving
(large enough) sums of boolean variables are automatically detected
by FaCiLe and handled internally by a specific efficient mechanism.
The user may thus be willing to benefit from these features by
choosing a suitable problem modeling.
Note on precision and overflow
Users should be carefull when expecting the arithmetic solver to
compute bounds from variables with very large domain, that means
with values close to max_int
or min_int
(depending
of the system and architecture). Especially with exponentiation
and multiplication, an integer overflow may occur which will yield
an exception if compiled in byte code and a wrong calculation if
compiled in native code. An unexpected result when performing such
operations in native code should thus always be checked against the
byte code version.
Another possible source of miscalculation is the rounding performed
when computing bounds of expressions involving exponentiation and
multiplication or division: float operations are possibly performed
with a loss of accuracy which is most of the time corrected by
rounding, but errors might still occur for large numbers for which
nth root or division requires a high precision (let's say 10-6)
not provided by available float operators. Please send a bug report
if you think such a case occured (see page ??).
2.4.3 Global Constraints
Beside arithmetic constraints, FaCiLe provides so-called ``global
constraints'' which express a relation on a set of variables. They are
defined in separate modules in which a function (and possibly several
variants) usually named cstr
yields the constraint; these
functions takes an array of variables as their main argument.
The most famous one is probably the ``all different'' constraint which
expresses that all the elements of an array of variables must take
different values. This constraint is invoked by the function
Alldiff.cstr ?algo vars
where vars
is an array of
variables and algo
an optional argument that controls the
efficiency of the constraint (see 4.1):
-
Lazy
waits for the instantiation of a variable and
then removes the chosen value from the domains of the other variables;
-
Bin_matching evt
uses a more sophisticated algorithm
(namely ``bin matching'' [2]) which is called whenever
the event evt
(see 3.2.1) occurs on one of the array variable.
let vars = Fd.array 5 0 4;;
val vars : Facile.Var.Fd.t array =
[|<abstr>; <abstr>; <abstr>; <abstr>; <abstr>|]
let ct = Alldiff.cstr vars;;
val ct : Facile.Cstr.t = <abstr>
Fd.fprint_array stdout vars;;
[|_35{[0-4]}; _36{[0-4]}; _37{[0-4]}; _38{[0-4]}; _39{[0-4]}|]- : unit = ()
Cstr.post ct; Fd.unify vars.(0) 3;;
- : unit = ()
Fd.fprint_array stdout vars;;
[|3; _36{[0-2;4]}; _37{[0-2;4]}; _38{[0-2;4]}; _39{[0-2;4]}|]- : unit = ()
Module FdArray
provides the ``element'' constraint named
FdArray.get
which allows to index an array of variables by a variable,
and the min
(and max
) constraint which returns a variable
constrained to be equal to the variable that will instantiate to the minimal
(respectively maximal) value among the variables of an array:
let vars = [|Fd.interval 7 12; Fd.interval 2 5; Fd.interval 4 8|];;
val vars : Facile.Var.Fd.t array = [|<abstr>; <abstr>; <abstr>|]
let index = Fd.interval (-10) 10;;
val index : Facile.Var.Fd.t = <abstr>
let vars_index = FdArray.get vars index;;
val vars_index : Facile.Var.Fd.t = <abstr>
Fd.fprint stdout index;;
_66{[0-2]}- : unit = ()
Fd.fprint stdout vars_index;;
_67{[2-12]}- : unit = ()
let mini = FdArray.min vars;;
val mini : Facile.Var.Fd.t = <abstr>
Fd.fprint stdout mini;;
_69{[2-5]}- : unit = ()
FdArray.get
and FdArray.min
which produce a new variable
(and thus hide an underlying constraint) have also their
``constraint'' counterpart FdArray.get_cstr
and
FdArray.min_cstr
which take an extra variable as argument and
return a constraint of type Cstr.t
that must be posted to be
effective: FdArray.min_cstr vars mini
is therefore equivalent
to the constraint
fd2e (FdArray.min vars) =~ fd2e mini
,
and
FdArray.get_cstr vars index v to
fd2e (FdArray.get vars index) =~ fd2e v
.
More sophisticated global constraints are available as well as FaCiLe
built-in constraints:
-
the global cardinality constraint [5]
(a.k.a. ``distribute'' constraint):
Gcc.cstr
(see 4.6);
- the sorting constraint [1]:
Sorting.cstr
(see 4.11).
2.4.4 Reification
FaCiLe constraints can be ``reified'' thanks to the Reify
module
and its function Reify.boolean
(see 4.10) which takes
an argument of type Cstr.t
and returns a new boolean variable.
This boolean variable is interpreted as the truth value of the relation
expressed by the constraint and the following equivalences hold:
-
the boolean variable is bound to 1 iff the constraint is satisfied,
and the constraint is thereafter posted;
- the boolean variable is bound to 0 iff the constraint is violated,
and the negation of the constraint is thereafter posted;
otherwise, i.e. it is not yet known if the constraint is satisfied
or violated and the boolean variable is not instantiated, the reification
of a constraint do not perform any domain reduction on the variables involved.
In the following example, the boolean variable is_x_less_than_y
is constrained to the truth value of the inequation constraint x < y:
let x = Fd.interval 3 6 and y = Fd.interval 5 8;;
val x : Facile.Var.Fd.t = <abstr>
val y : Facile.Var.Fd.t = <abstr>
let x_less_than_y = Reify.boolean (fd2e x <~ fd2e y);;
val x_less_than_y : Facile.Var.Fd.t = <abstr>
Fd.fprint stdout x_less_than_y;;
_74{[0-1]}- : unit = ()
Cstr.post (fd2e y >=~ i2e 7);;
- : unit = ()
Fd.fprint stdout x_less_than_y;;
1- : unit = ()
Fd.fprint stdout (Reify.boolean (fd2e x =~ fd2e y));;
0- : unit = ()
When posted, the reification of a constraint calls the check
function
(see 3.3) of the constraint, which verifies
whether it is satisfied or violated (without performing domain reduction).
If it is violated, the negation of the constraint is posted with a call
to another function of the constraint dedicated to reification,
namely not
(see 3.3). Both functions are always defined
for all constraints but their default behaviour is merely exception
raising (Failure "Fatal error: ..."
) which means that the constraint
is actually not reifiable - as specified in the documentation of the
relevant constraints in the reference manual.
Mainly arithmetic constraints are reifiable (as well as the ``interval''
constraint of module Interval
, see 4.8) and
others (global ones) are not.
Reified constraint are by default waked up with the events triggering
its standard awakening (i.e. when directly posted, not reified) and those of its negation. This behaviour might possibly be too time
costly (for some specific problem) and the call to
Reify.boolean
with its optional argument
delay_on_negation
(see 4.10) set to false
disables it, i.e. the events associated with the negation of the
constraint are ignored.
Module Reify
also provides standard logic (infix) operators over
constraints:
-
&&~~
, conjunction;
-
||~~
, disjunction;
-
=>~~
, implication;
-
<=>~~
, equivalence;
-
not
3, negation;
These operators can be directly accessed through the opening of module
Easy
except Reify.not
(for obvious reasons). They can be combined
to yield new logic operators like the ``exclusive or'' for example:
let x = Fd.interval 3 5 and y = Fd.interval 5 7;;
val x : Facile.Var.Fd.t = <abstr>
val y : Facile.Var.Fd.t = <abstr>
let xor ct1 ct2 = Reify.not (ct1 <=>~~ ct2) in
let xor_cstr = xor (fd2e x =~ i2e 5) (fd2e y =~ i2e 5) in
Cstr.post (xor_cstr);
Cstr.post (fd2e x <=~ i2e 4);
Printf.printf "x=%a y=%a\\n" Fd.fprint x Fd.fprint y;;
x=_82{[3-4]} y=5
- : unit = ()
Furthermore, module Arith
contains convenient shortcuts to reify
its basic arithmetic constraints:
=~~
, <>~~
, <=~~
, >=~~
, <~~
, >~~
These operators stand for the reification (and transformation into
arithmetic expression) of their basic counterparts, i.e. they take
two arithmetic expressions as operands and yield a new arithmetic
expression being the boolean variable related to the truth value of the
arithmetic constraint. e1 =~~ e2
is therefore equivalent to
fd2e (Reify.boolean (e1 =~ e2))
These operators can also be directly accessed through the opening of module
Easy
. In the following example, the constraint stating that at least
two of the three variables contained in array vs
must be
greater than 5 is expressed with the reified greater or equal
>=~~
:
let vs = Fd.array 3 0 10;;
val vs : Facile.Var.Fd.t array = [|<abstr>; <abstr>; <abstr>|]
Cstr.post (Arith.sum (Array.map ~f:(fun v -> fd2e v >~~ i2e 5) vs) >=~ i2e 2);
Fd.fprint_array stdout vs;;
[|_91{[0-10]}; _92{[0-10]}; _93{[0-10]}|]- : unit = ()
If vs.(1)
is forced to be less than 5, the two other variables become
greater than 5:
Cstr.post (fd2e vs.(1) <=~ i2e 5);
Fd.fprint_array stdout vs;;
[|_91{[6-10]}; _92{[0-5]}; _93{[6-10]}|]- : unit = ()
2.5 Solving
Most constraint models are not tight enough to yield directly a single
solution, such that search (and/or optimization) is necessary to find
appropriate ones. FaCiLe uses goals to search for solutions.
All built-in goals and functions to create and combine goals are
gathered in module Goals
(see 4.7). This section
only introduces ``ready-to-use'' goals intended to implement basic
search strategies, but more experimented users shall refer to sections
3.1.2 and 3.4 where combining goals
with iterators, and building of goals from scratch are explained.
FaCiLe's most standard labeling goals is Goals.indomain
which
instantiates non-deterministically a single variable by disjunctively
trying each value still in its domain in increasing order. To be
executed, a goal must then be passed as argument of function
Goals.solve
which returns true
if the goal succeeds, and
false
if it fails.
let x = Fd.create (Domain.create [-4;2;12]);;
val x : Facile.Var.Fd.t = <abstr>
Goals.solve (Goals.indomain x);;
- : bool = true
Fd.fprint stdout x;;
-4- : unit = ()
So the first attempt to instantiate x
(to -4) obviously succeeds.
The values of the domain of x
can be enumerated with a slightly
more sophisticated goal which fails just after Goals.indomain
.
Module Goals
provides Goals.fails
, which is a goal that
fails immediately, and conjunction and disjunction operators,
respectively &&~
and ||~
(which can be directly accessed
when module Easy
is open), to combine simple goals.
Hence such an enumeration goal would look like:
Goals.indomain x &&~ Goals.fail
But the result of such a goal will be failure and the state of the
system (variable x
not instantiated) will not be restored. A
simple disjunction of this goal with the goal that succeeds
immediately, Goals.success
, yields the desirable behaviour :
Goals.indomain x &&~ Goals.fail ||~ Goals.success
In order to display the execution of this goal, a printing goal
gprint_fd
which prints a variable on the standard
output (but will not be detailed in this section, see 3.4.1)
can eventually be inserted (conjunctively) between indomain
and fail
:
let x = Fd.create (Domain.create [-4;2;12]);;
val x : Facile.Var.Fd.t = <abstr>
let goal = Goals.indomain x &&~ gprint_fd x &&~ Goals.fail ||~ Goals.success;;
val goal : Facile.Goals.t = <abstr>
Goals.solve goal;;
-4 2 12 - : bool = true
Note that the logic operators have standard priorities.
Module Goals
provides as well the function Goals.instantiate
that allows to specify the odering strategy of the labeling.
Goals.instantiate
takes as first argument a function which is given
the current domain of the variable (as single argument) and should return
an integer candidate for instantiation. Labeling of variable x
in
decreasing order is then merely:
let label_and_print labeling v =
labeling v &&~ gprint_fd v &&~ Goals.fail ||~ Goals.success;;
val label_and_print :
(Facile.Var.Fd.t -> Facile.Goals.t) -> Facile.Var.Fd.t -> Facile.Goals.t =
<fun>
Goals.solve (label_and_print (Goals.instantiate Domain.max) x);;
12 2 -4 - : bool = true
Function label_and_print
is defined here to lighten the
writting of enumeration goals (it takes only the instantiation goal
and the variable as arguments). In the example below, variable
x
is labeled in increasing order of the absolute value of its
values. Function Domain.choose
allows to specify only the
relevant order:
let goal =
label_and_print
(Goals.instantiate (Domain.choose (fun v1 v2 -> abs v1 < abs v2))) x;;
val goal : Facile.Goals.t = <abstr>
Goals.solve goal;;
2 -4 12 - : bool = true
Beside non-deterministic instantiation, FaCiLe provides also
Goals.unify
to enforce the instantiation of a variable (which
might be already bound) to a given integer value:
Goals.solve (Goals.unify x 2);;
- : bool = true
Fd.fprint stdout x;;
2- : unit = ()
Goals.solve (Goals.unify x 12);;
- : bool = false
Goals.solve (Goals.unify (Fd.int 0) 0);;
- : bool = true
2.6 Optimization
Classic Branch & Bound search is provided by the module Opti
with the function minimize
. It allows to solve a specified goal (g) while minimizing a cost defined by a finite domain
variable (c):
-
Goal g is solved and its associated cost c (minimum
of variable c) is computed;
- a new constraint stating c < c is added;
- the process loops until goal fails.
The third argument of Opti.minimize
is a function f
called each time a solution is found. The argument of the function is
the current cost (minimum of variable c).
Opti.minimize
returns the result of the last call to function
f
. Because the function f
may never be called, this
result is packed with an option
type. So if goal
does
not have any solution, returned value is None
. If goal
does have solutions and the search is not interrupted, the returned value is
(Some x)
where x
is the value returned by function
f
when the last solution was found.
The following example solves the minimization of x2+y2 while x+y=10 :
let x = Fd.interval 0 10 and y = Fd.interval 0 10 in
Cstr.post (fd2e x +~ fd2e y =~ i2e 10);
let c = Arith.e2fd (fd2e x **~ 2 +~ fd2e y **~ 2) in
let optimum =
Opti.minimize (Goals.indomain x &&~ Goals.indomain y) c
(fun c' -> Printf.printf "Found %d\\n" c'; (Fd.min x, Fd.min y)) in
match optimum with
None -> Printf.printf "No solution found\\n"
| Some (x, y) ->
Printf.printf "Optimal solution: cost=%d x=%d y=%d\\n" (Fd.min c) x y;;
Found 100
Found 82
Found 68
Found 58
Found 52
Found 50
Optimal solution: cost=50 x=5 y=5
- : unit = ()
- 1
- Type Var.concrete_fd constructors Unk and Val
stand respectively for ``Unknown'' (unbound) and ``Value'' (bound).
- 2
- Comparison functions return 0 if both arguments
are equal, a positive integer if the first is greater than the second
and a negative one otherwise (like specified in the OCaml standard
library).
- 3
- This one is obviously not infix.