Kiwi - A Minimalist CP Solver

04/28/2017 ∙ by Renaud Hartert, et al. ∙ 0

Kiwi is a minimalist and extendable Constraint Programming (CP) solver specifically designed for education. The particularities of Kiwi stand in its generic trailing state restoration mechanism and its modulable use of variables. By developing Kiwi, the author does not aim to provide an alternative to full featured constraint solvers but rather to provide readers with a basic architecture that will (hopefully) help them to understand the core mechanisms hidden under the hood of constraint solvers, to develop their own extended constraint solver, or to test innovative ideas.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

Nowadays, a growing number of real world problems are successfully tackled using constraint solvers. The hybridization of constraint solvers with other combinatorial technologies such as mixed integer programming [1, 3, 14], local search [12, 15, 17], and particularly conflict driven clause learning [6, 7, 16] has been at the center of substantial advances in many domains.

Unfortunately, while many open-source constraint solvers exist [4, 8, 9, 10, 11], modifying these solvers to hybridize them with other technologies, to extend them with specific structured domains, or to enhance them with new functionalities, may prove to be a time consuming and discouraging adventure due to the impressive number of lines of code involved in those open-source projects. Also starting the development of a constraint solver from scratch may also prove itself to be quite a challenge even if one has a deep understanding of the mechanisms and techniques at play in constraint solvers and constraint propagators.

The goal of this paper is to present Kiwi — a minimalist and extendable constraint solver. The source code of the core components of Kiwi is under 200 lines of Scala code and is the result of rethinking and simplifying the architecture of the open-source OscaR solver [11] to propose what we believe to be a good trade-off between performance, clarity, and conciseness.

By developing Kiwi, the author does not aim to provide an alternative to full featured constraint solvers but rather to provide students with a basic architecture that will (hopefully) help them to understand the core mechanisms hidden under the hood of constraint solvers, to develop their own extended constraint solver, or to test innovative ideas.111In this regard, Kiwi can be seen as an attempt to follow the initiative of minisat [5] but in the context of constraint programming.

2 Overview of the Solver

We start the presentation of Kiwi by briefly describing its three main components: propagation, search, and state restoration.

Propagation

The propagation system reduces the domain of the variables by filtering values that are part of no solution according to the constraints. The architecture of this system is described in section 4.

Search

Unfortunately, propagation alone is usually not sufficient to solve a constraint satisfaction problem. Constraint solvers thus rely on a divide-and-conquer procedure that implicitly develops a search tree in which each node is a subproblem of its ancestors. Leaves of this search-tree are either failed nodes – i.e. inconsistent subproblems – or solutions. Propagation is used at the beginning of each node to reduce the domain of the variables and thus to prune fruitless branches of the search tree. The search procedure of Kiwi is described in section 5.

State restoration

The state restoration system manages the different search nodes explored during search. Precisely, it is the component that restores the domain of the variables to their previous state each time a backtrack occurs. Its main purpose is to reduce the cost of copying the entire state of each node to provide users with an efficient trade-off between memory and processing costs. State restoration mechanisms are presented in the next section.

More than a constraint solver, Kiwi must be seen as a set of the components involved in the core of classic constraint solvers. Indeed, Kiwi does not provide users with abstraction to help them model and solve combinatorial problems. Also, we only give little attention to the filtering procedures involved in constraint propagation – which are arguably the most complex parts of every constraint solver. While the simplest binary constraints may only require a few lines of code, more complex global constraints usually rely on sophisticated algorithms and data structures. Scheduling constraints are a perfect example of such complex global constraints. The reader that is interested by global constraints and propagators may refer to [2] for a wide catalogue of global constraints and links towards the relevant literature.

3 State restoration

Copying and storing the whole state of each search node is usually too memory consuming for a real constraint solver. The state restoration system thus has to rely on different trade-offs to restore the domain of the variables as efficiently as possible.222State restoration is of course not limited to domains and may also be used to maintain incremental data structures or other components. There are three main approaches to implements such a system:

  • Copying. A complete copy of the domains is done and stored before changing their state.

  • Recomputation. Domains are recomputed from scratch when required.

  • Trailing. Domain changes are recorded incrementally and undone when required.

