Tuesday, November 20, 2012

Tidying up the house

Introduction

In the series of posts on problem solving by means of search, either blind of informed, we ended up with a single Problem class containing all the data and functions/methods necessary for declaring, solving and obtaining solutions for complex problems. We applied the Problem class to a number of real-world problems like the well-known missionaries&cannibals and N-queens problems, and rounded it all up with a road  navigation problem across the beautiful land of Romania.

I've decided I'm going to write a new series on solving problems in continuous spaces by means of local search and optimization. This is the kind of problems that a rover robot meets when travelling the world, for  example.

But, as my Mom uses to say, "before hosting new guests tidy up the house". So we're going to revise the implementation used across the previous series and clean it up according to OO principles. Thus this post serves as a quick introduction of OO design as well. Are you ready? Let's go then, I hope you enjoy the trip!

Problems with the Problem class (no pun intended)

The main issue with the Problem class we developed is that it mixes two different concepts in one single class. The class does not only model a problem but also its solution and the algorithm used to reach it.

This fact shows up notoriously in the need for two methods, treeSearch() and treeSearchWithCost(), which perform the same function using different algorithm classes. Moreover, there is no clue whatsoever in the class definition about which algorithms can be used with which algorithm class (i.e. which generators can be passed to each method).

Another indication of this mix up is that we cannot -at least not simultaneously- get multiple solutions to the same problem, not even using different algorithms.

Here's the definition of the Problem class we ended up with after the previous series (I've omitted class' private members for clarity):

class Problem(object):

    class Node(tuple): ...
            
    def __init__ ( self, initial, goal, successor, cost ): ...
        
    performance = property(getPerformance)
    
    resources = property(getResources)
    
    solution = property(getSolution)
    
    def solved ( self ): ...
    
    def solutionCost ( self ): ...
        
    def graphSearch ( self, gen ): ...
    
    def treeSearch ( self, gen, chk_dups = False ): ...

    def treeSearchWithCost ( self, gen ): ...
                        
    @staticmethod
    def depthFirstGenerator ( successor ): ...
        
    @staticmethod
    def breadthFirstGenerator ( successor ): ...
            
    @staticmethod
    def depthLimitedGenerator ( successor, max_level ): ...
        
    @staticmethod
    def uniformCostGenerator ( successor, cost ): ...
                    
    @staticmethod
    def iterativeDeepeningGenerator ( successor, anchor ): ...

    @staticmethod
    def iterativeLengtheningGenerator ( successor, cost, anchor ): ...
                    
    @staticmethod
    def bidirectionalSearchGenerator ( goal ): ...

    @staticmethod ...
    def greedyBestFirstGenerator ( successor, h ):
    
    @staticmethod
    def aStarGenerator ( successor, cost, h ): ...

Snippet 1 - Class structure of class Problem from the previous series

Our design goals with this exercise are as follows:
  1. Having a Problem class that models exclusively a single problem;
  2. Having a ProblemSolver class that solves problems expressed as instances of Problem;
  3. Separating the blind and cost-based algorithm classes so it can be chosen which algorithm class to use, if possible using the same interface
So we're going to split the Problem class into a -shorter- Problem class and a new ProblemSolver class. As criteria for what to put where, we'll use a simple but effective rule-of-thumb: if something can't live without a class, then it's part of that class. This criterion is not only used in OO design but in Entity-Relationship modelling as well.

Let's start with the Node sub-class. A Node is an artifact used to hold book-keeping information in a search tree while a problem is being solved by means of a given algorithm. Thus, from its definition and the fact it does not need a Problem instance to live, we'll remove it from Problem. We'll include it in ProblemSolver though, not because that's the only other place it could go but because it makes sense. After all, even if a Node can live without a ProblemSolver, what's the point anyway?.

Then with Problem's properties. Neither of those are properties of Problem (they can happily live without it), but seem to be part of ProblemSolver (apart from solution, which may be debatable, the others make no sense without being attached to a ProblemSolver).

And finally with the class' methods; none of the class nor instance methods are part of Problem according to our rule-of-thumb criterion, but seem to be part of ProblemSolver instead.

Thus we're moving the Node sub-class, properties and methods over to the new ProblemSolver class. We will end up with something like this:

class Problem(object):

    def __init__ ( self, initial, goal, successor, cost ): ...

class ProblemSolver(object):
        
    class Node(tuple): ...
            
    performance = property(getPerformance)
    
    resources = property(getResources)
    
    solution = property(getSolution)
    
    def solve ( self, problem, gen, chk_dups = False ): ...

    def solved ( self ): ...
    
    def solutionCost ( self ): ...
        
    def graphSearch ( self, gen ): ...
    
    def treeSearch ( self, gen, chk_dups = False ): ...

    def treeSearchWithCost ( self, gen ): ...
                        
    @staticmethod
    def depthFirstGenerator ( successor ): ...
        
    @staticmethod
    def breadthFirstGenerator ( successor ): ...
            
    @staticmethod
    def depthLimitedGenerator ( successor, max_level ): ...
        
    @staticmethod
    def uniformCostGenerator ( successor, cost ): ...
                    
    @staticmethod
    def iterativeDeepeningGenerator ( successor, anchor ): ...

    @staticmethod
    def iterativeLengtheningGenerator ( successor, cost, anchor ): ...
                    
    @staticmethod
    def bidirectionalSearchGenerator ( goal ): ...

    @staticmethod ...
    def greedyBestFirstGenerator ( successor, h ):
    
    @staticmethod
    def aStarGenerator ( successor, cost, h ): ...

Snippet 2 - Class split between Problem and ProblemSolver

We've taken a first stab at the design but we're not done yet. Still our ProblemSolver class mixes up the two algorithm classes and provides no clue at which generators can be applied to which search method. Since we're talking about algorithm classes here, the design seems to naturally lend towards separating ProblemSolver into two different classes: one using blind search and another one using cost-based search. In order to offer the same interface for both algorithm classes, we might devise a inheritance scheme where the basic ProblemSolver provides the foundation for problem solving like properties, initialization method etc. plus an implementation of the interface using blind search, whereas a derived CostBasedProblemSolver overloads the interface to use cost-based search. We'd end up with something like in the following snippet:


class Problem(object):

    def __init__ ( self, initial, goal, successor, cost ): ...

class ProblemSolver(object):
        
    class Node(tuple): ...
            
    performance = property(getPerformance)
    
    resources = property(getResources)
    
    solution = property(getSolution)
    
    def solve ( self, problem, gen, chk_dups = False ): ...

    def solved ( self ): ...
    
    def solutionCost ( self ): ...
        
    def graphSearch ( self, gen ): ...
    
    def treeSearch ( self, gen, chk_dups = False ): ...
                        
    @staticmethod
    def depthFirstGenerator ( successor ): ...
        
    @staticmethod
    def breadthFirstGenerator ( successor ): ...
            
    @staticmethod
    def depthLimitedGenerator ( successor, max_level ): ...
        
    @staticmethod
    def iterativeDeepeningGenerator ( successor, anchor ): ...
                    
    @staticmethod
    def bidirectionalSearchGenerator ( successor, goal ): ...

    @staticmethod ...
    def greedyBestFirstGenerator ( successor, h ):

class CostBasedProblemSolver(ProblemSolver):


    def treeSearch ( self, gen, chk_dups = False ): ...

    @staticmethod
    def uniformCostGenerator ( successor, cost ): ...
                    
    @staticmethod
    def iterativeLengtheningGenerator ( successor, cost, anchor ): ...
    
    @staticmethod
    def aStarGenerator ( successor, cost, h ): ...

