Search code examples
pythonalgorithmpathnoise

Noisy path generated by A* algorithm


I'm working on a front-end for a robotic project (an 'autonomous' car that localizes itself using some sensors and a map - generated from an SVG file).

For the robot to be controllable, we must generate paths between its current position and its goal. I used the easiest algorithm for that : A*.

I got some weird results doing that : The car tends to go on multiples of 45° degree, and one especially annoying problem : some generated paths are quite noisy !

See the noisy path near the orange rectangle in this case :

Vizualisation of the generated path

Is there anyway to avoid those weird/noisy results ? Eventually we'd want to build a path with the minimum number of heading angle changes. (the car can turn without moving, so we don't need any path 'smoothing').

Here's my A* implementation :

def search(self, begin, goal):
    if goal.x not in range(self.width) or goal.y not in range(self.height):
        print "Goal is out of bound"
        return []
    elif not self.grid[begin.y][begin.x].reachable:
        print "Beginning is unreachable"
        return []
    elif not self.grid[goal.y][goal.x].reachable:
        print "Goal is unreachable"
        return []
    else:

        self.cl = set()
        self.ol = set()

        curCell = begin
        self.ol.add(curCell)

        while len(self.ol) > 0:

            # We choose the cell in the open list having the minimum score as our current cell
            curCell = min(self.ol, key = lambda x : x.f)

            # We add the current cell to the closed list
            self.ol.remove(curCell)
            self.cl.add(curCell)

            # We check the cell's (reachable) neighbours :
            neighbours = self.neighbours(curCell)

            for cell in neighbours:
                # If the goal is a neighbour cell :
                if cell == goal:
                    cell.parent = curCell
                    self.path = cell.path()
                    self.display()
                    self.clear()
                    return self.path
                elif cell not in self.cl:
                    # We process the cells that are not in the closed list
                    # (processing <-> calculating the "F" score)
                    cell.process(curCell, goal)

                    self.ol.add(cell)

EDIT 1: By popuplar demand, here's the score calculation function (process) :

def process(self, parent, goal):
    self.parent = parent
    self.g = parent.distance(self)
    self.h = self.manhattanDistance(goal)
    self.f = self.g + self.h

EDIT Here's the neighbours method (updated following user1884905's answer) :

def neighbours(self, cell, radius = 1, unreachables = False, diagonal = True):
    neighbours = set()
    for i in xrange(-radius, radius + 1):
        for j in xrange(-radius, radius + 1):
            x = cell.x + j
            y = cell.y + i
            if 0 <= y < self.height and 0 <= x < self.width and ( self.grid[y][x].reachable or unreachables ) and (diagonal or (x == cell.x or y == cell.y)) :
                neighbours.add(self.grid[y][x])

    return neighbours

(this looks complicated but it just gives the 8 neighbours - including diagonal neighbours - of a cell ; it can also take a radius different from 1 because it's used for other features)

And distance calculations (depending on the use of diagonals neighbours or not : )

def manhattanDistance(self, cell):
    return abs(self.x - cell.x) + abs(self.y - cell.y)

def diagonalDistance(self, cell):

    xDist = abs(self.x - cell.x)
    yDist = abs(self.y - cell.y)

    if xDist > yDist:
        return 1.4 * yDist + (xDist - yDist)
    else:
        return 1.4 * xDist + (yDist - xDist)

Solution

  • Without being able to see how you have implemented your neighbour and distance functions, I still have a good guess about what is going wrong:

    You should not use manhattan distance if you allow for diagonal traversal.

    The manhattan distance in the goal-function should be a measure of the shortest distance to the goal. (Which it isn't, if you can drive diagonally through building-blocks.)

    The easiest way to fix this would be to keep the manhattan distance as a goal-function and change the definition of neighbours to only include the four left-right-up-down adjacent cells.

    Edit

    There are still problems with your code. The following pseudo code is taken from wikipedia. I have marked important lines that you will have to check. You must ensure that i) you are updating the nodes in the open set if you find a better solution and ii) you always take into account the previously traveled distance.

    function A*(start,goal)
         closedset := the empty set    // The set of nodes already evaluated.
         openset := {start}    // The set of tentative nodes to be evaluated, initially containing the start node
         came_from := the empty map    // The map of navigated nodes.
    
         g_score[start] := 0    // Cost from start along best known path.
         // Estimated total cost from start to goal through y.
         f_score[start] := g_score[start] + heuristic_cost_estimate(start, goal)
    
         while openset is not empty
             current := the node in openset having the lowest f_score[] value
             if current = goal
                 return reconstruct_path(came_from, goal)
    
             remove current from openset
             add current to closedset
             for each neighbor in neighbor_nodes(current)
                 // -------------------------------------------------------------------
                 // This is the way the tentative_g_score should be calculated.
                 // Do you include the current g_score in your calculation parent.distance(self) ?
                 tentative_g_score := g_score[current] + dist_between(current,neighbor)
                 // ------------------------------------------------------------------- 
                 if neighbor in closedset
                     if tentative_g_score >= g_score[neighbor]
                         continue
    
                 // -------------------------------------------------------------------
                 // You never make this comparrison
                 if neighbor not in openset or tentative_g_score < g_score[neighbor]
                 // -------------------------------------------------------------------
                     came_from[neighbor] := current
                     g_score[neighbor] := tentative_g_score
                     f_score[neighbor] := g_score[neighbor] + heuristic_cost_estimate(neighbor, goal)
                     if neighbor not in openset
                         add neighbor to openset
    
         return failure
    
     function reconstruct_path(came_from, current_node)
         if current_node in came_from
             p := reconstruct_path(came_from, came_from[current_node])
             return (p + current_node)
         else
             return current_node