Trailing is the prominent approach used in many constraint solvers [4, 5, 9, 10, 11].333Although, some solvers such as Gecode [8] rely on an hybrid state restoration mechanism based on both copying and recomputation. The idea behind trailing is to maintain the sequence of changes that occured on a given branch since the root of the search tree. We call such a sequence the trail. Each node can be represented by a prefix of the trail that corresponds to the sequence of changes that lead to this node. Particularly, the sequence of changes that lead to a node is always an extension of the sequence that lead to its direct ancestor and so on (the root node being represented by the empty sequence). This incremental representation is convenient for backtracks as they can be performed by undoing the last changes of the trail until restoring the sequence of the desired node.

For instance, let us consider the sequence of changes depicted in Figure 1. The first node has been computed from the root node by applying 3 changes , the second node has been computed from the first node by applying 4 changes , and the third node has been computed from the second node by applying changes. We can easily restore the state of the second node by undoing the last changes of the trail until , i.e., .

Figure 1: A trail made of a sequence of changes. The sequence of changes that lead to node is always an extension of the sequence of changes that lead to node .

In Kiwi, we chose to manage state restoration with an extended trailing mechanism. The particularity of our system is that it allows each stateful object (e.g. the domain of a variable) to manage its own restoration mechanism internally. This mechanism can be based on trailing, copying, or even recomputation. The remainder of this section is dedicated to the implementation of this system.

3.1 Changes as an Abstraction

Each time a state changing operation is performed, the necessary information to undo this operation is stored on the trail. Such undo information can have many forms but is usually represented by a pair made of a memory location and of its corresponding value.

In Kiwi, we chose to directly store the functions responsible of undoing state changes as first class objects, i.e., as closures. Each stateful object thus relies on its own restoration mechanism which is handled by the closure itself.

The abstract class of an undo operation, namely Change, is presented in Code 3.1. It contains a single function undo which, as its name suggests, is responsible for undoing the change.

1 abstract class Change {
2   def undo(): Unit // Undo the change
3 }
Code 3.1: The Change abstract class.

The complete implementation of a stateful mutable integer is presented in section 3.3.

3.2 The Trail

Our trailing system is implemented with two stacks:

  • The first is a stack of Change objects that represents the trail. It is sorted chronologically such that the most recent change is on top of the stack (the root node being the empty stack).

  • The second stack maps each node of the current branch to its corresponding prefix in the trail. Thanks to the incrementality of the trail, only the position of the last change that lead to a node needs to be stored to characterize the whole sequence of changes that lead to this node.

Figure 2 illustrates the relation between both stacks.

Figure 2: The trailing system of Kiwi is implemented with two stacks.

The whole implementation of Trail, our trailing system, is presented in Code 3.2. Changes are registered using the store function that pushes them on top of trail (line 6). The newNode function registers the state of the current node by storing the current size of the trail (line 8). Conversely, the undoNode function restores the previous node by undoing the changes on top of the trail until its size corresponds to the size stored with the previous node (lines 10 to 12 and 15 to 17).

1 class Trail {
3   // Stacks to map nodes to the trail
4   private val trail = new Stack[Change]()
5   private val nodes = new Stack[Int]()
7   // Store the change
8   def store(change: Change): Unit = trail.push(change)
10   // Mark the beginning of a new node
11   def newNode(): Unit = nodes.push(trail.size)
13   // Restore the previous node
14   def undoNode(): Unit = {
15     if (nodes.size > 0) undoUntil(nodes.pop())
16   }
18   // Restore the root node
19   def undoAll(): Unit = undoUntil(0)
21   private def undoUntil(size: Int): Unit = {
22     while (trail.size > size) trail.pop().undo()
23   }
24 }
Code 3.2: Implementation of the trailing system of Kiwi

3.3 Trailed Integer