Snippet 3 - Final class split fulfilling all our design goals

This re-factoring forces us to update all our unit tests to adapt to the new classes. The update is no big deal, just slight modifications to the test code. For example:

    initial = ...
    goal = ...
    cost = lambda s, n: ...
    successor = lambda n: ...
    h = lambda n: ...            
    problem = Problem(initial, goal, successor, cost)
        
    solver = ProblemSolver()
    solver.solve(problem, ...)
    self.assertTrue(solver.solved(problem))

Snippet 4 - Modified unit test code using the new class split

I specially love how much sense the solver.solved(problem) call seems to have. When calls to your interface seem to mean what they actually do, it's good sign that you're on the right track with your design.

The complete code for the re-factored search framework can be obtained here. As always, if you're actually leveraging this code in your designs please drop me a line so I know this work is being useful to someone else.

In the next post we'll implement Local Search and Optimization based problem solvers that enable us to solve problems in continuous spaces. I hope to see you there.

Friday, September 7, 2012

Finally: informed search algorithms

Introduction

In this post we'll look at how to implement informed search, a.k.a. heuristic search. Heuristic search leverages knowledge of the problem space to speed up search, which is a very nice feature when search trees are very wide and/or deep. Previous knowledge of the problem space is modeled as a heuristic function, which is a function receiving a state as argument and returning a value representing how far the heuristic function thinks that state is from a goal state.

The effectiveness of a heuristic search algorithm when compared to an uninformed search counterpart depends heavily on the heuristic function's ability to provide accurate hints about how far from a goal the current node being examined is. Devising accurate heuristics is an art in itself, though there are automated methods that may help. We won't be discussing heuristic devising in this post, but you can find additional information in the AI book I've used as reference across the series.

Heuristic function characterization

So can we use any function taking a state as input and providing a numerical value as output as an effective heuristic? The obvious answer is no. In order to be effective with a search algorithm, a heuristic function h must fulfill a number of properties:
  1. Returned value must be positive and not zero for all but the solution state, and must be 0 for the solution state;
  2. The function must be admissible, i.e. it must always underestimate the actual cost of reaching a solution from any given state;
  3. In addition to being admissible the function should be consistent, i.e. when evaluated for a state actually closer to a solution than a previous state it must return a value lower than that returned for said previous state (this condition is required only when checking for duplicates, which concerns us since if you check the treeSearchWithCost() method from the previous post you'll see that's always the case)
The above conditions are necessary for a heuristic search algorithm to be complete and optimal, though they are not sufficient, as we we'll see shortly.

It turns out that it is not very common to meet not consistent heuristics, specially with real-world problems like the one we used in the previous post. Let's continue using the same problem. A consistent heuristic for that problem is the straight line distance SLD between two towns. Since the straight line distance is the shortest possible distance between two points (in 3 dimensions, for the astro-physicists amongst you), it must be admissible. And applying the triangle inequality it can be seen that it is also consistent. We can model this heuristic function in our problem definition as follows:

sld_bucharest = {
    "Arad": 366,
    "Bucharest": 0,
    "Craiova": 160,
    "Drobeta": 242,
    "Eforie": 161,
    "Fagaras": 176,
    "Giurgiu": 77,
    "Hirsova": 151,
    "Iasi": 226,
    "Lugoj": 244,
    "Mehadia": 241,
    "Neamt": 234,
    "Oradea": 380,
    "Pitesti": 100,
    "Rimnicu Vilcea": 193,
    "Sibiu": 253,
    "Timisoara": 329,
    "Urcizeni": 80,
    "Vaslui": 199,
    "Zerind": 374,
}


h = lambda n: sld_bucharest[n]
Snippet 1 - Modeling the consistent SLD heuristic

We will be using the SLD heuristic with the algorithms that follow.

Greedy best-first search

The simplest form of heuristic search algorithm is called greedy best-first search. In this algorithm search always follows blindly the path with the lowest value of h.

Implementing greedy best-first search in our generic search framework is stupidly simple; we just have to apply breadth-first search sorting the fringe in order of increasing h. See the following code.


@staticmethod
def greedyBestFirstGenerator ( successor, h ):
    heuristicSuccessor = lambda n: sorted(successor(n), key=h)
    return Problem.breadthFirstGenerator(heuristicSuccessor)

Snippet 2 - Greedy best-first search algorithm

We wrap the passed successor() function into a heuristicSuccessor() function that merely sorts the children returned according to their h values. At every level down the tree, the search algorithm expands states in increasing order of estimated cost to the goal state. The first solution found at a given level is therefore the shortest solution down to that level. I underline "down to that level" because, as you'll see if you examine carefully the Romania road map from the previous post, the first solution popping up on the fringe is not actually the shortest solution (we know from the previous post that the first solution pops up at level 4 while the best solution pops up at level 6).

Therefore, despite using a consistent heuristic function greedy best-first search is complete but not optimal.

Notice that greedy-best first search is not a cost-based search algorithm, thus it must be used with the initial, unmodified search algorithm from the first post:


problem.graphSearch(Problem.greedyBestFirstGenerator(successor, h))
self.assertTrue(problem.solved())
print ("Solution found using greedy-best-first search: ", str(problem.solution))
print("Algorithm performance: ", problem.performance)

Snippet 3 - Testing greedy best-first search

A* search

Let's move on to a heuristic search algorithm that is both complete and optimal, as long as the heuristic function is consistent. It is the "a star" algorithm, which uses as sorting function a combination of what we know and what we believe, i.e.: f(s) = c(s) + h(s), being c(s) the actual cost of reaching state s from the initial state and h(s) the estimated cost of reaching the goal state from state s.

The mathematical proof that using a consistent heuristic function h with A* makes it optimal and complete can be found in the book. It is a beautiful while easy to follow demonstration so I encourage you to check it out.

The generator that enables A* in our common search framework is as follows:


@staticmethod
def aStarGenerator ( successor, cost, h ):
    def nestedGenerator ( fringe ):
        best_node = heapq.heappop(fringe)
        descendants = successor(best_node.item()) or []
        for desc in descendants:
            if desc != best_node.parent():  # going back where we come from
                f = best_node.cost() + cost(desc, best_node.item()) + h(desc)
                heapq.heappush(fringe, Problem.Node((f, desc, best_node)))
        yield fringe
    return nestedGenerator

Snippet 4 - Generator implementing A* search

Once more we're using the Python heap functions to deal with ordering the states according to their respective f values. If you compare the A* generator with the uniform cost generator from the previous post, you'll see the only difference is in the line that calculates the value of f. I could actually have used the same generator code for both but I think it is not a good idea to mix both algorithms under the same signature.

To test the A* algorithm with our example problem:


problem.treeSearchWithCost(Problem.aStarGenerator(successor, cost, h))
self.assertTrue(problem.solved())
print ("Solution found using A*: " + str(problem.solution))
print("Algorithm performance: ", problem.performance)

Snippet 4 - Generator implementing A* search

Voilá! If you check the output of this test, you'll see this algorithm is the only one that has been able to find the optimum solution for driving from Arad to Bucharest. This confirms the A* algorithm is both complete and optimal. However, this comes at a cost. The algorithm performance, measured as the number of states expanded, is by large the worst one we've seen so far.

There are several variants of the A* algorithm that tackle its weaknesses in order to make better usage of the  computing resources made available to the algorithm execution. I recommend that you check out the book to  round up your knowledge of heuristic search.

