Search code examples
pythonsearchgraphnodesa-star

Revisiting visited nodes in A* Search


I'm trying to apply A* search on orienteering to get the optimal route to take. The inputs are two files - one image file which explains the terrains and one text file to define elevations. I calculate the terrain difficulty based on pre-set values to define how fast the terrain can be traversed. I've also defined the elevation difficulty based on slope (downward slope gives faster speeds and vice versa).

The terrain and elevation data is stored in matrices (list of lists). The inputs are therefore indices which is the same as points on the map. Two inputs are provided - eg:

start = [230,327]
end = [241,347]

The problem is that my code keeps re-visiting the nodes that are already present in the visited Queue. The node is defined as follows:

class Node:
    def __init__(self,value,parent,start=[],goal=[]):
        self.children = []
        self.parent = parent
        self.value = value
        self.timeToGoal = 0.0000
        self.timeTravelled = 0.0000

        if parent:
            timeToParent = self.parent.timeTravelled
            [parentX, parentY] = parent.value
            [currentX, currentY] = self.value
            xDiff = abs(currentX - parentX)
            yDiff = abs(currentX - parentX)
            distance = 12.7627
            if xDiff == 0 and yDiff != 0:
                distance = 10.29
            elif xDiff != 0 and yDiff == 0:
                distance = 7.55
            # distanceFromParent = math.sqrt(((currentX - parentX) ** 2) - (currentY - parentY) ** 2)
            speedFromParent = 1.388 * calculateTerrainDifficulty( terrainMap[currentX][currentY]) * calculateElevationDifficulty(elevationMap[parentX][parentY], elevationMap[currentX][currentY], distance)
            timeTravelledFromParent = 0
            if speedFromParent != 0:
                timeTravelledFromParent = distance / speedFromParent
            else:
                "Error: Speed from Parent Cannot Be Zero"
            self.timeTravelled = timeToParent + timeTravelledFromParent
            self.path = parent.path[:]
            self.path.append(value)
            self.start = parent.start
            self.goal = parent.goal

        else:
            self.path = [value]
            self.start = start
            self.goal = goal

    def GetTime(self):
        pass

    def CreateChildren(self):
        pass

I also used a SubNode class for defining the functions, with time being defined as the time to self + pythagorean hypotenuse distance to goal:

class SubNode(Node):
    def __init__(self, value, parent, start=[], goal=[]):
        super(SubNode, self).__init__(value, parent, start, goal)
        self.timeToGoal = self.GetTime()

    def GetTime(self):
        if self.value == self.goal:
            return 0
        [currentX, currentY] = self.value
        [targetX, targetY] = self.goal
        parentTime = 0
        if self.parent:
            parentTime = self.timeTravelled
        heuristicTime = 99999.99
        # Pythagorean Hypotenuse - Straight-line Distance
        distance = math.sqrt(((int(currentX) - int(targetX)) ** 2) + (int(currentY)- int(targetY)) ** 2)
        speed = 1.38 * calculateTerrainDifficulty(terrainMap[currentX][currentY])
        if speed != 0:
            heuristicTime = distance / speed
        heuristicTime=heuristicTime+parentTime
        return heuristicTime


    def CreateChildren(self):
        if not self.children:
            dirs = [-1, 0, 1]
            [xVal, yVal] = self.value
            for xDir in dirs:
                newXVal = xVal + xDir
                if newXVal < 0 or newXVal > 394: continue
                for yDir in dirs:
                    newYVal = yVal + yDir
                    if ((xVal == newXVal) and (yVal == newYVal)) or (newYVal < 0 or newYVal > 499) or (
                        calculateTerrainDifficulty(terrainMap[newXVal][newYVal]) == 0):
                        continue
                    child = SubNode([newXVal, newYVal], self)
                    self.children.append(child)

The A* search class was defined as follows. You can see that I have put the condition in there to make sure nodes are not revisited, and when I put a print in there I can see that the condition is met multiple times.

class AStarSearch:
    def __init__(self, start, goal):
        self.path = []
        self.visitedQueue = []
        self.priorityQueue = PriorityQueue()
        self.start = start
        self.goal = goal

    def Search(self):
        startNode = SubNode(self.start, 0, self.start, self.goal)
        count = 0
        self.priorityQueue.put((0, count, startNode))
        while (not self.path and self.priorityQueue.qsize()):
            closestChild = self.priorityQueue.get()[2]e
            closestChild.CreateChildren()
            self.visitedQueue.append(closestChild.value)
            for child in closestChild.children:
                if child.value not in self.visitedQueue:
                    count += 1
                    if not child.timeToGoal:
                        self.path = child.path
                        break
                    self.priorityQueue.put((child.timeToGoal, child.value, child))
        if not self.path:
            print("Not possible to reach goal")
        return self.path

Due to some reason, my program keeps re-visiting some nodes (as I can see from the output when I print the visited queue. How can i avoid this?

[[230, 327], [231, 326], [229, 326], [231, 325], [231, 328], [229, 328], [231, 327], [229, 327], [231, 327], [229, 327], [229, 325], [231, 324], [230, 323], [231, 329], [229, 329], [231, 327], [229, 327], [229, 324], [231, 330], [231, 323], [229, 330], [229, 331]]

Another problem I'm facing is this one:

TypeError: unorderable types: SubNode() < SubNode()

Is there a way to overcome this one also without changing the use of python's Priority Queue?


Solution

  • You need to add a test on closestChild instead of its children:

    closestChild = self.priorityQueue.get()[2]e
    if closesChild.value not in self.visitedQueue:
        closestChild.CreateChildren()
        self.visitedQueue.append(closestChild.value)
    

    Otherwise, you can say you visit n1 and then n2, both linking to node n3. n3 is added twice in the priorityqueue, so it is popped twice, then added twice to visitedQueue.

    The condition if child.value not in self.visitedQueue: is then useful to speed up things (by keeping a smaller priority queue), but not necessary (since unnecessary objects in priorityQueue will be thrown away when unpiling them).

    About the error you're getting: PriorityQueue does not support custom ordering, which is what you need for your priority queue, so you'll have to make a custom one. There is an example here. Obviously your _get_priority function would need to return timeTravelled instead of item[1]

    EDIT 3: We (tobias_k and I) first said you need to implement the __eq__ function for SubNode so that python knows when two of them are equal, but it is actually not the case since you're storing only the values in self.visitedQueue.