We now have all the pieces required to build our first stateful object: a stateful integer variable444Integer variable here refers to a mutable integer. called TrailedInt. As for classic integer variables, the value of TrailedInt can be accessed and updated. It however keeps track of the different values it was assigned to in order to restore them each time a backtrack occurs.

Similarly to most stateful objects in Kiwi, TrailedInt implements its own internal state restoration mechanism. It is based on a stack of integers that represents the sequence of values that were assigned to this TrailedInt since its initialization. Restoring the previous state of TrailedInt thus amounts to update its current value to the last entry of the stack (which is then removed from the stack).

The implementation of TrailedInt is presented in Code 3.3. The current value of the object is stored in the private variable currentValue. It is accessed with the getValue function. The setValue function is the one used to modify the value of the object. It pushes the current value on top of the stack of old values (line 7), updates currentValue (line 8), and notifies Trail that a change occurred (line 9).

The particularity of this implementation is that TrailedInt directly implements the Change abstract class and its undo operation (lines 12 to 14). This has the advantage of reducing the overhead of instantiating a new Change object each time a state changing operation is performed on TrailedInt.

1 class TrailedInt(trail: Trail, initValue: Int) extends Change {
3   private val oldValues = new Stack[Int]()
4   private var currentValue: Int = initialValue
6   def getValue: Int = currentValue
8   def setValue(value: Int): Unit = {
9     if (value != currentValue) {
10       oldValues.push(currentValue)
11       currentValue = value
12       trail.store(this)
13     }
14   }
16   override def undo(): Unit = {
17     currentValue = oldValues.pop()
18   }
19 }
Code 3.3: Implementation of TrailedInt, a stateful integer variable with its own internal state restoration mechanism.

3.4 Improvements

The trailing system we presented suffers from a major weakness. For instance, TrailedInt keeps track of all the values it was assigned to. However, only the values it was assigned to at the beginning of each state are required for state restoration. Hopefuly, we can easily fix this problem with timestamps. The idea is to associate a timestamp to each search node and to only register the current value of TrailedInt if it has not yet been registered at a given timestamp. The interested reader can refer to the 12th chapter of [13] for detailed explanations on how to improve trailing systems.

4 The Propagation System

Propagation is the process of using the filtering procedure embedded in the constraints to reduce the domain of the variables. To achieve this, the propagation system of Kiwi relies on the following entities:

  • The propagators which are responsible of performing constraint propagation by updating the domain of the variables, or to notify that a conflict occured. They contain the actual filtering procedures of their respective constraint. Propagators are registered on different types of events, such as the reduction of a variable’s domain, and need to be propagated each time one of these events occurs.

  • The propagation queue that synchronizes propagators to perform propagation until no further filtering can be achieved or a conflict has been detected.

  • The variables which are updated by the propagators and are responsible for notifying the propagation queue if a propagator needs to be considered for further propagation.

The interaction between those abstractions is illustrated in Figure 3. Observe that the propagation queue, PropagQueue, does not directly interact with the variables.

Figure 3: Interaction between the components at play in the propagation system of Kiwi.

4.1 Propagators

Kiwi does not contain any object that actually represents a constraint. Instead, a constraint is implicitly defined by the set of propagators which must ensure that the constraint’s logical relation holds. A constraint can thus be composed of a single propagator registered on all the variables in the constraint’s scope, or by a set of propagators, each one being registered on a subset of the constraint’s scope.

The abstract class Propagator is presented in Code 4.1. It contains the following functions:

  • The enqueued boolean is a flag used by the propagation queue to indicate that the propagator is awake and waiting for propagation.

  • The init function registers the propagator on the events of interest and perform its initial propagation. It return true if the propagation suceed and false if it lead to a conflict.

  • The propagate function performs the actual filtering. Like init, it returns false if and only if propagation lead to a conflict.

1 abstract class Propagator {
2   var enqueued: Boolean    // True if the propagator is awake
3   def init(): Boolean      // Register the propagator and propagate
4   def propagate(): Boolean // Calls the filtering procedure
5 }
Code 4.1: The Propagator abstract class.

As an example, we present the complete implementation of a simple binary constraint in section 4.4.