And this is it, we're done! I hope you've enjoyed this series of posts as much as I enjoyed writing it. One last thing: if you decide to use the common search framework in your own project, please leave a comment or something so I know my work is being useful to someone else.

Thanks for reading and catch you all around!


Monday, August 13, 2012

One further notch: cost-based search

Introduction

If you've followed closely the previous posts, you might be wondering what the cost formal argument in the generators' signatures is for. We've been consistently passing a constant lambda function as actual argument, and the code doesn't seem to be using it at all. Well, the time has come for providing an explanation: there's a kind of uninformed search algorithms that try to improve search time and/or space by filtering out nodes based on the actual cost of reaching a node. These algorithms are useful when the problem space allows allocating a numeric cost to the action of moving from a parent node to a child node. So far we haven't met such a problem space, hence we've been using a constant function (known as unitary cost function).

A problem space that lends easily to cost-based search is that of finding the shortest path by car between two towns. Here the cost function is the driving distance between two cities connected by a road. If we map nodes to cities and paths to roads, we might use a cost-based search algorithm to try to find the optimum way of driving from city A to city B.

Let's define such a problem space to use it with our search algorithms. We'll use Romanian towns and roads to honor the AI book all these posts are based on. Graphically the map looks as in the following figure (ignore the figure within braces inside each state, we'll see what's that about in the next post):

Figure 1 - Romania road map with driving distance between cities

We can model that map easily using a Python dictionary:

distance = {
    ("Oradea","Zerind"): 71,
    ("Oradea","Sibiu"): 151,
    ("Neamt","Iasi"): 87,
    ("Zerind","Arad"): 75,
    ("Iasi","Vaslui"): 92,
    ("Arad","Sibiu"): 140,
    ("Arad","Timisoara"): 118,
    ("Sibiu","Fagaras"): 99,
    ("Sibiu","Rimnicu Vilcea"): 80,
    ("Fagaras","Bucharest"): 211,
    ("Vaslui","Urziceni"): 142,
    ("Rimnicu Vilcea","Pitesti"): 97,
    ("Rimnicu Vilcea","Craiova"): 146,
    ("Timisoara","Lugoj"): 111,
    ("Lugoj","Mehadia"): 70,
    ("Pitesti","Bucharest"): 101,
    ("Pitesti","Craiova"): 138,
    ("Hirsova","Urziceni"): 98,
    ("Hirsova","Eforie"): 86,
    ("Urcizeni","Bucharest"): 85,
    ("Mehadia","Drobeta"): 75,
    ("Bucharest","Giurgiu"): 90,
    ("Drobeta","Craiova"): 120,
}

Instead of defining classes to model a graph or using a data structures library, we're defining the problem space as a simple dictionary mapping pairs of towns to the driving distance between them.  Pairs not present in the dictionary do not have a road connecting them.

Now let's define a cost function that uses the above problem space knowledge to measure the cost of driving  from town A to town B, as long as A and B are connected by a single road segment (i.e. no intermediate towns need to be traversed) :

cost = lambda s, n: distance.get((s,n), distance.get((n,s))) or (s==n and 0)

We're using the get() method of the dict class instead of direct subscripting, like in 'distance[("A","B")]'. The reason is that subscripting raises an exception if the index is not found in the dictionary, whereas get() gives us the chance of providing an additional argument that shall be used if the index is not found. In addition get() doesn't raise an exception if the search fails, but just returns None. That semantic allows us to condense the dictionary access code in a convenient single lambda function. Notice the driving distance between town A and itself is 0, so we add a final consideration to our cost function instead of adding all those stupid { ("A","A"): 0 } items to the dictionary.

The successor() function for this problem space can also be written as a simple lambda function:

successor = lambda n: [p[1-i] for i in range(1) for p in distance if p[i] == n]

I'll let you try to find out how the successor() function does its job. From the second for loop in the comprehension it can be deduced this is not the best way of obtaining the successors of a node. A tree-like data structure would allow us to be much more efficient, but it would make the code more complex too. This is a trade-off that you'll find over and over when using Python: you'll be able to express complex concepts easy and fast, sacrificing run-time performance. But you'll have your system ready to show very quickly and nothing prevents you from optimizing the heavy parts as soon there's time for it.

Cost-based search: the basics

The main difference between the brute-force approaches we've seen so far and cost-based search is the order in which states are expanded. Cost-based search algorithms pick the next state to expand from the current fringe using a function of the cost of reaching that state from the initial state. An important implication of that behavior is that it is not possible to pick a set of states to expand at one time, as e.g. the breadth-first search algorithm does; the reason being that with each state expanded, new child states cheaper than the ones already in the fringe might be generated, thus altering any order established before the expansion.

One advantage of the recursive algorithm proposed in the first post is that we don't need to perform housekeeping of the data structure holding the current fringe; all the states expanded down to a given recursion level are kept in the successive stacks of the enclosing recursion levels. This is possible because within a recursion level we don't need to access the states generated in an enclosing level. However, that status quo changes with cost-based search. After expanding a number of states at a given recursion level, the candidate to be expanded next might have been generated at any enclosing recursion level, in addition to the current level. This breaks our previous scheme and forces us to make slight modifications to the algorithm; unfortunately, these modifications turn the new algorithm incompatible with the previous one. The good news are that the new algorithm can efficiently behave as the previous one if the appropriate cost functions are used.

Another advantage of the previous algorithm is that it is able to model the search tree very efficiently by means of a dictionary relating children (keys) with parent (value). That has to do as well with the recursive nature of the algorithm and the order in which states are expanded; when returning from en enclosed recursion level because a solution has been found, the enclosing level can safely assume the parent of the state added to the solution by the enclosed level is held at its local stack and can be obtained as desc[sol(0)]. From the paragraph above you can conclude this situation changes as well, since the parent of a state included in the solution by a recursion level might be in the local stack of any enclosing level. Thus we need another way to model the parent-child relationship, while adding the step cost of going from parent to child to the equation.

Let's see how the slightly modified algorithm looks like:
class Problem(object):

    ...

    class Node(tuple):

        '''
        A simple tuple wrapper to make the algorithm implementations
        easier to understand
        '''
        cost = lambda n: n[0]
        item = lambda n: n[1]
        parent = lambda n: n[2]

    def getPerformance ( self ):
        return self.__performance
    performance = property(getPerformance)    

    ...


def treeSearchWithCost ( self, gen ):
    self.__performance = 0
    fringe = [Problem.Node((0, self.__initial, None))]
    visited = set()
    h = lambda n: fringe[0] == n and n not in visited
    sol = self.__recurseWithCost(fringe, gen, visited, h)
    if sol:
        self.__solution = [n.item() for n in sol if n]
                        
def __recurseWithCost ( self, fringe, gen, visited, h ):
    for n in filter(h, fringe):
        self.__performance += 1
        if self.__goal(n.item()):
            return [n]                    # the base case
                
    for desc in gen(fringe):
        try:
            visited.update(\
                map(Problem.Node.item, map(Problem.Node.parent, desc)))
        except TypeError:                # 'NoneType' is not subcsriptable
            visited and visited.clear()  # desc is the initial state, IDDF
        except AttributeError:           # 'NoneType' has no attribute 'visited'
            pass                         # user doesn't want us to check for dups
                     
        sol = self.__recurseWithCost(desc, gen, visited, h) # the recursive case
        if sol:
            try:
                sol.insert(0, sol[0].parent())
            except:
                pass
            return sol

Snippet 1 - Modified code for cost-based search

