390 likes | 535 Views
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. Constraint Satisfaction Problems (CSP).
E N D
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
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
Static properties of a CSP network • Node consistency • vDi: ci(v) • Not very interesting • Arc consistency • vDi wDj : cij(v,w) • Most relevant • Path consistency • vDi wDj uDk: cik(v,u),ckj(u,w) • Usually too expensive …
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 125-148, 1999.
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
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
AC-4 - initialization • procedure INITIALIZE • Q <- {}; • S <- {}; % initialize each element of structure S • for each (Vi,Vj) in arcs(G) do % (Vi,Vj) and (Vj,Vi) are same elements • 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
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
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?
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)
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 • …
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
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
suspend (delay) Delayed Goals s2 s3 s1 r1, …, rk, q1, …, qm, sl s4 Prio 1 Prio 2 … Prio 12 schedule (wake) Resolvent in ECLiPSe p1 , … , pn .
Consistency check 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. 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. delay capacity(T,N) if var(T);var(N). Imperative style
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 ).
c_fwd X Y c_bwd Constraint via Propagation Goals Alternatively implemented as X Y c X Y c_prop
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 ).
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
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
Bounds consistency Bounds-consistent greater-equal: ge(X, Y) :- get_max(X, XH), % get current bounds get_min(Y, YL), impose_min(X, YL), % impose new bounds impose_max(Y, XH). X >= Y ( var(X),var(Y) -> suspend(ge(X,Y), 0, [[X,Y]->constrained]) ; true ),
More precise waking conditions Bounds-consistent greater-equal: 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). X >= Y
Variables, Attributes and Suspended Goals X { suspend: inst constrained ic: 1..9 min max hole } Y { suspend: inst constrained ic: 1..9 min max hole } ge(X,Y)
Directional propagators ge(X, Y) :- ge_fwd(X,Y), ge_bwd(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). X >= Y
Directional Propagators X { suspend: inst constrained ic: 1..9 min max hole } Y { suspend: inst constrained ic: 1..9 min max hole } ge_fwd(X,Y) ge_bwd(X,Y)
Repeated waking on domain updates • E.g. propagating bounds • 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! X >= Y
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
Variables, Attributes and Suspended Demon X { suspend: inst constrained ic: 1..9 min max hole } Y { suspend: inst constrained ic: 1..9 min max hole } ge(X,Y,Susp)
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) ).
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
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
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
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!
Implementing a new domain :- module(enum). % library name :- meta_attribute(enum, [ % attribute name and handlers unify: unify_enum/2, print: print_enum/2, compare_instances: ..., copy_term: ...]). make_enum_variable(Var, Values) :- % constructor Attr = enum(Values,_), init_suspension_list(2, Attr), add_attribute(Var, Attr). unify_enum(Value, Attribute) :- % unify handler check if Value is compatible with Attribute exclude(Var{Attribute}, Value) ?- % domain access primitive delete Value from possible values in Attribute ...
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
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
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) • ...