4.2 The Propagation Queue

The propagation queue, PropagQueue, contains all the propagators that are waiting for propagation. It is responsible for synchronizing these propagators until no further filtering can be achieved, or until a conflict has been detected. When running, the propagation process dequeues one propagator from the propagation queue and calls its propagate function to reduce the domain of some variables. Of course, reducing the domain of a variable usually awakes new propagators that are then enqueued in the propagation queue. This process is repeated until either the propagation queue becomes empty, meaning that no further propagation is possible, or until a conflict occured.

The implementation of PropagQueue is presented in code 4.2. The enqueue function enqueues a propagator in the propagation queue but only if it is not already contained in it (lines 5 to 10). The propagate function contains the main loop of the propagation process (lines 12 to 22). It empties the queue by dequeuing each propagator and calling their propagate function if no conflict has occurred yet. The function concludes by returning true if no conflict occurred; it returns false otherwise. Observe that the propagation queue is emptied even if a conflict occured.

1 class PropagQueue {
3   // The propagation queue
4   private val queue = new Queue[Propagator]()
6   // Enqueue the propagator if it is not already enqueued
7   def enqueue(propagator: Propagator): Unit = {
8     if (!propagator.enqueued) {
9       propagator.enqueued = true
10       queue.enqueue(propagator)
11     }
12   }
14   // Propagation process
15   def propagate(): Boolean = {
16     var unfailed = true
17     // Propagate the propagators
18     while (!queue.isEmpty) {
19       val propagator = queue.dequeue()
20       // Only call propagate if no conflict occurred
21       unfailed = unfailed && propagator.propagate()
22       propagator.enqueued = false
23     }
24     return unfailed
25   }
26 }
Code 4.2: Implementation of the propagation queue of Kiwi.

4.3 Variables

Variables are the last piece of our propagation system. Interestingly enough, Kiwi does not provide variables with an interface to implement. This design choice is one of the reasons that facilitates the extension of Kiwi with additional structured domain representations [13]. While variables does not have to respect any requirements, they usually offer the following functionalities:

  • A variable has a domain that represents the set of all possible values it could be assigned to.

  • A variable offers functions to remove possible values from its domain until it becomes a singleton, in which case the variable is assigned, or it becomes empty, meaning that a conflict occured. This domain must be restored to its previous state when a backtrack occurs.

  • A variable allows propagators to watch particular modifications of its domain. The role of the variable is then to enqueue these propagators in the propagation queue when one of these modifications occur.

As an example, we focus on a particular type of integer variable that we call interval variable.555Interval variables are commonly used by bound-consistent propagators [13]. The domain of an interval variable is characterized by its minimal value and its maximal value . It contains all the values contained in the interval . Particularly, the domain of an interval variable can only be reduced by increasing its minimal value or by decreasing its maximal value. We subsequently refer to the minimal and maximal values of the domain as the bounds of the variables.

The implementation of IntervalVar is presented in Code 4.3. As expected, it is made of two TrailedInt that represent the minimum and maximum value of the variable’s domain. The minWatchers and maxWatchers queues are used to store propagators that must respectively be awaken when the minimum value of the variable is increased or when the maximum value of the variable is decreased. These queues are filled using the watchMin and watchMax functions. The updateMin function is the one responsible for increasing the minimum value of the variable. It operates as follows:

  1. If the new minimum value exceeds the maximum value of the variable, then the domain becomes empty and the function returns false to notify this conflict (line 14).666We do not actually empty the domain of the variable because this change will be directly undone by backtracking.

  2. If the new minimum value is lower or equal to the current minimum value, then nothing happens and the function returns true (line 15).

  3. Otherwise, the function updates the minimum value to its new value, awakes all the propagators contained in the minWatchers queue and returns true to notify that no conflict occured (lines 17 to 19).

For the sake of conciseness, we do not describe the implementation of watchMax and updateMax as they are symmetric to their min version.