We've added a Node class to our definition of Problem. This is just a façade class for a tuple; we'll be using a tuple to hold the cost and the parent of a state, and using the façade class makes the code much more inteligible than using indexing, like in cost = node[0], parent = node[2] etc. In exchange for this syntactic sugar we lose some performance since a virtual function call is more expensive than indexing a small tuple, but in my oppinion that's a small loss compared to the understanding of the code we gain.

Another addition to the class is the performance property. We're going to use a performance figure to evaluate the relative quality of the generators we explore in the coming sections, and the performance property is the realization of that figure. As you can see in the code, the value of the property is reset each time we call the treeSearchWithCost() method, and is increased with every node selected for expansion. thus, at the end of each algorithm run the performance property holds the number of expanded states. The smaller this value, the more efficient the algorithm is in finding a solution to the problem.

The single remarkable change that remains to explain is within the treeSearchWithCost() method. As you can see, the filter function h is set to fringe[0] == n and n not in visited. If you remember the previous post, this function was used to prevent expanding already expanded states, returning True when the state is not in the list of visited nodes. Here we're enhancing it so in addition to that, it returns only the first element in the fringe. But why's that?

Remember brute-force algorithms just find a solution, they don't care whether it's the best one or if there are others (well, in fact breadth-first search shall find the solution with the smallest number of steps). However a cost-based optimal algorithm cannot go by that simple rule. Imagine a solution pops up in the fringe at absolute cost 200, while there are some other, not yet expanded states in that fringe with absolute costs 100, 150 and 185. Any of those three states has the potential to reach a solution with a cost lower than 200, so we cannot just return the solution that popped up and go to sleep.  The algorithm must explore all the states, in cost order, until it makes sure the first solution in the fringe is the best one. And that happens when the heap's head is a solution, because then there's no other unexpanded state whose absolute cost is lower than that of the head. Thus we'll check only the heap's head for deciding if we've found the solution.

Uniform cost-based search

Let's move on to the search algorithms themselves. We'll start with the simplest form of cost-based search, called uniform cost-based search. The generator that runs uniform cost-based search on our problem space is as follows:
@staticmethod
def uniformCostGenerator ( successor, cost ):
    def nestedGenerator ( fringe ):
        best_node = heapq.heappop(fringe)
        descendants = successor(best_node.item()) or []
        for desc in descendants:
            if desc != best_node.parent():  # going back to where we come from
                f = best_node.cost() + cost(desc, best_node.item())
                heapq.heappush(fringe, Problem.Node((f, desc, best_node)))
        yield fringe
    return nestedGenerator
Snippet 2 - Uniform cost search generator function

Surprisingly simple, isn't it?. But hidden in the apparent simplicity lies a major change with respect to the previous generators: as opposed to those, this generator modifies the fringe it receives from its caller in-place, and yields that same fringe instead of a newly generated one. This is because after yielding, the next state to expand might indeed be in the fringe received (if e.g. all the states just generated are more expensive than some state in that fringe). We could have created a copy of the fringe received and returned the modified copy instead, but when the fringe is large the performance penalty would be unacceptable.

Other than that, the generator uses a heap to sort the descendants of the state it expands in the current iteration by cost order, with the node having the lowest cost sitting at the beginning of the heap. Actually, the only requirement the algorithm has on the heap is to keep the lowest cost node at the head (remember that treeSearchWithCost() always expands the node at the head of the fringe), so using the Python heap implementation is very convenient due to the space and performance gains attained by partial ordering with respect to a fully sorted heap. For further details about the Python heap container see the Python Library Reference.

The following image shows the patch followed by uniform-cost search down to the fringe leading to Bucharest:

Figure 2 - Path followed by Uniform Cost-based search from Arad down to Bucharest

Like breadth-first search, uniform-cost search will find a solution if there's one, i.e. it is complete. If we consider path costs uniform-cost search will not only find a solution, but the best solution in terms of path cost, i.e. it is optimal. Notice breadth-first search might not be optimal in cost terms, since it always finds the shortest path in terms of number of steps but the cost of taking those steps might actually be higher than that of the steps in another solution with a higher number of steps.

We can test uniform-cost search with our brand new problem definition, as follows (I suggest you put the problem space together with the code that follows in a testNavigation() method)  :

initial = "Arad"
goal = "Bucharest"
problem = Problem(initial, goal, successor, cost)
problem.treeSearch(Problem.uniformCostGenerator(successor, cost))
self.assertTrue(problem.solved())
print ("Solution found using uniform-cost search: " + str(problem.solution))
print ("Algorithm performance: ", problem.performance)

Snippet 3 - Testing uniform cost search

If you check the solution the algorithm found, you'll see that it is not the apparently shortest Arad->Sibiu->Fagaras->Bucharest (4 steps). This is because the cost of that path is indeed higher than that of the Arad->Sibiu->R.Vilcea->Pitesti->Bucharest path (you can check that yourself by carefully looking at each step's cost in our problem state definition above).

Iterative Lengthening search

The same as we derived an iterative algorithm from breadth-first search (see the previous post), a cost-based iterative algorithm can be derived from uniform-cost search. The idea would be to keep the completeness and optimality guarantees of uniform-cost search while avoiding its excessive memory requirements (recall that uniform-cost search, as breadth-first search, keeps all the expanded nodes in local memory). This algorithm is called iterative lengthening search, and is coded as follows:

@staticmethod
def iterativeLengtheningGenerator ( successor, cost, anchor ):
    var = { "max_cost": 0, "new_max_cost": 0 }
    def nestedGenerator ( fringe ):
        max_cost = var["max_cost"]
        new_max_cost = var["new_max_cost"]
        best_node = heapq.heappop(fringe)
        descendants = successor(best_node.item()) or []            
        for desc in descendants:
            if desc != best_node.parent():  # going back to where we come from
                f = best_node.cost() + cost(desc, best_node.item())
                if f <= max_cost:
                    heapq.heappush(fringe, Problem.Node((f, desc, best_node)))
                else:
                    new_max_cost = min(f, new_max_cost or f)
        var["new_max_cost"] = new_max_cost
            
        if len(fringe) == 0:
            var["new_max_cost"] = 0
            var["max_cost"] = new_max_cost
            heapq.heappush(fringe, Problem.Node((0, anchor, None)))
                
        yield fringe
    return nestedGenerator
Snippet 4 - Iterative Lengthening search generator

If you check the iterative deepening search generator from previous post you'll find some resemblances here. The differences between that one and iterative lengthening search are that where iterative deepening search expands states in the current level in sequential order, iterative lengthening search expands states by cost order, starting with the one with the lowest cost; and instead of using a number of steps as limit, it uses the smallest cost from the previous iteration. The latter allows the algorithm to discard paths further down the tree which cost is higher than that of other state previously expanded. The search shall trace back and forth until it finds the optimum solution in terms of cost.

Unfortunately the addition of path costs to the equation considerably widens the alternative paths the algorithm has to explore, so it suffers from excessive trace back and forth overhead. Moreover, as I mentioned in the previous post this way of implementing an iterative search algorithm is far from optimal, since it keeps all the states expanded in a previous iteration in main memory. Actually, if you test this algorithm you'll probably get a recursion limit reached error from the Python interpreter.

Once again, we can test this algorithm with our problem space as follows:

initial = "Arad"
goal = "Bucharest"
problem = Problem(initial, goal, successor, cost)
problem.treeSearchWithCost(
    Problem.iterativeLengtheningGenerator(successor, cost, initial))
self.assertTrue(problem.solved())
print ("Solution found using ILS: " + str(problem.solution))
print ("Algorithm performance: ", problem.performance)
Snippet 5 - Testing iterative lengthening search

