Implementing Constraints
Overview
A look at the literature
CSPs
Arc consistency algorithms
Implementation in ECLiPSe:
Low level:
Suspensions and Attributes
Prototyping in ECLiPSe:
Intermediate: Constraint Handling Rules (CHR)
High level: Propia
2
Constraint Satisfaction Problems (CSP)
Much of the literature talks about “CSPs”
This usually refers to binary CSPs:
A fixed set of variables X1,…Xn
Every variable has a finite domain Di
arbitrary domain, does not have to be ordered
Binary (2-variable) constraints only cij(Xi,Xj)
constraint defined as sets of consistent value pairs
Any CSP can be transformed to binary CSP
We don’t normally do that
Instead generalise binary techniques
3
Static properties of a CSP network
Node consistency
vDi: ci(v)
Not very interesting
Arc consistency
vDi w Dj : cij(v,w)
Most relevant
Path consistency
vDi wDj u Dk: cik(v,u),ckj(u,w)
Usually too expensive
…
4
Arc consistency algorithms
Consistency in networks of relations [AC1-3]
A.K. Mackworth, in Artificial Intelligence 8, pages 99-118, 1977.
Arc and path consistency revised [AC4]
R. Mohr and T.C. Henderson, in Artificial Intelligence 28, pages 225-233, 1986.
A generic arc-consistency algorithm and its specializations [AC5]
P. Van Hentenryck, Y. Deville, and C.-M. Teng, in Artificial Intelligence 57, pages 291-321,
1992.
Arc-consistency and arc-consistency again [AC6]
C. Bessiere, in Artificial Intelligence 65, pages 179-190, 1994.
Using constraint metaknowledge to reduce arc consistency
computation [AC7]
C. Bessiere, E.C. Freuder, and J.-R. Régin, in Artificial Intelligence 107, pages 125148, 1999.
5
AC-1
procedure REVISE(Vi,Vj)
DELETE <- false;
for each X in Di do
if there is no such Y in Dj such that (X,Y) is consistent,
then
delete X from Di;
DELETE <- true;
endif;
endfor;
return DELETE;
procedure AC-1
end REVISE
Q <- {(Vi,Vj) in arcs(G),i#j};
repeat
CHANGE <- false;
for each (Vi,Vj) in Q do
CHANGE <- REVISE(Vi,Vj) or CHANGE;
endfor
until not(CHANGE)
end AC-1
G is the constraint graph, Q is a queue
6
AC-3
procedure AC-3
Q <- {(Vi,Vj) in arcs(G),i#j};
while not Q empty
select and delete any arc (Vk,Vm) from Q;
if REVISE(Vk,Vm) then
Q <- Q union {(Vi,Vk) such that
(Vi,Vk) in
arcs(G),i#k,i#m}
endif
endwhile
end AC-3
Re-check only arcs that were affected
Queue contains arcs (constraints)
This is a practical algorithm
Easy to generalise to n-ary constraints
7
AC-4 - initialization
procedure INITIALIZE
elements
Q <- {};
S <- {};
% initialize each element of structure S
for each (Vi,Vj) in arcs(G) do
% (Vi,Vj) and (Vj,Vi) are same
for each a in Di do
total <- 0;
for each b in Dj do
if (a,b) is consistent according to the constraint (Vi,Vj) then
total <- total+1;
Sj,b <- Sj,b union {<i,a>};
endif
endfor;
counter[(i,j),a] <- total;
if counter[(i,j),a]=0 then
delete a from Di;
Q <- Q union {<i,a>};
endif;
endfor;
endfor;
return Q;
end INITIALIZE
8
AC-4 - fixpoint computation
procedure AC-4
Q <- INITIALIZE;
while not Q empty
select and delete any pair <j,b> from Q;
counter[(i,j),a] <- counter[(i,j),a] - 1;
for each <i,a> from Sj,b do
if counter[(i,j),a]=0 & a is still in Di then
delete a from Di;
Q <- Q union {<i,a>};
endif
endfor
endwhile
end AC-4
Maintains support counters for each domain element
Queue contains information about the deleted domain value
9
Relation to Search
These algorithms look at the constraint network statically
They are not strong enough to enforce global consistency
of the network, so we still need search
Search decisions change some domain(s), which means
we may lose any achieved xxx-consistency
Overall design needs to answer 2 questions:
What level(s) of consistency do we want to employ?
At what time (during search) do we want which consistency?
10
Consistency during search
Generate and test
Instantiate all problem variables, then check all constraints
Standard backtracking
Check every constraint as soon as both its variables are instantiated
Forward Checking (~ Label Propagation)
Look ahead
When one variable is instantiated make the constraint arc consistent
When a variable is instantiated, make the whole graph arc consistent again
(maintain arc consistency at all times)
(This terminology used in P.v.Hentenryck: Constraint Satisfaction
in LP)
11
Other forms of consistency
Interval (bounds) consistency
Useful for ordered domains
We don’t look at every domain value
Only make sure that smallest and largest domain value are consistent
Depending on the constraint semantics, bounds consistency implies arc
consistency, or not
Used in many ECLiPSe libraries
lib(fd) - integer domain
lib(ic) - integer and real number domain
lib(ic_sets) - integer set domain
lib(ic_symbolic) - ordered symbolic domain
…
12
Implementing Constraints with
Suspensions and Atrributes
What you need to know
To implement additional constraint for existing
solver:
suspend / resume mechanism
the solver’s domain access interface
To implement a solver over a new domain:
variable attribute mechanism
14
Basic Programming Support
Suspending the execution of goals
delay-clause or suspend/3,4
Corresponds to the queue in AC-3!
Data/Event-driven waking on change
attaching to variables + condition
Change notifications
Allows to say when computation should happen
messages from variables to constraints (see lib(notify_ports))
Information about what changed (for AC-4 style algorithms)
Priorities for goals
Allows to tune efficiency
15
Resolvent in ECLiPSe
suspend
(delay)
s2
r1, …, rk,
Prio 1
q1, …, qm,
Prio 2 …
p1 , … , pn .
Prio 12
s3
s1
Delayed Goals
s4
sl
schedule
(wake)
16
Consistency check
delay capacity(T,N) if var(T);var(N).
Declarative style
capacity(1, N) :- N>=0.0, N=<350.0.
capacity(2, N) :- N>=0.0, N=<180.0.
capacity(3, N) :- N>=0.0, N=<50.0.
Imperative style
capacity(T,N) :- (var(T);var(N)), !,
suspend(capacity(T,N), 0, [T,N]->inst).
capacity(1, N) :- N>=0.0, N=<350.0.
capacity(2, N) :- N>=0.0, N=<180.0.
capacity(3, N) :- N>=0.0, N=<50.0.
17
Forward Checking
:- lib(ic).
delay capacity(Type,N) if var(Type),var(N).
capacity(Type, N) :- var(N),
( Type=1 -> N :: 0.0..350.0
; Type=2 -> N :: 0.0..180.0
; Type=3 -> N :: 0.0..50.0
; fail
).
capacity(Type, N) :- nonvar(N),
N>=0.0,
( N=<50.0 ->
Type :: [1,2,3]
; N=<180.0 -> Type :: [1,2]
; N=<350.0 -> Type =
1
; fail
).
18
Constraint via Propagation Goals
X
c
Y
X
c_prop
Y
Alternatively
implemented
as
c_fwd
X
Y
c_bwd
19
Forward Checking - 2 agents
capacity(Type, N) :-
capacity_forward(Type, N),
capacity_backward(Type, N).
delay capacity_forward(Type, _N) if var(Type).
capacity_forward(Type, N) :( Type=1 -> N :: 0.0..350.0
; Type=2 -> N :: 0.0..180.0
; Type=3 -> N :: 0.0..50.0
; fail
).
delay capacity_backward(_Type, N) if var(N).
capacity_backward(Type, N) :- N>=0.0,
( N=<50.0 ->
Type :: [1,2,3]
; N=<180.0 -> Type :: [1,2]
; N=<350.0 -> Type =
1
; fail ).
20
Explicitly Suspending Goals
Providing more flexibility than delay-clauses
Creation
make_suspension(+Goal, +Priority, -Suspension)
Attaching to attributed variable
insert_suspension(+Vars, +Suspension, +Index,
+AttributeName)
Combined create & attach
suspend(+Goal, +Priority, +Condition)
suspend(+Goal, +Priority, +Condition, -Suspension)
Delayed goal viewer shows suspended goals
21
Triggering of suspensions
When goals (constraints) are suspended, they are usually
attached to variables with trigger conditions:
X->inst
when X becomes instantiated (most specific)
X->constrained
when X becomes constrained in any way (most general)
X->ic:min, X->ic:max, X->ic:hole, X->type
when the lower bound / upper bound / other value in the domain of X changes
Other trigger conditions are defined by the various
solvers
22
Bounds consistency
Bounds-consistent greater-equal:
X >= Y
ge(X, Y) :( var(X),var(Y) ->
suspend(ge(X,Y), 0, [[X,Y]>constrained])
; true ),
get_max(X, XH),
current bounds
% get
get_min(Y, YL),
impose_min(X, YL),
bounds
% impose new
impose_max(Y, XH).
23
More precise waking conditions
Bounds-consistent greater-equal:
X >= Y
ge(X, Y) :( var(X),var(Y) ->
suspend(ge(X,Y), 0, [X->ic:max,Y>ic:min])
; true ),
get_max(X, XH),
get_min(Y, YL),
impose_min(X, YL),
impose_max(Y, XH).
24
Variables, Attributes and
Suspended Goals
X{
suspend:
inst
constrained
ic:
1..9
min
max
hole
}
ge(X,Y)
Y{
suspend:
inst
constrained
ic:
1..9
min
max
hole
}
25
Directional propagators
ge(X, Y) :ge_fwd(X,Y),
ge_bwd(X,Y).
X >= Y
ge_fwd(X, Y) :( var(X) -> suspend(ge_fwd(X,Y), 0, [X->ic:max])
; true ),
get_max(X, XH),
impose_max(Y, XH).
ge_bwd(X, Y) :-
( var(Y) -> suspend(ge_bwd(X,Y), 0, [Y->ic:min])
; true ),
get_min(Y, YL),
impose_min(X, YL).
26
Directional Propagators
X{
suspend:
inst
constrained
ic:
1..9
min
max
hole
}
ge_fwd(X,Y)
ge_bwd(X,Y)
Y{
suspend:
inst
constrained
ic:
1..9
min
max
hole
}
27
Repeated waking on domain updates
E.g. propagating bounds
X >= Y
ge(X, Y) :( var(X),var(Y) ->
suspend(ge(X,Y), 0, [X->ic:max, Y->ic:min])
; true ),
get_max(X, XH),
get_min(Y, YL),
impose_min(X, YL),
% impose new bounds
impose_max(Y, XH).
Properties
variables remain variables
arbitrary number of steps (limited only by domain size)
Potential performance problem:
re-suspending identical goal over and over again!
28
Domain/bounds propagation using a
“demon”
Same constraint as before:
ge(X, Y) :suspend(ge(X,Y,Susp), 0, [X->ic:max, Y->ic:min], Susp),
ge(X, Y, Susp).
:- demon ge/3.
% demon declaration
ge(X, Y, Susp) :( var(X),var(Y) -> true
% implicitly re-suspend
; kill_suspension(Susp) ),
% explicit kill
get_max(X, XH),
get_min(Y, YL),
impose_min(X, YL),
% impose new bounds
impose_max(Y, XH).
A “demon” does not need to be re-suspended
When woken, it splits into a woken and a suspended instance
Needs to be killed explicitly when no longer needed
29
Variables, Attributes and Suspended
Demon
X{
suspend:
inst
constrained
ic:
1..9
min
max
hole
}
ge(X,Y,Susp)
Y{
suspend:
inst
constrained
ic:
1..9
min
max
hole
}
30
Single-propagator max-constraint
mymax(A, B, M):get_bounds(A, MinA, MaxA),
get_bounds(B, MinB, MaxB),
get_bounds(M, MinM, MaxM),
( MinA >= MaxB ->
A = M
; MinB >= MaxA ->
B = M
; MinM > MaxB ->
A = M
; MinM > MaxA ->
B = M
;
call_priority((
Max is max(MaxA, MaxB),
Min is max(MinA, MinB),
impose_bounds(M, Min, Max),
impose_max(A, MaxM),
impose_max(B, MaxM),
Vars = [A,B,M],
( nonground(2, Vars, _) ->
suspend(mymax(A, B, M), 3, [Vars->ic:max,Vars->ic:min])
;
true
)
), 2)
).
31
N-ary constraints
Even more implementation choices:
Level of consistency
Algorithm
Eagerness
Trigger conditions and priority
Logical decomposition
e.g. alldifferent many disequality constraints
Operational decomposition
One or several propagator goals
Data-driven vs explicit fixpoint computation
32
Special case: Incremental checking
(1)
Value X occurs in List :
among(X, [Y|Ys]) :( var(Y) ->
suspend(among(X, [Y|Ys]),0,Y->inst)
; X = Y ->
true
;
among(X, Ys)
).
Properties
efficient, looks at most once at each list element
delayed goal may be different after each step
success after 1..N steps or failure after N steps
33
Incremental checking (2)
Value X does not occur in List:
not_among(X, []).
not_among(X, [Y|Ys]) :( var(Y) ->
suspend(not_among(X, [Y|Ys]),0,Y->inst)
;
X \= Y,
not_among(X, Ys)
).
Properties
Success after N steps or failure after 1..N steps
But: there may be an X at the end of the list, behind a variable!
Failure can be detected unnecessarily late
34
Parallel checking
Value X does not occur in List:
not_among(X, []).
not_among(X, [Y|Ys]) :( var(Y) ->
suspend(X\=Y, 0, Y->inst)
;
X \= Y
),
not_among(X, Ys).
Properties
Can expand into up to N delayed goals X\=Y
Failure detected as soon as possible
Normally more interesting than detecting success!
35
Implementing a new domain
:- module(enum).
% library name
:- meta_attribute(enum, [
unify: unify_enum/2,
print: print_enum/2,
compare_instances: ...,
copy_term: ...]).
% attribute name and handlers
make_enum_variable(Var, Values) :Attr = enum(Values,_),
init_suspension_list(2, Attr),
add_attribute(Var, Attr).
% constructor
unify_enum(Value, Attribute) :check if Value is compatible with Attribute
% unify handler
exclude(Var{Attribute}, Value) ?primitive
delete Value from possible values in Attribute
...
% domain access
37
Exercise 1
Write a constraint
atmost(+N, +List, +Value)
Meaning: at most N elements of List have value
Value
Behaviour: fail as soon as more than N elements are
instantiated to Value
Improvement: fail as soon as not enough variables
with Value in their domain are left over
38
Exercise 2
Write a constraint
offset(?X,+C,?Y)
which is like
offset(X,C,Y) :- Y #= X+C.
But maintains domain consistency (propagates “holes”)
Use
Suspension built-ins
Domain access primitives from the ic_kernel module
39
Domain attribute access in lib(ic)
Getting domain representation from a variable
get_domain_as_list(+Var, -ListOfValues)
get_bounds(+Var, -Min, -Max)
get_min(+Var, -Min)
get_max(+Var, -Max)
get_domain_size(+Var, -Size)
...
Updating a variable’s domain
impose_bounds(+Var, +Min, +Max)
impose_min(+Var, +Min)
impose_max(+Var, +Max)
exclude(+Var, +Value)
...
40
© Copyright 2026 Paperzz