1 class IntervalVar(pQueue: PropagQueue, trail: Trail,
2                   initMin: Int, initMax: Int) {
4   private val min = new TrailedInt(trail, initMin)
5   private val max = new TrailedInt(trail, initMax)
6   private val minWatchers = new Queue[Propagator]()
7   private val maxWatchers = new Queue[Propagator]()
9   def getMin: Int = min.getValue
10   def getMax: Int = max.getValue
11   def isAssigned: Boolean = getMin == getMax
13   def watchMin(propagator: Propagator): Unit = {
14     minWatchers.enqueue(propagator)
15   }
17   def updateMin(newMin: Int): Boolean = {
18     if (newMin > max.getValue) false
19     else if (newMin <= min.getValue) true
20     else {
21       min.setValue(newMin)
22       for (propagator <- minWatchers) pQueue.enqueue(propagator)
23       true
24     }
25   }
27   // Similar to watchMin
28   def watchMax(propagator: Propagator): Unit = { ... }
30   // Similar to updateMin
31   def updateMax(newMax: Int): Boolean = { ... }
32 }
Code 4.3: Implementation of an interval variable.

4.4 The Lower or Equal Constraint

We now have all the components at hand to understand and implement our first constraint. We focus on the simple binary constraint where both and are interval variables (see Code 4.3).

The constraint is made of a single propagator called LowerEqual (see Code 4.4). Its propagate function ensures the following rules:

  • The maximum value of is always lower or equal to the maximum value of (line 12).

  • The minimum value of is always greater or equal to the minimum value of (line 13).

The propagate function returns false if ensuring these rules empties the domain of either or ; it returns true otherwise. The init function performs the initial propagation (line 4) and registers the propagator on its variables (lines 6 and 7). Like propagate, it returns true if the propagation succeeded and false if a conflict occurred. Note that the propagator does not have to be awaken when the maximum value of or the minimum value of change since they have no impact on both previous rules.

1 class LowerEqual(x: IntervalVar, y: IntervalVar) extends Propagator {
3   override def init(): Boolean = {
4     if (!propagate()) false
5     else {
6       x.watchMin(this)
7       y.watchMax(this)
8       true
9     }
10   }
12   override def propagate(): Boolean = {
13     if (!x.updateMax(y.getMax)) false
14     else if (!y.updateMin(x.getMin)) false
15     else true
16   }
17 }
Code 4.4: Implementation of a propagator to enforce the relation .

4.5 Improvement and Discussion

There are many ways to improve our propagation system. For instance, let us consider the LowerEqual propagator from the previous section. If the maximal value of becomes lower or equal to the minimum value of , then no further filtering is possible. We say that the propagator is entailed and thus does not have to be propagated anymore. We could implement this additional condition by adding a stateful boolean to the Propagator interface and by checking the value of this boolean each time the propagator is enqueued in the propagation queue. Another improvement is to add different levels of priority to propagators. Indeed, some propagators have a much faster filtering procedure than others. It is therefore useful to propagate these propagators first in order to give as much information as possible to slower propagators. The interested reader can refer to the 4th and 12th chapters of [13] for detailed additional improvements.

5 Search

The final part of our solver is the search system. Its aim is to explore the whole search space of the problem looking for solutions or proving that no solution exists. It uses trailing to manage the nodes of the search tree and applies propagation to prune fruitless branches. The search system is based on a depth-first-search algorithm that relies on heuristics to determine the order in which nodes are explored. The following sections are dedicated to the description of these components.

5.1 Heuristics and Decisions

The aim of the heuristic is to define and order the children of any internal node of the search tree. It does so by associating each node with a sequence of decisions, where a decision is any operation that transforms a node into one of its children – e.g. assigning a variable to a value, or removing that value from the domain of the variable. The order in which decisions are returned corresponds to the order in which children have to be visited in the search tree. Particularly, the heuristic returns an empty sequence of decisions if the current node is a valid leaf, i.e., a solution.

Let us first take a look at the interface of a Decision (see Code 5.1). It contains the single function apply that is responsible of applying the decision. It returns false if applying the decisions directly lead to a conflict (e.g. if we try to assign a variable to a value that is not part of its domain for instance); it returns true otherwise.