Conclusion

In the next -and last- post, we shall look at a different, smarter breed of algorithms that leverage additional knowledge about the problem space to improve its search performance. Nonetheless, the new algorithms fit seamlessly in the common treeSearchWithCost() search framework. I hope you follow me through to that point. While you wait eagerly :), there are two interesting exercises you can do on your own:

  1. Modify the treeSearch() algorithm from the previous post so it uses the Node class, instead of a simple dictionary-like structure. Also disable the optionality of duplicate checking, making it mandatory as in treeSearchWithCost()diff what you get with the treeSearchWithCost() code from this post: there's a remarkable difference between both we haven't talked about, can you reason where it comes from?
  2. Enhance the treeSearch() algorithm from the previous post with performance measuring. Run the different brute-force algorithms from the previous post on the Romania road map and check the differences with respect to uniform-cost based search.



Thursday, May 3, 2012

Solving well-known algorithmic problems

Introduction

Before looking at informed search, let me show you how to apply the search framework we've created to two well-known algorithmic problems. I hope this part turns out to be refreshing after the hard reading of the previous post.

The missionaries and cannibals problem

In this problem the world is made of a river, a boat, a boat operator, three missionaries and three cannibals. The boat operator can use the boat to cross the river, carrying at most 2 passengers in every trip. When the boat operator leaves a group alone on one side of the river, the number of missionaries and cannibals in that group must be balanced to prevent one group from overwhelming and -probably- killing the other while the boat operator is busy crossing the river.

The initial state of this world is three missionaries and three cannibals sitting on one side of the river. The goal of the problem is to cross everyone to the opposite side of the river, while keeping everyone alive (consider the boat operator is once a hardened marine and has no fear of cannibals, not even of missionaries!).

Before delving into the algorithmic solution to this problem, have a try at solving it using just your grey matter. First tackle it using a casual approach, and if that doesn't cut it use a systematic approach along the lines of problem formulation as described in the previous post.

A possible formulation of the problem that allows us to solve it using our search framework could be as follows:

initial = ((3,3), (0,0))
goal = ((0,0), (3,3))
cost = lambda x : 1
def successor ( s ):
    mis = lambda b: b[0]
    can = lambda b: b[1]
    ouch = lambda a: mis(a) > 0 and can(a) > 0 and mis(a) != can(a)
    def trip(a, b, a_to_b):
        if mis(a_to_b) > mis(a) or can(a_to_b) > can(a): # check physical limits
            raise Exception("Not enough people!")
        nxt_a = (mis(a) - mis(a_to_b), can(a) - can(a_to_b))
        if ouch(nxt_a):                        # discard fail a->b trips
            raise Exception("Holocaust!")
        nxt_b = (mis(b) + mis(a_to_b), can(b) + can(a_to_b))
        return nxt_a, nxt_b
                    
    result = []
    a, b = s[0], s[1]
    boat_floats = ((0,0), (1,0), (2,0), (1,1), (0,1), (0,2))
    for a_to_b in boat_floats:
        try:
            tmp_a, tmp_b = trip(a, b, a_to_b)
        except Exception as e:
            continue

        for b_to_a in filter(lambda b_to_a: b_to_a != a_to_b, boat_floats):
            try:
                nxt_b, nxt_a = trip(tmp_b, tmp_a, b_to_a)
            except Exception as e:
                continue
            if not ouch(nxt_a):                  # discard wrong b->a trips
                result.append((nxt_a, nxt_b))

    return result
Snippet 1 - Formulation of the missionaries and cannibals problem

From the snippet above, the toughest part is the function obtaining all the successors of a given state. We model each state as a 2-elements tuple, each element representing one side of the river; each river side in turn is modelled as a 2-elements tuple where the first element represents a number of missionaries and the second element represents a number of cannibals. Using this approach, the initial state is given by the expression ((3,3), (0,0)) whereas the goal state is given by the expression ((0,0), (3,3)). To model the boat contents we use a 2-elements tuple with the same meaning as a river side tuple.

Within the successor() function, we define three utility lambdas returning the number of missionaries and cannibals respectively in a side tuple (mis(side) and can(side)) and the validity of a side tuple (ouch(side)). Then we define a function simulating a boat trip between two sides (trip(side_a, side_b, boat_contents)) that returns what would be the new world state after performing that trip.

The main task of the successor() function is performed in the loop that follows. Given a world state, the loop tries all the possible combinations of trips side a -> side b and backwards, recording each valid combination in a list that is returned as output of the function.

Now we can apply the uninformed search algorithm from the previous post to the problem. Let's write it as a test method that we can integrate into our unit test suite:


    def testMissionariesAndCannibals(self):

        <paste code from snippet 1 right here>


        problem = Problem(initial, goal, successor, cost)
        problem.treeSearch(Problem.depthFirstGenerator(successor), True)

        self.assertTrue(problem.solved())

Snippet 2 - Solving the missionaries and cannibals problem

Observe how we've used the duplicate-checking version of the algorithm. If you've analyzed the problem a little bit as I suggest above, you've seen the state space is actually a graph. Therefore we try to avoid the search to get stuck in a loop within the graph. This problem is simple enough to be solved very quickly using depth-first search, but I suggest you do your own experiments using other search methods, with and without duplicate checking, and see the results.

The 8-queens problem

This well-known game theory problem consists of placing 8 queens on a chess board so no queen attacks any other. The problem can be extended to any number of queens since its solution does not depend on board size. However increasing board size has a big impact on the world's state space thus the resources used by the search, since the number of states is at least the cubic root of n!, being n the board size.

Let's formulate this problem in a way suitable for our search algorithm:



initial = ()
cost = lambda x : 1
goal = lambda b : b == n

def successor ( board ):
    def attacks ( q1, q2 ):
        return q1[0]==q2[0] or q1[1]==q2[1] or abs(q1[0]-q2[0])==abs(q1[1]-q2[1])
            
    result = []
    for col in range(len(board), n):
        for row in range(n):
            new_queen = (row, col)
            hostiles = [q for q in board if attacks(new_queen, q)]
            if len(hostiles) == 0:
                nxt_board = board + (new_queen,)
                result.append(nxt_board)            
    return tuple(result)

Snippet 3 - Formulation of the n-queens problem

Examine snippet 3 carefully. Can you spot anything unusual? In previous problems the goal was a definite state of the world, modelled as a constant (either some number or a tuple as in the missionaries&cannibals problem above).  That's because the solutions to those problems were exactly those sought states. In this case however, there are many valid solutions and the context of the problem does not tell us that one is preferred over the other. Moreover, specifying a single solution requires us to solve the problem in the first place! To find out whether a state is a solution to our problem we need to define a function taking the state as input and validating it against the goal constraints. Instead of checking the relative positions of all the queens currently on the board each time we call the goal() function, we chose to generate only valid states in the successor() function and then cheking if we have placed n queens on the board. By generating only valid states we severely decrease the number of states that have to be generated, which helps quite a bit memory-hungry search methods like breadth-first.

But for the goal() function to be used with our search algorithm, we need to make one (and just one) little  change. Goal checking must change from using the == operator to evaluating the goal() function. The following code snippet shows the updated search algorithm using the goal() function:

def treeSearch ( self, gen ):
    fringe = [self.__initial]
    self.__solution = self.__recurse(fringe, gen)

def __recurse ( self, fringe, gen ):
    for n in fringe:
        if self.__goal(n):
            return [n] # the base case

    for desc in gen(fringe):
        sol = self.__recurse(desc, gen) # the recursive case
        if sol:
            try:
                sol.insert(0, desc[sol[0]])
            except:
                pass
            return sol