1 abstract class Decision {
2   def apply(): Boolean
3 }
Code 5.1: The Decision abstract class.

As an example, we consider the implementation of both decisions in Code 5.2. The GreaterEqual decision updates the minimal value of to . It returns true if is lower or equal to the maximal value of ; it returns false otherwise. The Assign decision assign the variable to the value . It returns true if is part of the domain of ; it returns false otherwise.

1 class GreaterEqual(x: IntervalVar, v: Int) extends Decision {
2   override def apply(): Boolean = x.updateMin(v)
3 }
5 class Assign(x: IntervalVar, v: Int) extends Decision {
6   override def apply(): Boolean = x.updateMin(v) && x.updateMax(v)
7 }
Code 5.2: Implementation of two decisions.

Code 5.3 presents the interface of Heuristic. It contains the single function nextDecisions which, as mentioned above, returns a sequence – an iterator precisely – of Decision objets that will be used by the search system to build the search tree.

1 abstract class Heuristic {
2   def nextDecisions(): Iterator[Decision]
3 }
Code 5.3: The Heuristic abstract class.

We illustrate the use of this interface with the StaticMin heuristic (see Code 5.4). It builds a binary search tree in which each left branch tries to assign a variable to its minimal value and each right branch tries to remove that value from the domain of its variable (see Figure 4). The order in which StaticMin tries to assign variables is statically determined by the order of the variables in the vars array. The first part of the heuristic (lines 6 to 7) searches for the index of the first unassigned variable in vars. This index is stored in the nextUnassigned stateful integer (line 8) – the use of a stateful integer is not required here but reduces the overhead of systematically scanning the first already assigned variables. If all variables have been assigned, the heuristic returns an empty iterator to inform the search system that the current node is a leaf (line 10). Otherwise, it selects the next unassigned variable and tries to assign it with its minimum value – left branch – or to remove that value from the variable’s domain – right branch – (lines 12 to 15).777The LowerEqual and the GreaterEqual decisions have similar implementations.

Figure 4: Search tree built by the StaticMin heuristic on variable .
1 class StaticMin(trail: Trail, vars: Array[IntervalVar])
2                 extends Heuristic {
4   private val nextUnassigned = new TrailedInt(trail, 0)
6   override def nextDecisions: Iterator[Decision] = {
7     // Search for the next unassigned variable
8     var i = nextUnassigned.getValue
9     while (i < vars.length && vars(i).isAssigned) i += 1
10     nextUnassigned.setValue(i)
11     // Compute the sequence of decisions
12     if (i == vars.length) Iterator() // leaf node
13     else {
14       val x = vars(i)
15       val v = x.getMin
16       // Assign x to v or remove v from the domain of x
17       Iterator(new LowerEqual(x, v), new GreaterEqual(x, v + 1))
18     }
19   }
20 }
Code 5.4: Implementation of a static binary search heuristic that tries to assign variables to their minimum value.

5.2 The Depth-First-Search

Our depth-first-search algorithm explores the search tree defined by the heuristic. It relies on propagation to reduce the domain of the variables and thus pruning fruitless branches of the search tree.

The particularity of our implementation is that it manages its own stack of nodes instead of relying on recursion. The link between the stack and the actual search tree is illustrated in Figure 5 (note that, by abuse of language, we subsequently refer to ‘decisions’ as ‘nodes’ and vice-versa). The stack (depicted on the left) contains the iterator of decisions of each node in the currently explored branch of the search tree. Precisely, the first iterator corresponds to the children of the root node, the second iterator corresponds to the children of node , and so on. The gray element of each iterator is its current element. Hence, dashed elements have already been read while white elements still have to be. The search tree (depicted on the right) follows the same color code. Dashed nodes are the root of already explored subtrees, gray nodes are part of the branch that is currently explored by the search, and white nodes still have to be visited. Node is the node that is currently visited by the search.

Figure 5: Relation between the stack of iterators and the search tree.

The stack starts empty at the beginning of the search to indicate that the root node has not yet been visited. The search then uses the heuristic to expand the root node by pushing its sequence of children on the stack. The first unvisited child on top of the stack is then selected and expanded in the same way. This process is repeated until, eventually, the search reaches a node with an empty sequence of decisions, i.e., a leaf. It then operates as follows:

  1. If the iterator on top of the stack still contains unvisited nodes, then the search backtracks by visiting the next element of this iterator – the next sibling of the current node.

  2. Otherwise, we know that the iterator on top of the stack does no contain any unvisited nodes. The search thus removes this iterator from the top of the stack and repeats this process.

Figure 6 illustrates this operation where steps (a) and (b) respectively correspond to the second and the first above situations. The search keeps exploring the search tree by pushing and popping iterators on the stack until it becomes empty, meaning that the whole search tree has been explored.

Figure 6: Relation between the stack and the search tree in case of backtrack.

The implementation of our depth-first-search algorithm is presented in Codes 5.5 and 5.6. As expected, it contains an internal stack of iterators that we call decisions (line 2). The onSolution function (line 4) is a hook to be overridden by the user. It defines the action to perform each time the search encounter a solution. The propagateAndExpand function (lines 5 to 15) is called each time a new node is visited. It returns false if and only if the currently visited node is a leaf (either it is a solution or not); it returns true otherwise. The first part of that function is to propagate the visited node (line 6). This operation may lead to a conflict in which case false is returned to indicate that the node is a failed leaf. Otherwise, we call the nextDecisions function of heuristic to get the children of the node as a sequence of decisions (line 7). If this sequence is empty, then we know that the node is a solution, we call the onSolution function, and returns false (lines 8 to 10). If the node actually has children, its iterator of decisions is pushed on the stack and the function returns true (lines 11 to 13).