Snippet 4 - Updated common search algorithm

I'll let as an exercise to you the modification of the unit tests we've coded so far so they work with the updated algorithm.

Now we can solve the problem surprisingly fast using duplicate-checking depth-first search, as follows:


    def testQueens ( self, n = 8 ):

        <paste code from snippet 3 right here>

        problem = Problem(initial, goal, successor, cost)
        problem.treeSearch(Problem.depthFirstGenerator(successor), True)
        self.assertTrue(problem.solved())
     
        print ("Solution found: " + str(problem.solution[-1]))


Snippet 5 - Solving the 8-queens problem

As before, I urge you to experiment with other search methods and obtain your own conclusions.

In the next post I'll show you how to use cost-based search with this algorithm framework. Till then, spend some time sharpening your python coding skills.


Tuesday, March 20, 2012

One uninformed search algorithm to rule them all

Introduction

Uninformed search algorithms perform exhaustive search of trees and graphs looking for a solution to a given  problem. The trees/graphs traversed by the algorithms represent states of the world the problem concerns. You can read more about solving problems applying uninformed search in chapter 3 of the book "Artificial Intelligence: a Modern Approach" by Russell&Norvig.

In this post I'm tackling problem solving applying uninformed search using Python, and more specifically some of the less known features of the lenguage such as recursion, generators and closures. I think this is interesting because it gives you the chance to learn about problem solving, search algorithms and advanced Python in one single place.

Ther're a few standard algorithms that perform uninformed search on trees/graphs. These are:
  • Depth-first search: search the tree/graph starting from the root node and going deep down as fast as possible. When a dead end state (i.e. a state with no successors) is found, trace back to the state immediately preceding the dead end and try going down through another successor. Search ends when a solution is found, or alternatively when all nodes have been searched. The following drawing shows how a depth-first search progresses through an example tree:
Figure 1 - Evolution of depth-first search on example tree
  • Breadth-first search: search the tree/graph starting from the root node and searching every generation of nodes in full before moving down to the next generation. In every-day language, search all the parents before start searching their sons. Search ends when a solution is found, or alternatively when all nodes have been searched. The following drawing shows how a breadth-first search progresses through an example tree:
Figure 2 - Evolution of breadth-first search on example tree
  • Limited-depth search: this is a variant of depth-first search where traceback to the parent node takes place not when a dead-end state is found, but when a pre-determined, constant depth has been reached. Again in every-day language, if we limit search depth to grand-sons, a depth-first seach would stop and traceback at every grand-son of the root, regardless that grand-son has children of its own. this algorithm tackles the biggest weakness of depth-first search algorithms: if the tree has no pre-determined depth or is a graph a depth-first search algorithm shall easily get stuck without finding a solution. The following drawing shows how a limited-depth search progresses though an example tree:
Figure 3 - Evolution of limited-depth search with limit 2 on example tree
  • Iterative deepening depth-first search: this is a variant of limited-depth search, which deepens down into the tree progressively one level at a time. So it starts with the grandparent, then it searches grandparent and its children, then it searches grandparent, parents and their children, and so on. Iterative deepening search improves one weak spot of limited-depth search: if the solution lies one or more levels further down than the pre-determined depth limit the algorithm won't find it. The following drawing shows how an iterative deepening depth-first search algorithm proceeds through an example tree:
Figure 4 - Evolution of IDDF search on example tree

  • Bidirectional search: this is actually two parallel breadth-first searches that start with the root node respective solution node, and go down respective up until both searches meet at a common node. This algorithm can prove very fast at certain problems but has the obvious drawback that a solution node must be easily identifiable, and all the parent nodes of a given node must be easy to obtain. These conditions do not hold for most search problems. The following drawing shows how a bidirectional search algorithm proceeds through an example tree:
Figure 5 - Evolution of bidirectional search on example tree

Each of the above algorithms possesses its own strengths and weaknesses, and each lends better to certain kind of problems. As a rule of thumb, the one providing best memory/processor usage trade-off for most problems is iterative deepening depth-first search (IDDF). Additionally, IDDF search is complete, which means it shall always find a solution if there's one (and the tree is of finite depth, of course). I'll let the theoretical details of algorithm properties to the respectable book mentioned at the beginning of this post, and focus from here on algorithm implementation using Python.

Problem formulation


Well-established game theory postulates that solving complex problems by algorithmic means requires a specific problem formulation approach. More specifically, a problem formulation consists of:
  1. An initial state of the world
  2. A function f(s) = S' producing a set of world states S' from a known state s
  3. A set of one or more goal states of the world that we want to reach
  4. A cost function c(s,t) giving the cost of reaching state t from parent state s
Let's write a simple Python class modeling this approach to problem formulation:

from functools import reduce # Only for Python 3

class Problem(object):
    '''
    Model of a problem following the problem formulation theory of chapter 3
    '''

    def __init__ ( self, initial, goal, successor, cost ):
        '''
        Constructor
        '''
        self.__goal = goal
        self.__successor = successor
        self.__cost = cost
        self.__initial = initial
        self.__solution = None

    def getSolution ( self ):

        '''
        Returns the current solution to the problem if any has been
        found, or None otherwise.
        The solution is a list of states, with the initial state at [0]
        and the goal state at [-1].
        '''
        return self.__solution
    solution = property(getSolution)

    def solved ( self ):

        '''
        Tells whether the problem has been solved
        '''
        return self.__solution and (self.__goal == self.__solution[-1])

    def solutionCost ( self ):

        '''
        Returns the cost of reaching the found solution if any has been found,
        or None otherwise
        '''
        if self.solved():
            return reduce(self.__cost, self.__solution)
Sinppet 1 - Python class modeling a problem definition

Search algorithms


Given the previous definition, the following methods implement depth-first and breadth-first search respectively using recursion:

def treeSearch ( self, search_func ):
    '''
    Call passing Problem.recurseDepthFirst or Problem.recurseBreadthFirst
    as search_func as appropriate.
    '''
    fringe = [self.__initial]

    self.__solution = search_func(self, fringe)

def recurseDepthFirst ( self, next ):
    if next == self.__goal:
        return [next] # the base case

    descendants = self.__successor(next) or [] # the recursive case
    for desc in descendants:
    sol = self.recurseDepthFirst(desc)
    if sol:
        sol.append(next)
        return sol

def recurseBreadthFirst ( self, fringe ):
    for n in fringe:
        if n == self.__goal:
            return [n] # the base case

    new_fringe = {}
    for n in fringe:
        descendants = self.__successor(n) or []
    for desc in descendants:
        new_fringe[desc] = n
    if len(new_fringe): # don't remove!!!
        sol = self.recurseBreadthFirst(new_fringe)
        if sol:
            sol.append(new_fringe[sol[-1]])
            return sol
Snippet 2 - Depth-first and breadth-first search implemented using recursion

We could go on and implement similar methods for depth-limited search and IDDF search. However, looking at the code for both search algorithms above we can't help but notice some remarkable similarities. The first paragraph of recurseDepthFirst() is a particularization of the first paragraph of recurseBreadthFirst() for a sequence of lenght 1. Thus it can be easily made common across both methods. Something similar goes for the second paragraph. That made me wonder if it'd be possible to perform both searches using a single algorithm, parameterized somehow to perform one or the other search. The interesting answer is yes, it is possible. Leveraging Python's generators we can write a common search method that uses one or the other search algorithm. Let's see how:


def treeSearch ( self, gen ):

    fringe = [self.__initial]

    self.__solution = self.__recurse(fringe, gen)



def __recurse ( self, fringe, gen ):

    for n in fringe:

        if n == self.__goal:

            return [n] # the base case



    for desc in gen(fringe):

        sol = self.__recurse(desc, gen) # the recursive case

        if sol:
            try:
                sol.insert(0, desc[sol[0]])
            except:
                pass
            return sol
Snippet 3 - Common search algorithm

The snippet above is a generalization of the structure of the previous algorithms. It is valid for both depth-first and breadth-first search, and the specific algorithm behavior is provided by means of the 'gen' argument. That can in principle be any iterable type, but we'll use a couple of generator functions to achieve the 'magic' we're looking for:
@staticmethod
def depthFirstGenerator ( successor ):
    def nestedGenerator ( fringe ):
        for n in fringe:
            descendants = successor(n) or []
            for desc in descendants:
                yield { desc: n }
    return nestedGenerator

@staticmethod
def breadthFirstGenerator ( successor ):
    def nestedGenerator ( fringe ):
        new_fringe = {}
        for n in fringe:
            descendants = successor(n) or []
            for desc in descendants:
                new_fringe[desc] = n
        yield new_fringe
    return nestedGenerator
Snippet 4 - Generator functions for depth-first and breadth-first search

In Python, any function containing a yield sentence is a generator function, or generator in short. The interpreter creates an iterable from the generator that can then be used in any construct where a normal iterable can be used. Since the functions do not use any members of the Problem class they could have been defined anywhere in a namespace accessible from the calling code, but to keep global namespace tidy we've defined the methods within the Problem class as static methods (using the @staticmethod decorator).

Notice the definition of function nestedGenerator() enclosed inside the supposed generator method. According to what you've just read, nestedGenerator() is the actual generator function (since it contains the yield sentence). The enclosing method is a facility to parameterize the actual generator with a successor() function, so we avoid passing the successor function around all the time. The Python facility that enables the nested function to "see" the actual value of the successor argument in run-time is called a closure. When a function is defined within another function, all the variables local to the enclosing function at the moment it is called are provided to the enclosed function as part of its namespace. This set of variables is known as the closure of the enclosed function (as a side note you could have made the successor function and the two above methods members of the Problem class, but then I wouldn't need to use the cool closure facility).

Here's how the Problem class can be used:

class ProblemTest(unittest.TestCase):

    def testProblem(self):
        initial = 1
        successor = lambda x : (2*x, 2*x + 1)
        cost = lambda x : 1
        goal = 8 # goal must be on a leftmost branch of the tree!!
        problem = Problem(initial, goal, None, cost)
        problem.treeSearch(Problem.depthFirstGenerator(successor))
        self.assertTrue(problem.solved())
        self.assertEqual(
            problem.solution, [1, 2, 4, 8],
            "Solution found is " + str(problem.solution))
Snippet 5 - Unit test class showing how to use the Problem class with depth-first search

Notice that we've not changed the Problem class constructor signature from the initial implementation; but since now the successor function is an argument to the pseudo-generator function, we set the constructor argument's actual value to None to make sure it is not being called elsewhere.

Notice also the comment beside the goal setting sentence. From your knowledge of search algorithm properties, can you tell what would happen if the goal value weren't in a leftmost branch of the tree?.

Using breadth-first search is just as easy:

        goal = 10
        problem = Problem(initial, goal, successor, cost)
        problem.treeSearch(Problem.breadthFirstGenerator(successor))
        self.assertTrue(problem.solved())
        self.assertEqual(
            problem.solution, [1, 2, 5, 10],
            "Solution found is " + str(problem.solution))
Snippet 6 - Continuation of ProblemTest.testProblem() method showing how to use breadth-first search

Observe in this case we can set any goal we want (I picked 10 so execution is fast, but you may try and test different values to see how it goes). The reason for this is that breadth-first is complete as long as the successor() function produces a finite number of states.

OK, so far we've implemented the two main classes of search algorithms with one single method. But now you should be intrigued: can the other search algorithms be implemented leveraging the same method as well?. Once again, I can say the answer is yes. Both depth-limited search and IDDF search can be implemented without changing a line by writing new specialized generators. Let's start with the easy one, depth-limited search:

@staticmethod
def depthLimitedGenerator ( successor, max_level ):
    var = { "level": 0 }
    def nestedGenerator ( fringe ):
        var["level"] = var["level"] + 1
        if var["level"] < max_level:
            for n in fringe:
                descendants = successor(n) or []
                for desc in descendants:
                    yield { desc: n }
        var["level"] = var["level"] - 1
    return nestedGenerator
Snippet 7 - Generator fulfilling the depth-limited search algorithm

We're diving into the darkest corners of Python here. Remember the concept of closures introduced above? What we want to do here is adding level and max_level variables to the generator's closure. Then inside the generator we increase the level variable until it reaches max-level, at which moment we trace back one generation (decreasing the level variable by one and returning). But why am I using the map subterfuge instead of just declaring variable level as normal, like "level = 0" and then "level = level + 1" inside the generator function?. Think for a while before continue reading.

The answer to the above question lies in the way the Python interpreter handles nested namespaces and variables local to a function. Whenever the interpreter defines a function a new namespace is assigned to that function; the new namespace inherits all the enclosing namespaces all the way up to the global namespace. Your function code can read the value of a symbol defined within any of those namespaces, however when writing a value to one of those symbols the interpreter creates a new symbol in the function namespace that shadows any symbol with the same name in any enclosing namespace.

Thus, in our generator above, static method depthLimitedGenerator() gets a new namespace in which variables successor and max_level exist, in addition to the Problem class and global namespaces. We could have defined a variable called level as well before the actual generator function nestedGenerator(); however, within that function we can only read the value of level, and if we do something like "level = level + 1", the interpreter creates a new level variable local to nestedGenerator()'s namespace that shadows the level variable from the enclosing function's namespace, rendering the value of that variable (part of nestedGenerator()'s closure) inaccessible. You can go and try this yourself.

So our only alternative is to wrap the variable we want to write from within the enclosed function in a map. In fact what we're doing is creating our own namespace local to the depthLimitedGenerator() and enclosed functions. From within nestedGenerator() we can safely write the new level value since in doing that we're not writing the map itself but the item referenced by the map.

Another quirk from the Python language is we cannot reuse the existing depth-first generator implementation; to be considered a generator, the function must explicitly contain the construct "yield". A generator called from within another function does not turn the latter into a generator itself.

You can test our depth-limited search implementation as follows:

    goal = 10
    problem = Problem(initial, goal, successor, cost)
    problem.treeSearch(Problem.depthLimitedGenerator(successor, 4))
    self.assertTrue(problem.solved())
    self.assertEqual(
        problem.solution, [1, 2, 5, 10],
        "Solution found is " + str(problem.solution))
Snippet 8 -  Continuation of ProblemTest.testProblem() method showing how to use depth-limited search 

Observe how we instantiate the generator passing a max_level value of 4. Can you guess what would happen if we passed a max_level value of 3 instead?

To conclude this post, I'll show you the generator implementing IDDF search. This is the most complex one but if you've made it this far you've got all you need to understand it. Here it goes:

@staticmethod
def iterativeDeepeningGenerator ( successor, anchor ):
    var = { "max_level": 1, "level": 0 }
    def nestedGenerator ( fringe ):
        var["level"] = var["level"] + 1
        if var["level"] <= var["max_level"]:
            new_fringe = {}
            for n in fringe:
                descendants = successor(n) or []
                for desc in descendants:

                    yield { desc: n }
                var["level"] = var["level"] - 1
            yield new_fringe
        else:
            var["level"] = 0
            var["max_level"] = var["max_level"] + 1
            yield [anchor]
    return nestedGenerator
Snippet 9 - Generator fulfilling the IDDF search algorithm

We're passing a new parameter to the pseudo-generator function (the one that creates the actual generator), which contains the state which the algorithm must trace back to when reaching the current level of depth in the search tree. Aside that, there's nothing really different in this generator from what you've already seen before.

One drawback of this way of implementing an iterative search is that when tracing back to the anchor node, we are keeping the whole set of states expanded to that moment in the stacks of the enclosing recursion levels; this is necessary for the previous algorithms since they use that set to trace back and to build the path to the solution once one has been found. But for an iterative algorithm it is a waste of memory since we won't need those states anymore (we're re-starting the algorithm from the initial node).