1 class Search(solver: Solver, trail: Trail, heuristic: Heuristic) {
3   private val decisions = new Stack[Iterator[Decision]]()
5   // Called each time a solution is found
6   def onSolution(): Unit = {}
8   private def propagateAndExpand(): Boolean = {
9     if (!solver.propagate()) return false
10     val nextDecisions = heuristic.getDecisions()
11     if (nextDecisions.isEmpty) {
12       onSolution()
13       false
14     else {
15       decisions.push(nextDecisions)
16       true
17     }
18   }
20   def search(maxNodes: Int): Boolean = { ... }
21 }
Code 5.5: Solver

The search

function is probably the most complex piece of code of

Kiwi (see Code 5.6). It is the one that is actually responsible of exploring the search tree. It takes a maximum number of nodes to visit in parameter to limit the time spent exploring the search tree – which grows exponentially with the size of the problem to solve. The function returns true if it was able to explore the whole search tree without exceeding the maximum number of nodes; it returns false otherwise. The first part of the search is to call propagateAndExpand to perform the initial root propagation and expand the root node (line 4). If that call returns false, then we know that the root node is already a leaf (either it is a solution or not). The search thus returns true to notify the user that the whole search tree – which is made of a single node – has been explored. Otherwise, we know that the decisions stack contains its first non-empty iterator (i.e. the children of the root) and that we can start exploring the search tree. The actual search starts if the decisions stack contains at least one iterator – which is always true at this step – and if the maximum number of nodes is not exceeded (line 6). The first step of the loop is to check that the iterator on top of the stack still contains unapplied decisions, i.e., unvisited nodes (lines 7 and 8). The next step is one of the following:

  1. If the iterator still contains some decisions, then the search visits the next node by notifying the trailing system that we are building a new node (line 10), and then by applying the next decision of the top iterator (lines 11 and 12). The search undo the state of this new node if it is a leaf (line 13), i.e., if applying the decision directly lead to a conflict or if the propagateAndExpand function returned false.

  2. If the iterator does not contain any unapplied decision, then it is removed from the decisions stack (line 15) and the state of the current node is undone (line 16) (see Figure 6).

The main loop is repeated until either the decisions stack becomes empty or until the maximum number of visited nodes has been exceeded. The search then concludes by undoing all the not yet undone nodes and by removing all the remaining iterators from decisions (lines 20 and 21). It finally returns true if and only if it was able to explore the whole search tree (line 23).

1 def search(maxNodes: Int): Boolean = {
2   var nodes = 0
3   // Root propagation
4   if (!propagateAndExpand()) return true
5   // Start DFS
6   while (!decisions.isEmpty && nodes < maxNodes) {
7     val nextDecisions = decisions.top
8     if (nextDecisions.hasNext) {
9       nodes += 1
10       trail.newNode()
11       val decision = nextDecisions.next()
12       val success = decision.apply()
13       if (!success || !propagateAndExpand()) trail.undoNode()
14     } else {
15       decisions.pop()
16       trail.undoNode()
17     }
18   }
19   // Clear trail and decisions
20   trail.undoAll()
21   decisions.clear()
22   // Return true if the search is complete
23   return nodes < maxNodes
24 }
Code 5.6: The depth-first-search algorithm of Kiwi.

References

  • [1] J Christopher Beck and Philippe Refalo. A hybrid approach to scheduling with earliness and tardiness costs. Annals of Operations Research, 118(1-4):49–71, 2003.
  • [2] Nicolas Beldiceanu, Mats Carlsson, Sophie Demassey, and Thierry Petit. Global constraint catalogue: Past, present and future. Constraints, 12(1):21–62, 2007.
  • [3] Alexander Bockmayr and Nicolai Pisaruk. Detecting infeasibility and generating cuts for mip using cp. In

    Proceedings of the 5th International Workshop on Integration of AI and OR Techniques in Constraint Programming for Combinatorial Optimization Problems, CPAIOR

    , volume 3, 2003.
  • [4] Xavier Lorca Charles Prud’homme, Jean-Guillaume Fages. Choco3 documentation. TASC, INRIA Rennes, LINA CNRS UMR 6241, COSLING S.A.S., 2014.
  • [5] Niklas Eén and Niklas Sörensson. An extensible sat-solver. In Theory and applications of satisfiability testing, pages 502–518. Springer, 2003.
  • [6] Thibaut Feydy, Andreas Schutt, and Peter J Stuckey. Semantic learning for lazy clause generation. In Proceedings of TRICS Workshop: Techniques foR Implementing Constraint programming Systems, TRICS’13, Uppsala, Sweden, 2013.
  • [7] Thibaut Feydy and Peter J Stuckey. Lazy clause generation reengineered. In Principles and Practice of Constraint Programming-CP 2009, pages 352–366. Springer, 2009.
  • [8] Gecode Team. Gecode: Generic constraint development environment, 2006. Available from http://www.gecode.org.
  • [9] IBM ILOG CPLEX CP Optimizer. V12.6.
  • [10] Or-tools Team. or-tools: Google optimization tools, 2015. Available from https://developers.google.com/optimization/.
  • [11] OscaR Team. OscaR: Scala in OR, 2012. Available from
    https://bitbucket.org/oscarlib/oscar.
  • [12] Laurent Perron, Paul Shaw, and Vincent Furnon. Propagation guided large neighborhood search. Principles and Practice of Constraint Programming–CP 2004, pages 468–481, 2004.
  • [13] Francesca Rossi, Peter Van Beek, and Toby Walsh. Handbook of constraint programming. Elsevier Science, 2006.
  • [14] Domenico Salvagnin and Toby Walsh. A hybrid mip/cp approach for multi-activity shift scheduling. In Principles and practice of constraint programming, pages 633–646. Springer, 2012.
  • [15] Pierre Schaus. Variable objective large neighborhood search. Submitted to CP13, 2013.
  • [16] Andreas Schutt, Thibaut Feydy, Peter J Stuckey, and Mark G Wallace. Why cumulative decomposition is not as bad as it sounds. In Principles and Practice of Constraint Programming-CP 2009, pages 746–761. Springer, 2009.
  • [17] P. Shaw. Using constraint programming and local search methods to solve vehicle routing problems. Principles and Practice of Constraint Programming—CP98, pages 417–431, 1998.