I'll let as an exercise to you fixing the search algorithm so previously generated states are dropped when re-starting the algorithm from the initial state. You won't achieve that by tweaking or re-ordering the lines in the generator, you need to change the treeSearch() method so it calls recurse() in a loop until a solution is found (or the search tree is exhausted). You'll need some additional stuff in the recurse() method to allow treeSearch() to distinguish when recurse() returns because it has exhausted the search tree, otherwise treeSearch() will get stuck in an endless loop!.

Checking for duplicates

So far we've been dealing with searching solutions for problems whose evolution follows a strict tree pattern. What happens if we apply the above algorithms to problems whose world evolves following a graph pattern? If the graph is acyclic it poses no major issue; it shall take longer for the algorithms to find a solution, but in the end if the algorithm is complete it shall find one. Things are different for cyclic graphs though. When the graph is cyclic all but the depth-limited search algorithm shall get stuck when entering the cycle.

Problems whose world evolves as a cyclic graph are not uncommon. For instance, when searching a route between two locations on a road map, usually ther're two roads between every intermediate location: the modern, fast roadway and the old, two-way slow road. Without further ado our algorithms shall get stuck navigating from A to B then from B to A over and over again.

Thus we need to enhance our algorithms for avoiding cyclic graphs, and the easiest way of doing it is allowing the algorithm to know when it has already "visited" a world state it reaches. Armed with this knowledge the algorithm may decide to not progress any further along that line and trace back and forth until it reaches a new, unknown state. This approach is known as checking for duplicates.

The idea is simple: every state that is expanded (i.e. its subsequent states are generated) by the algorithm is added to a list of visited states. Before expanding a state, the algorithm checks for the presence of the state in that list, and if it finds it then the state won't be expanded. Let's see how this logic fits in our general uninformed search method:

def treeSearch ( self, gen, chk_dups = False ):
    fringe = [self.__initial]
    visited = None
    h = None
    if chk_dups:
        visited = set()
        h = lambda n: n not in visited
    self.__solution = self.__recurse(fringe, gen, visited, h)

def __recurse ( self, fringe, gen, visited, h ):
    for n in filter(h, fringe):
        if n == self.__goal:
            return [n] # the base case

    for desc in gen(filter(h, fringe)):
        try:
            visited.update(desc.values())
        except:
            pass

    sol = self.__recurse(desc, gen, visited, h) # the recursive case
    if sol:
        try:
            sol.insert(0, desc[sol[0]])
        except:
            pass
    return sol
Snippet 10 - Enhanced search method able to navigate cyclic graphs

We have introduced a few changes here, haven't we? First, we allow the user of the treeSearch function to specify whether it wants the search to check for duplicates. Checking for duplicates makes the algorithm slower and more memory-hungry so it's a good idea to be able to short-circuit it if we know we're dealing with a tree before hand.

If our user wants duplicates checked, we create a list of visited states and a discarding function. The discarding function receives a state as actual argument and returns False when the state is to be discarded in the search, or True otherwise. Notice how the closure concept applies here once more? No matter where we call the h function, it shall have access to the visited variable. I've chosen the variable name h for this function for reasons that will be exposed when we talk about informed search algorithms; for now it should be enough to say the h stands for heuristic. Both the list of visited and the discard function are given to our recursive method as actual arguments.

Now within the recursive method, in the initial loop where we check for goals we use a filter() iterator instead of the whole fringe passed as argument. The filter() iterator applies its first argument as a function to every element of its second argument as an iterable, and returns only those elements for whose the result of the function is False. Thus, with this enhancement we're avoiding checking a state that has already been checked. in a previous call.

Next, instead of passing the whole fringe to the generator we're passing it a filter() iterator again. Remember that the different generators we have just expand the state list they receive in one or other way depending on the algorithm they correspond to. Thus, by filtering out certain states we prevent those states from being expanded. One might be tempted to optimize the code above and create a single filter() iterator that is re-used in both loops; don't do it. Once the filter() iterator is burned in the first loop it shall not return any more states and the second loop shall be skipped.

Now how do we decide which states shall be filtered out in the two loops? Simple: once a list of states has been returned by the generator, we add any parent states of the former to the list of visited states. Remember the lists of states returned by our generators are actually dictionaries whose entries point to their respective parent states (except when the list contains only the initial state, in which case it is exactly that: a list; hence the try: except clause wrapping the "sol.insert( desc[sol[0]] )" sentence).

Why the try: except clause when updating the list of visited states? The purpose is two-fold: for correctness, just in case the fringe contains only the initial state in which case the fringe is a raw list and calling values() on it would fail; and for efficiency, avoiding an if sentence checking for visited set to None in every step of the for loop. This idiom gives the microprocessor a tip that visited set to None is an exceptional case, thus allowing its code and data HW pre-fetchers to perform more eficiently when running that piece of code. Here I'm assuming users of treeSearch() shall set chk_dups argument to True in 90% of the cases, since otherwise each step of the loop raises an exception ruining our optimization attempt.

The enhanced function can be tried adding some code to our testSearch() unit test; instead of the simple f(n) = (2*n, 2*n+1) successor function we've been using up to here, we shall use a modified successor function that one in two times returns a parent state in addition to the child states:

f(n) = (random() < 0.5 and (2*x, 2*x + 1)) or (max(1, int(x/2)), 2*x, 2*x + 1)

Observe the above successor function returns changing graphs between searches, or even within the same search!. However that poses no issue for our enhanced search algorithm, as you'll see if you run the test method with varying goal values.

    goal = 10
    successor = \

        lambda x: (random() < 0.5 and (2*x, 2*x + 1)) \
        or (max(1, int(x/2)), 2*x, 2*x + 1)
    problem = Problem(initial, goal, successor, cost)
    problem.treeSearch(Problem.breadthFirstGenerator(successor), chk_dups=True)
    self.assertTrue(problem.solved())
    self.assertEqual(
    problem.solution, [1, 2, 5, 10],
    "Solution found is " + str(problem.solution))
Snippet 11 - Continuation of testSearch() unit test trying our enhanced search method

Obviously, for the just described approach to be valid states must be possible to tell apart of each other; even if it may not have been fully apparent to you, the tree search algorithms did not require this property!.

Conclusion

I hope you've enjoyed this little journey into uninformed search algorithms and Python. My next challenge would be designing a generator for the common algorithm that performs bidirectional search.

In the next post we'll see how to use this algorithm framework to solve some classic computing problems. Enjoy yourselves till then!



Go middlewares for object-oriented programmers Go language (Golang, http://golang.org ) is a very simple procedural programming language of ...