Latest commit

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Build StatusCoverage Status

What is THIS?

A Pythonic implementation of the famous A* algorithm.

Why ANOTHER implementation

Because coding is awesome! Also because I really dislike the mess that the usual implementations create. The A* algorithm is simple and beautiful and so must be its implementation.

How is THIS diferent?

This is different because I refactored all the logic in propositional layers. This is achieved grouping together the parts of the algorithm that has the same level of abstraction, exposing the pure logic of A* in its higher layer.

How can it be TESTED?

It comes with a maze_solving_example.py snippet to test the algorithm in a simple weighted maze ( with random weights ). If you run it you will obtain something like this:

$ python examples/maze_solving_example.py
↓ ↓ ← ← ← ← ← ← ← .
↓ ↓ ← ← ← ↑ ↑ ← ← .
↓ ↓ ← ← ← . . ↑ ← .
↓ ↓ ← ##########. . . ↑ .
→ S ← ##########. . . ↑ .
↑ ↑ ← ##########. . . ↑ .
↑ ↑ ← ##########. . → ↑ .
↑ ↑ ← ##########. . ↑ . .
↑ ↑ ← ##########. . E . .
↑ ↑ ← ##########. . . . .
5 4 5 6 7 8 9 10 11 .
4 3 4 5 10 10 10 11 12 .
3 2 3 4 11 . . 12 13 .
2 1 2 ##########. . . 14 .
1 S 1 ##########. . . 15 .
2 1 2 ##########. . . 16 .
3 2 3 ##########. . 18 17 .
4 3 4 ##########. . 19 . .
5 4 5 ##########. . E . .
6 5 6 ##########. . . . .
[...]

The first diagram represents where each point came from. Starting in the E ( standing for ENDING POINT) we backtrack each arrow untill we reach the S ( standing for STARTING POINT). This path is the shortest path. And you know what?

This is also true for every point in the diagram!!!

The arcane forces of A* make that if you start in a visited point and backtrack using the arrows you will get the shortest path.

The second diagram is the cost to reach each point from the START (S).

A third diagram, not shown here, will also be printed. This diagram shows the estimated total cost from START (S) to END (E) at each point, which is central to the efficiency of the A* algoritm as explained below.

Can you EXPLAIN the algorithm?

Yeah! The idea of the A* algorithm is that starting from the start point we visit the points that are cheaper to visit. The cost of visiting a neighbor point depends on how costly is to go from the current point to a neighbor. So we check for all the points what is the neighbor that is cheaper to visit and we visit it.

The A* algorithm is basically the following:

defa_star_search(graph, start, end):
""" Calculates the shortest path from start to end. :param graph: A graph object. The graph object can be anything that implements the following methods: graph.neighbors( (x:int, y:int) ) : Iterable( (x:int,y:int), (x:int,y:int), ...) graph.cost( (x:int,y:int) ) : int :param start: Tuple of two ints representing the starting point. :param end: Tuple of two ints representing the ending point. :returns: A DijkstraHeap object. """frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )
whilefrontier:
current_node=frontier.pop()
ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontierforneighboringraph.neighbors( current_node.point ):
cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

Lets go line by line:

frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )

This line creates a DijkstraHeap object and puts the starting point in it. We will see later how this can be implemented but the best part is that....This is not part of the algorithm! What is a DijkstraHeap then? This is a cost queue that has the following properties:

  • If we try to insert an already visited element in the queue the DijkstraHeap will do nothing.
  • The DijkstraHeap always pop the element that has the lowest cost and NEVER pops an already visited element.

Cool! So this DijkstraHeap knows the visiting order of the elements. Its like a heap but never pops an already visited element.

By the way, a Node object is a tuple of the form ( total_cost_estimate, point, point_from_we_came ).

whileTrue:

We loop until we have found a path, or failed to find one by exhausting all elements in the queue.

current_node=frontier.pop()

Each iteration we pop an element from the DijkstraHeap. This element always has the lowest cost element because the DijkstraHeap has this property ( because is a heap and heaps are awesome ).

At this point maybe you are asking yourself why the name frontier? Well, this is because when you are at the starting point and you visit neighbors, the queue of the nodes to be visited is like a expanding frontier (imagine a closed curve that becomes bigger and bigger in size). From which sides this frontier will expand first depends on the weights of the nodes among other things (like the distance to the ending point...etc).

ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontier

If we have reached the end, we stop and return the DijkstraHeap that has all the information about our path (because it knows how we reach each element).

forneighboringraph.neighbors( current_node.point ):

We get each of the current point neighbors

cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

For each neighbor we calculate the new cost of reaching this neighbor from the current point. This cost is formed by three quantities:

  1. The cost of reaching the current point, which is the stored cost estimate minus the heuristic distance at that point (explained below).
  2. The cost of going from the current point to the neighbor.
  3. The distance of the neighbor to the end point that we are looking.

Why this 3rd cost? Because we want to explore first the points that are near the end destination and expend less time in the points that are far from it. So if we artificially give the point a higher cost if the point is far from the destination it will be visited later.

The new cost is thus an estimate of the total cost, without knowing what lies ahead. It grows along the path as we encounter obstacles or higher-cost steps. It is essential that the heuristic never overestimates the remaining distance, otherwise the path is not necessarily optimal since the best path may not be visited before we find the end (and terminate).

When we have calculated this new cost estimate we insert the point in the cost queue.

But what about the MISTERIOUS DijkstraHeap?

Is like I said a heap that remembers the visited elements and where they came from and never pops an already visited element. The implementation is very simple:

classDijkstraHeap(list):
""" An augmented heap for the A* algorithm. This class encapsulated the residual logic of the A* algorithm like for example how to manage elements already visited that remain in the heap, elements already visited that are not in the heap and from where we came to a visited element. This class will have three main elements: - A heap that will act as a cost queue (self). - A visited dict that will act as a visited set and as a mapping of the form point:came_from - A costs dict that will act as a mapping of the form point:cost_so_far """def__init__(self, first_node=None):
self.visited=dict()
self.costs=dict()
iffirst_nodeisnotNone:
self.insert(first_node)
definsert(self, element):
""" Insert an element into the Dijkstra Heap. :param element: A Node object. :return: None """ifelement.pointnotinself.visited:
heapq.heappush(self,element)
defpop(self):
""" Pop an element from the Dijkstra Heap, adding it to the visited and cost dicts. :return: A Node object """whileselfandself[0].pointinself.visited:
heapq.heappop(self)
ifself:
next_elem=heapq.heappop(self)
self.visited[next_elem.point] =next_elem.came_fromself.costs[next_elem.point] =next_elem.costreturnnext_elem

So WHAT is this deep logic you talk about?

I think the deep logic about A* can be summarized in the following two simple points:

  • We visit the nodes in order, being this order the cost of going from the starting point to this particular node.

  • We artificially alter the cost of visiting one node taking into account how far this particular node is from the destination, making the furthest nodes more costly.

And all the stuff about the cost queue, the heap, not visiting a node already visited, what we do with nodes in the queue that have been visited.....that is important stuff but is NOT the A* algorithm: it is secondary logic and secondary problems that lead to secondary data structures.

About

A pythonic implementation of the A* algorithm.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Build StatusCoverage Status

What is THIS?

A Pythonic implementation of the famous A* algorithm.

Why ANOTHER implementation

Because coding is awesome! Also because I really dislike the mess that the usual implementations create. The A* algorithm is simple and beautiful and so must be its implementation.

How is THIS diferent?

This is different because I refactored all the logic in propositional layers. This is achieved grouping together the parts of the algorithm that has the same level of abstraction, exposing the pure logic of A* in its higher layer.

How can it be TESTED?

It comes with a maze_solving_example.py snippet to test the algorithm in a simple weighted maze ( with random weights ). If you run it you will obtain something like this:

$ python examples/maze_solving_example.py
↓ ↓ ← ← ← ← ← ← ← .
↓ ↓ ← ← ← ↑ ↑ ← ← .
↓ ↓ ← ← ← . . ↑ ← .
↓ ↓ ← ##########. . . ↑ .
→ S ← ##########. . . ↑ .
↑ ↑ ← ##########. . . ↑ .
↑ ↑ ← ##########. . → ↑ .
↑ ↑ ← ##########. . ↑ . .
↑ ↑ ← ##########. . E . .
↑ ↑ ← ##########. . . . .
5 4 5 6 7 8 9 10 11 .
4 3 4 5 10 10 10 11 12 .
3 2 3 4 11 . . 12 13 .
2 1 2 ##########. . . 14 .
1 S 1 ##########. . . 15 .
2 1 2 ##########. . . 16 .
3 2 3 ##########. . 18 17 .
4 3 4 ##########. . 19 . .
5 4 5 ##########. . E . .
6 5 6 ##########. . . . .
[...]

The first diagram represents where each point came from. Starting in the E ( standing for ENDING POINT) we backtrack each arrow untill we reach the S ( standing for STARTING POINT). This path is the shortest path. And you know what?

This is also true for every point in the diagram!!!

The arcane forces of A* make that if you start in a visited point and backtrack using the arrows you will get the shortest path.

The second diagram is the cost to reach each point from the START (S).

A third diagram, not shown here, will also be printed. This diagram shows the estimated total cost from START (S) to END (E) at each point, which is central to the efficiency of the A* algoritm as explained below.

Can you EXPLAIN the algorithm?

Yeah! The idea of the A* algorithm is that starting from the start point we visit the points that are cheaper to visit. The cost of visiting a neighbor point depends on how costly is to go from the current point to a neighbor. So we check for all the points what is the neighbor that is cheaper to visit and we visit it.

The A* algorithm is basically the following:

defa_star_search(graph, start, end):
""" Calculates the shortest path from start to end. :param graph: A graph object. The graph object can be anything that implements the following methods: graph.neighbors( (x:int, y:int) ) : Iterable( (x:int,y:int), (x:int,y:int), ...) graph.cost( (x:int,y:int) ) : int :param start: Tuple of two ints representing the starting point. :param end: Tuple of two ints representing the ending point. :returns: A DijkstraHeap object. """frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )
whilefrontier:
current_node=frontier.pop()
ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontierforneighboringraph.neighbors( current_node.point ):
cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

Lets go line by line:

frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )

This line creates a DijkstraHeap object and puts the starting point in it. We will see later how this can be implemented but the best part is that....This is not part of the algorithm! What is a DijkstraHeap then? This is a cost queue that has the following properties:

  • If we try to insert an already visited element in the queue the DijkstraHeap will do nothing.
  • The DijkstraHeap always pop the element that has the lowest cost and NEVER pops an already visited element.

Cool! So this DijkstraHeap knows the visiting order of the elements. Its like a heap but never pops an already visited element.

By the way, a Node object is a tuple of the form ( total_cost_estimate, point, point_from_we_came ).

whileTrue:

We loop until we have found a path, or failed to find one by exhausting all elements in the queue.

current_node=frontier.pop()

Each iteration we pop an element from the DijkstraHeap. This element always has the lowest cost element because the DijkstraHeap has this property ( because is a heap and heaps are awesome ).

At this point maybe you are asking yourself why the name frontier? Well, this is because when you are at the starting point and you visit neighbors, the queue of the nodes to be visited is like a expanding frontier (imagine a closed curve that becomes bigger and bigger in size). From which sides this frontier will expand first depends on the weights of the nodes among other things (like the distance to the ending point...etc).

ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontier

If we have reached the end, we stop and return the DijkstraHeap that has all the information about our path (because it knows how we reach each element).

forneighboringraph.neighbors( current_node.point ):

We get each of the current point neighbors

cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

For each neighbor we calculate the new cost of reaching this neighbor from the current point. This cost is formed by three quantities:

  1. The cost of reaching the current point, which is the stored cost estimate minus the heuristic distance at that point (explained below).
  2. The cost of going from the current point to the neighbor.
  3. The distance of the neighbor to the end point that we are looking.

Why this 3rd cost? Because we want to explore first the points that are near the end destination and expend less time in the points that are far from it. So if we artificially give the point a higher cost if the point is far from the destination it will be visited later.

The new cost is thus an estimate of the total cost, without knowing what lies ahead. It grows along the path as we encounter obstacles or higher-cost steps. It is essential that the heuristic never overestimates the remaining distance, otherwise the path is not necessarily optimal since the best path may not be visited before we find the end (and terminate).

When we have calculated this new cost estimate we insert the point in the cost queue.

But what about the MISTERIOUS DijkstraHeap?

Is like I said a heap that remembers the visited elements and where they came from and never pops an already visited element. The implementation is very simple:

classDijkstraHeap(list):
""" An augmented heap for the A* algorithm. This class encapsulated the residual logic of the A* algorithm like for example how to manage elements already visited that remain in the heap, elements already visited that are not in the heap and from where we came to a visited element. This class will have three main elements: - A heap that will act as a cost queue (self). - A visited dict that will act as a visited set and as a mapping of the form point:came_from - A costs dict that will act as a mapping of the form point:cost_so_far """def__init__(self, first_node=None):
self.visited=dict()
self.costs=dict()
iffirst_nodeisnotNone:
self.insert(first_node)
definsert(self, element):
""" Insert an element into the Dijkstra Heap. :param element: A Node object. :return: None """ifelement.pointnotinself.visited:
heapq.heappush(self,element)
defpop(self):
""" Pop an element from the Dijkstra Heap, adding it to the visited and cost dicts. :return: A Node object """whileselfandself[0].pointinself.visited:
heapq.heappop(self)
ifself:
next_elem=heapq.heappop(self)
self.visited[next_elem.point] =next_elem.came_fromself.costs[next_elem.point] =next_elem.costreturnnext_elem

So WHAT is this deep logic you talk about?

I think the deep logic about A* can be summarized in the following two simple points:

  • We visit the nodes in order, being this order the cost of going from the starting point to this particular node.

  • We artificially alter the cost of visiting one node taking into account how far this particular node is from the destination, making the furthest nodes more costly.

And all the stuff about the cost queue, the heap, not visiting a node already visited, what we do with nodes in the queue that have been visited.....that is important stuff but is NOT the A* algorithm: it is secondary logic and secondary problems that lead to secondary data structures.

About

A pythonic implementation of the A* algorithm.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Build StatusCoverage Status

What is THIS?

A Pythonic implementation of the famous A* algorithm.

Why ANOTHER implementation

Because coding is awesome! Also because I really dislike the mess that the usual implementations create. The A* algorithm is simple and beautiful and so must be its implementation.

How is THIS diferent?

This is different because I refactored all the logic in propositional layers. This is achieved grouping together the parts of the algorithm that has the same level of abstraction, exposing the pure logic of A* in its higher layer.

How can it be TESTED?

It comes with a maze_solving_example.py snippet to test the algorithm in a simple weighted maze ( with random weights ). If you run it you will obtain something like this:

$ python examples/maze_solving_example.py
↓ ↓ ← ← ← ← ← ← ← .
↓ ↓ ← ← ← ↑ ↑ ← ← .
↓ ↓ ← ← ← . . ↑ ← .
↓ ↓ ← ##########. . . ↑ .
→ S ← ##########. . . ↑ .
↑ ↑ ← ##########. . . ↑ .
↑ ↑ ← ##########. . → ↑ .
↑ ↑ ← ##########. . ↑ . .
↑ ↑ ← ##########. . E . .
↑ ↑ ← ##########. . . . .
5 4 5 6 7 8 9 10 11 .
4 3 4 5 10 10 10 11 12 .
3 2 3 4 11 . . 12 13 .
2 1 2 ##########. . . 14 .
1 S 1 ##########. . . 15 .
2 1 2 ##########. . . 16 .
3 2 3 ##########. . 18 17 .
4 3 4 ##########. . 19 . .
5 4 5 ##########. . E . .
6 5 6 ##########. . . . .
[...]

The first diagram represents where each point came from. Starting in the E ( standing for ENDING POINT) we backtrack each arrow untill we reach the S ( standing for STARTING POINT). This path is the shortest path. And you know what?

This is also true for every point in the diagram!!!

The arcane forces of A* make that if you start in a visited point and backtrack using the arrows you will get the shortest path.

The second diagram is the cost to reach each point from the START (S).

A third diagram, not shown here, will also be printed. This diagram shows the estimated total cost from START (S) to END (E) at each point, which is central to the efficiency of the A* algoritm as explained below.

Can you EXPLAIN the algorithm?

Yeah! The idea of the A* algorithm is that starting from the start point we visit the points that are cheaper to visit. The cost of visiting a neighbor point depends on how costly is to go from the current point to a neighbor. So we check for all the points what is the neighbor that is cheaper to visit and we visit it.

The A* algorithm is basically the following:

defa_star_search(graph, start, end):
""" Calculates the shortest path from start to end. :param graph: A graph object. The graph object can be anything that implements the following methods: graph.neighbors( (x:int, y:int) ) : Iterable( (x:int,y:int), (x:int,y:int), ...) graph.cost( (x:int,y:int) ) : int :param start: Tuple of two ints representing the starting point. :param end: Tuple of two ints representing the ending point. :returns: A DijkstraHeap object. """frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )
whilefrontier:
current_node=frontier.pop()
ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontierforneighboringraph.neighbors( current_node.point ):
cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

Lets go line by line:

frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )

This line creates a DijkstraHeap object and puts the starting point in it. We will see later how this can be implemented but the best part is that....This is not part of the algorithm! What is a DijkstraHeap then? This is a cost queue that has the following properties:

  • If we try to insert an already visited element in the queue the DijkstraHeap will do nothing.
  • The DijkstraHeap always pop the element that has the lowest cost and NEVER pops an already visited element.

Cool! So this DijkstraHeap knows the visiting order of the elements. Its like a heap but never pops an already visited element.

By the way, a Node object is a tuple of the form ( total_cost_estimate, point, point_from_we_came ).

whileTrue:

We loop until we have found a path, or failed to find one by exhausting all elements in the queue.

current_node=frontier.pop()

Each iteration we pop an element from the DijkstraHeap. This element always has the lowest cost element because the DijkstraHeap has this property ( because is a heap and heaps are awesome ).

At this point maybe you are asking yourself why the name frontier? Well, this is because when you are at the starting point and you visit neighbors, the queue of the nodes to be visited is like a expanding frontier (imagine a closed curve that becomes bigger and bigger in size). From which sides this frontier will expand first depends on the weights of the nodes among other things (like the distance to the ending point...etc).

ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontier

If we have reached the end, we stop and return the DijkstraHeap that has all the information about our path (because it knows how we reach each element).

forneighboringraph.neighbors( current_node.point ):

We get each of the current point neighbors

cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

For each neighbor we calculate the new cost of reaching this neighbor from the current point. This cost is formed by three quantities:

  1. The cost of reaching the current point, which is the stored cost estimate minus the heuristic distance at that point (explained below).
  2. The cost of going from the current point to the neighbor.
  3. The distance of the neighbor to the end point that we are looking.

Why this 3rd cost? Because we want to explore first the points that are near the end destination and expend less time in the points that are far from it. So if we artificially give the point a higher cost if the point is far from the destination it will be visited later.

The new cost is thus an estimate of the total cost, without knowing what lies ahead. It grows along the path as we encounter obstacles or higher-cost steps. It is essential that the heuristic never overestimates the remaining distance, otherwise the path is not necessarily optimal since the best path may not be visited before we find the end (and terminate).

When we have calculated this new cost estimate we insert the point in the cost queue.

But what about the MISTERIOUS DijkstraHeap?

Is like I said a heap that remembers the visited elements and where they came from and never pops an already visited element. The implementation is very simple:

classDijkstraHeap(list):
""" An augmented heap for the A* algorithm. This class encapsulated the residual logic of the A* algorithm like for example how to manage elements already visited that remain in the heap, elements already visited that are not in the heap and from where we came to a visited element. This class will have three main elements: - A heap that will act as a cost queue (self). - A visited dict that will act as a visited set and as a mapping of the form point:came_from - A costs dict that will act as a mapping of the form point:cost_so_far """def__init__(self, first_node=None):
self.visited=dict()
self.costs=dict()
iffirst_nodeisnotNone:
self.insert(first_node)
definsert(self, element):
""" Insert an element into the Dijkstra Heap. :param element: A Node object. :return: None """ifelement.pointnotinself.visited:
heapq.heappush(self,element)
defpop(self):
""" Pop an element from the Dijkstra Heap, adding it to the visited and cost dicts. :return: A Node object """whileselfandself[0].pointinself.visited:
heapq.heappop(self)
ifself:
next_elem=heapq.heappop(self)
self.visited[next_elem.point] =next_elem.came_fromself.costs[next_elem.point] =next_elem.costreturnnext_elem

So WHAT is this deep logic you talk about?

I think the deep logic about A* can be summarized in the following two simple points:

  • We visit the nodes in order, being this order the cost of going from the starting point to this particular node.

  • We artificially alter the cost of visiting one node taking into account how far this particular node is from the destination, making the furthest nodes more costly.

And all the stuff about the cost queue, the heap, not visiting a node already visited, what we do with nodes in the queue that have been visited.....that is important stuff but is NOT the A* algorithm: it is secondary logic and secondary problems that lead to secondary data structures.

About

A pythonic implementation of the A* algorithm.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Build StatusCoverage Status

What is THIS?

A Pythonic implementation of the famous A* algorithm.

Why ANOTHER implementation

Because coding is awesome! Also because I really dislike the mess that the usual implementations create. The A* algorithm is simple and beautiful and so must be its implementation.

How is THIS diferent?

This is different because I refactored all the logic in propositional layers. This is achieved grouping together the parts of the algorithm that has the same level of abstraction, exposing the pure logic of A* in its higher layer.

How can it be TESTED?

It comes with a maze_solving_example.py snippet to test the algorithm in a simple weighted maze ( with random weights ). If you run it you will obtain something like this:

$ python examples/maze_solving_example.py
↓ ↓ ← ← ← ← ← ← ← .
↓ ↓ ← ← ← ↑ ↑ ← ← .
↓ ↓ ← ← ← . . ↑ ← .
↓ ↓ ← ##########. . . ↑ .
→ S ← ##########. . . ↑ .
↑ ↑ ← ##########. . . ↑ .
↑ ↑ ← ##########. . → ↑ .
↑ ↑ ← ##########. . ↑ . .
↑ ↑ ← ##########. . E . .
↑ ↑ ← ##########. . . . .
5 4 5 6 7 8 9 10 11 .
4 3 4 5 10 10 10 11 12 .
3 2 3 4 11 . . 12 13 .
2 1 2 ##########. . . 14 .
1 S 1 ##########. . . 15 .
2 1 2 ##########. . . 16 .
3 2 3 ##########. . 18 17 .
4 3 4 ##########. . 19 . .
5 4 5 ##########. . E . .
6 5 6 ##########. . . . .
[...]

The first diagram represents where each point came from. Starting in the E ( standing for ENDING POINT) we backtrack each arrow untill we reach the S ( standing for STARTING POINT). This path is the shortest path. And you know what?

This is also true for every point in the diagram!!!

The arcane forces of A* make that if you start in a visited point and backtrack using the arrows you will get the shortest path.

The second diagram is the cost to reach each point from the START (S).

A third diagram, not shown here, will also be printed. This diagram shows the estimated total cost from START (S) to END (E) at each point, which is central to the efficiency of the A* algoritm as explained below.

Can you EXPLAIN the algorithm?

Yeah! The idea of the A* algorithm is that starting from the start point we visit the points that are cheaper to visit. The cost of visiting a neighbor point depends on how costly is to go from the current point to a neighbor. So we check for all the points what is the neighbor that is cheaper to visit and we visit it.

The A* algorithm is basically the following:

defa_star_search(graph, start, end):
""" Calculates the shortest path from start to end. :param graph: A graph object. The graph object can be anything that implements the following methods: graph.neighbors( (x:int, y:int) ) : Iterable( (x:int,y:int), (x:int,y:int), ...) graph.cost( (x:int,y:int) ) : int :param start: Tuple of two ints representing the starting point. :param end: Tuple of two ints representing the ending point. :returns: A DijkstraHeap object. """frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )
whilefrontier:
current_node=frontier.pop()
ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontierforneighboringraph.neighbors( current_node.point ):
cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

Lets go line by line:

frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )

This line creates a DijkstraHeap object and puts the starting point in it. We will see later how this can be implemented but the best part is that....This is not part of the algorithm! What is a DijkstraHeap then? This is a cost queue that has the following properties:

  • If we try to insert an already visited element in the queue the DijkstraHeap will do nothing.
  • The DijkstraHeap always pop the element that has the lowest cost and NEVER pops an already visited element.

Cool! So this DijkstraHeap knows the visiting order of the elements. Its like a heap but never pops an already visited element.

By the way, a Node object is a tuple of the form ( total_cost_estimate, point, point_from_we_came ).

whileTrue:

We loop until we have found a path, or failed to find one by exhausting all elements in the queue.

current_node=frontier.pop()

Each iteration we pop an element from the DijkstraHeap. This element always has the lowest cost element because the DijkstraHeap has this property ( because is a heap and heaps are awesome ).

At this point maybe you are asking yourself why the name frontier? Well, this is because when you are at the starting point and you visit neighbors, the queue of the nodes to be visited is like a expanding frontier (imagine a closed curve that becomes bigger and bigger in size). From which sides this frontier will expand first depends on the weights of the nodes among other things (like the distance to the ending point...etc).

ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontier

If we have reached the end, we stop and return the DijkstraHeap that has all the information about our path (because it knows how we reach each element).

forneighboringraph.neighbors( current_node.point ):

We get each of the current point neighbors

cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

For each neighbor we calculate the new cost of reaching this neighbor from the current point. This cost is formed by three quantities:

  1. The cost of reaching the current point, which is the stored cost estimate minus the heuristic distance at that point (explained below).
  2. The cost of going from the current point to the neighbor.
  3. The distance of the neighbor to the end point that we are looking.

Why this 3rd cost? Because we want to explore first the points that are near the end destination and expend less time in the points that are far from it. So if we artificially give the point a higher cost if the point is far from the destination it will be visited later.

The new cost is thus an estimate of the total cost, without knowing what lies ahead. It grows along the path as we encounter obstacles or higher-cost steps. It is essential that the heuristic never overestimates the remaining distance, otherwise the path is not necessarily optimal since the best path may not be visited before we find the end (and terminate).

When we have calculated this new cost estimate we insert the point in the cost queue.

But what about the MISTERIOUS DijkstraHeap?

Is like I said a heap that remembers the visited elements and where they came from and never pops an already visited element. The implementation is very simple:

classDijkstraHeap(list):
""" An augmented heap for the A* algorithm. This class encapsulated the residual logic of the A* algorithm like for example how to manage elements already visited that remain in the heap, elements already visited that are not in the heap and from where we came to a visited element. This class will have three main elements: - A heap that will act as a cost queue (self). - A visited dict that will act as a visited set and as a mapping of the form point:came_from - A costs dict that will act as a mapping of the form point:cost_so_far """def__init__(self, first_node=None):
self.visited=dict()
self.costs=dict()
iffirst_nodeisnotNone:
self.insert(first_node)
definsert(self, element):
""" Insert an element into the Dijkstra Heap. :param element: A Node object. :return: None """ifelement.pointnotinself.visited:
heapq.heappush(self,element)
defpop(self):
""" Pop an element from the Dijkstra Heap, adding it to the visited and cost dicts. :return: A Node object """whileselfandself[0].pointinself.visited:
heapq.heappop(self)
ifself:
next_elem=heapq.heappop(self)
self.visited[next_elem.point] =next_elem.came_fromself.costs[next_elem.point] =next_elem.costreturnnext_elem

So WHAT is this deep logic you talk about?

I think the deep logic about A* can be summarized in the following two simple points:

  • We visit the nodes in order, being this order the cost of going from the starting point to this particular node.

  • We artificially alter the cost of visiting one node taking into account how far this particular node is from the destination, making the furthest nodes more costly.

And all the stuff about the cost queue, the heap, not visiting a node already visited, what we do with nodes in the queue that have been visited.....that is important stuff but is NOT the A* algorithm: it is secondary logic and secondary problems that lead to secondary data structures.

About

A pythonic implementation of the A* algorithm.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Build StatusCoverage Status

What is THIS?

A Pythonic implementation of the famous A* algorithm.

Why ANOTHER implementation

Because coding is awesome! Also because I really dislike the mess that the usual implementations create. The A* algorithm is simple and beautiful and so must be its implementation.

How is THIS diferent?

This is different because I refactored all the logic in propositional layers. This is achieved grouping together the parts of the algorithm that has the same level of abstraction, exposing the pure logic of A* in its higher layer.

How can it be TESTED?

It comes with a maze_solving_example.py snippet to test the algorithm in a simple weighted maze ( with random weights ). If you run it you will obtain something like this:

$ python examples/maze_solving_example.py
↓ ↓ ← ← ← ← ← ← ← .
↓ ↓ ← ← ← ↑ ↑ ← ← .
↓ ↓ ← ← ← . . ↑ ← .
↓ ↓ ← ##########. . . ↑ .
→ S ← ##########. . . ↑ .
↑ ↑ ← ##########. . . ↑ .
↑ ↑ ← ##########. . → ↑ .
↑ ↑ ← ##########. . ↑ . .
↑ ↑ ← ##########. . E . .
↑ ↑ ← ##########. . . . .
5 4 5 6 7 8 9 10 11 .
4 3 4 5 10 10 10 11 12 .
3 2 3 4 11 . . 12 13 .
2 1 2 ##########. . . 14 .
1 S 1 ##########. . . 15 .
2 1 2 ##########. . . 16 .
3 2 3 ##########. . 18 17 .
4 3 4 ##########. . 19 . .
5 4 5 ##########. . E . .
6 5 6 ##########. . . . .
[...]

The first diagram represents where each point came from. Starting in the E ( standing for ENDING POINT) we backtrack each arrow untill we reach the S ( standing for STARTING POINT). This path is the shortest path. And you know what?

This is also true for every point in the diagram!!!

The arcane forces of A* make that if you start in a visited point and backtrack using the arrows you will get the shortest path.

The second diagram is the cost to reach each point from the START (S).

A third diagram, not shown here, will also be printed. This diagram shows the estimated total cost from START (S) to END (E) at each point, which is central to the efficiency of the A* algoritm as explained below.

Can you EXPLAIN the algorithm?

Yeah! The idea of the A* algorithm is that starting from the start point we visit the points that are cheaper to visit. The cost of visiting a neighbor point depends on how costly is to go from the current point to a neighbor. So we check for all the points what is the neighbor that is cheaper to visit and we visit it.

The A* algorithm is basically the following:

defa_star_search(graph, start, end):
""" Calculates the shortest path from start to end. :param graph: A graph object. The graph object can be anything that implements the following methods: graph.neighbors( (x:int, y:int) ) : Iterable( (x:int,y:int), (x:int,y:int), ...) graph.cost( (x:int,y:int) ) : int :param start: Tuple of two ints representing the starting point. :param end: Tuple of two ints representing the ending point. :returns: A DijkstraHeap object. """frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )
whilefrontier:
current_node=frontier.pop()
ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontierforneighboringraph.neighbors( current_node.point ):
cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

Lets go line by line:

frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )

This line creates a DijkstraHeap object and puts the starting point in it. We will see later how this can be implemented but the best part is that....This is not part of the algorithm! What is a DijkstraHeap then? This is a cost queue that has the following properties:

  • If we try to insert an already visited element in the queue the DijkstraHeap will do nothing.
  • The DijkstraHeap always pop the element that has the lowest cost and NEVER pops an already visited element.

Cool! So this DijkstraHeap knows the visiting order of the elements. Its like a heap but never pops an already visited element.

By the way, a Node object is a tuple of the form ( total_cost_estimate, point, point_from_we_came ).

whileTrue:

We loop until we have found a path, or failed to find one by exhausting all elements in the queue.

current_node=frontier.pop()

Each iteration we pop an element from the DijkstraHeap. This element always has the lowest cost element because the DijkstraHeap has this property ( because is a heap and heaps are awesome ).

At this point maybe you are asking yourself why the name frontier? Well, this is because when you are at the starting point and you visit neighbors, the queue of the nodes to be visited is like a expanding frontier (imagine a closed curve that becomes bigger and bigger in size). From which sides this frontier will expand first depends on the weights of the nodes among other things (like the distance to the ending point...etc).

ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontier

If we have reached the end, we stop and return the DijkstraHeap that has all the information about our path (because it knows how we reach each element).

forneighboringraph.neighbors( current_node.point ):

We get each of the current point neighbors

cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

For each neighbor we calculate the new cost of reaching this neighbor from the current point. This cost is formed by three quantities:

  1. The cost of reaching the current point, which is the stored cost estimate minus the heuristic distance at that point (explained below).
  2. The cost of going from the current point to the neighbor.
  3. The distance of the neighbor to the end point that we are looking.

Why this 3rd cost? Because we want to explore first the points that are near the end destination and expend less time in the points that are far from it. So if we artificially give the point a higher cost if the point is far from the destination it will be visited later.

The new cost is thus an estimate of the total cost, without knowing what lies ahead. It grows along the path as we encounter obstacles or higher-cost steps. It is essential that the heuristic never overestimates the remaining distance, otherwise the path is not necessarily optimal since the best path may not be visited before we find the end (and terminate).

When we have calculated this new cost estimate we insert the point in the cost queue.

But what about the MISTERIOUS DijkstraHeap?

Is like I said a heap that remembers the visited elements and where they came from and never pops an already visited element. The implementation is very simple:

classDijkstraHeap(list):
""" An augmented heap for the A* algorithm. This class encapsulated the residual logic of the A* algorithm like for example how to manage elements already visited that remain in the heap, elements already visited that are not in the heap and from where we came to a visited element. This class will have three main elements: - A heap that will act as a cost queue (self). - A visited dict that will act as a visited set and as a mapping of the form point:came_from - A costs dict that will act as a mapping of the form point:cost_so_far """def__init__(self, first_node=None):
self.visited=dict()
self.costs=dict()
iffirst_nodeisnotNone:
self.insert(first_node)
definsert(self, element):
""" Insert an element into the Dijkstra Heap. :param element: A Node object. :return: None """ifelement.pointnotinself.visited:
heapq.heappush(self,element)
defpop(self):
""" Pop an element from the Dijkstra Heap, adding it to the visited and cost dicts. :return: A Node object """whileselfandself[0].pointinself.visited:
heapq.heappop(self)
ifself:
next_elem=heapq.heappop(self)
self.visited[next_elem.point] =next_elem.came_fromself.costs[next_elem.point] =next_elem.costreturnnext_elem

So WHAT is this deep logic you talk about?

I think the deep logic about A* can be summarized in the following two simple points:

  • We visit the nodes in order, being this order the cost of going from the starting point to this particular node.

  • We artificially alter the cost of visiting one node taking into account how far this particular node is from the destination, making the furthest nodes more costly.

And all the stuff about the cost queue, the heap, not visiting a node already visited, what we do with nodes in the queue that have been visited.....that is important stuff but is NOT the A* algorithm: it is secondary logic and secondary problems that lead to secondary data structures.

About

A pythonic implementation of the A* algorithm.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Build StatusCoverage Status

What is THIS?

A Pythonic implementation of the famous A* algorithm.

Why ANOTHER implementation

Because coding is awesome! Also because I really dislike the mess that the usual implementations create. The A* algorithm is simple and beautiful and so must be its implementation.

How is THIS diferent?

This is different because I refactored all the logic in propositional layers. This is achieved grouping together the parts of the algorithm that has the same level of abstraction, exposing the pure logic of A* in its higher layer.

How can it be TESTED?

It comes with a maze_solving_example.py snippet to test the algorithm in a simple weighted maze ( with random weights ). If you run it you will obtain something like this:

$ python examples/maze_solving_example.py
↓ ↓ ← ← ← ← ← ← ← .
↓ ↓ ← ← ← ↑ ↑ ← ← .
↓ ↓ ← ← ← . . ↑ ← .
↓ ↓ ← ##########. . . ↑ .
→ S ← ##########. . . ↑ .
↑ ↑ ← ##########. . . ↑ .
↑ ↑ ← ##########. . → ↑ .
↑ ↑ ← ##########. . ↑ . .
↑ ↑ ← ##########. . E . .
↑ ↑ ← ##########. . . . .
5 4 5 6 7 8 9 10 11 .
4 3 4 5 10 10 10 11 12 .
3 2 3 4 11 . . 12 13 .
2 1 2 ##########. . . 14 .
1 S 1 ##########. . . 15 .
2 1 2 ##########. . . 16 .
3 2 3 ##########. . 18 17 .
4 3 4 ##########. . 19 . .
5 4 5 ##########. . E . .
6 5 6 ##########. . . . .
[...]

The first diagram represents where each point came from. Starting in the E ( standing for ENDING POINT) we backtrack each arrow untill we reach the S ( standing for STARTING POINT). This path is the shortest path. And you know what?

This is also true for every point in the diagram!!!

The arcane forces of A* make that if you start in a visited point and backtrack using the arrows you will get the shortest path.

The second diagram is the cost to reach each point from the START (S).

A third diagram, not shown here, will also be printed. This diagram shows the estimated total cost from START (S) to END (E) at each point, which is central to the efficiency of the A* algoritm as explained below.

Can you EXPLAIN the algorithm?

Yeah! The idea of the A* algorithm is that starting from the start point we visit the points that are cheaper to visit. The cost of visiting a neighbor point depends on how costly is to go from the current point to a neighbor. So we check for all the points what is the neighbor that is cheaper to visit and we visit it.

The A* algorithm is basically the following:

defa_star_search(graph, start, end):
""" Calculates the shortest path from start to end. :param graph: A graph object. The graph object can be anything that implements the following methods: graph.neighbors( (x:int, y:int) ) : Iterable( (x:int,y:int), (x:int,y:int), ...) graph.cost( (x:int,y:int) ) : int :param start: Tuple of two ints representing the starting point. :param end: Tuple of two ints representing the ending point. :returns: A DijkstraHeap object. """frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )
whilefrontier:
current_node=frontier.pop()
ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontierforneighboringraph.neighbors( current_node.point ):
cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

Lets go line by line:

frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )

This line creates a DijkstraHeap object and puts the starting point in it. We will see later how this can be implemented but the best part is that....This is not part of the algorithm! What is a DijkstraHeap then? This is a cost queue that has the following properties:

  • If we try to insert an already visited element in the queue the DijkstraHeap will do nothing.
  • The DijkstraHeap always pop the element that has the lowest cost and NEVER pops an already visited element.

Cool! So this DijkstraHeap knows the visiting order of the elements. Its like a heap but never pops an already visited element.

By the way, a Node object is a tuple of the form ( total_cost_estimate, point, point_from_we_came ).

whileTrue:

We loop until we have found a path, or failed to find one by exhausting all elements in the queue.

current_node=frontier.pop()

Each iteration we pop an element from the DijkstraHeap. This element always has the lowest cost element because the DijkstraHeap has this property ( because is a heap and heaps are awesome ).

At this point maybe you are asking yourself why the name frontier? Well, this is because when you are at the starting point and you visit neighbors, the queue of the nodes to be visited is like a expanding frontier (imagine a closed curve that becomes bigger and bigger in size). From which sides this frontier will expand first depends on the weights of the nodes among other things (like the distance to the ending point...etc).

ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontier

If we have reached the end, we stop and return the DijkstraHeap that has all the information about our path (because it knows how we reach each element).

forneighboringraph.neighbors( current_node.point ):

We get each of the current point neighbors

cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

For each neighbor we calculate the new cost of reaching this neighbor from the current point. This cost is formed by three quantities:

  1. The cost of reaching the current point, which is the stored cost estimate minus the heuristic distance at that point (explained below).
  2. The cost of going from the current point to the neighbor.
  3. The distance of the neighbor to the end point that we are looking.

Why this 3rd cost? Because we want to explore first the points that are near the end destination and expend less time in the points that are far from it. So if we artificially give the point a higher cost if the point is far from the destination it will be visited later.

The new cost is thus an estimate of the total cost, without knowing what lies ahead. It grows along the path as we encounter obstacles or higher-cost steps. It is essential that the heuristic never overestimates the remaining distance, otherwise the path is not necessarily optimal since the best path may not be visited before we find the end (and terminate).

When we have calculated this new cost estimate we insert the point in the cost queue.

But what about the MISTERIOUS DijkstraHeap?

Is like I said a heap that remembers the visited elements and where they came from and never pops an already visited element. The implementation is very simple:

classDijkstraHeap(list):
""" An augmented heap for the A* algorithm. This class encapsulated the residual logic of the A* algorithm like for example how to manage elements already visited that remain in the heap, elements already visited that are not in the heap and from where we came to a visited element. This class will have three main elements: - A heap that will act as a cost queue (self). - A visited dict that will act as a visited set and as a mapping of the form point:came_from - A costs dict that will act as a mapping of the form point:cost_so_far """def__init__(self, first_node=None):
self.visited=dict()
self.costs=dict()
iffirst_nodeisnotNone:
self.insert(first_node)
definsert(self, element):
""" Insert an element into the Dijkstra Heap. :param element: A Node object. :return: None """ifelement.pointnotinself.visited:
heapq.heappush(self,element)
defpop(self):
""" Pop an element from the Dijkstra Heap, adding it to the visited and cost dicts. :return: A Node object """whileselfandself[0].pointinself.visited:
heapq.heappop(self)
ifself:
next_elem=heapq.heappop(self)
self.visited[next_elem.point] =next_elem.came_fromself.costs[next_elem.point] =next_elem.costreturnnext_elem

So WHAT is this deep logic you talk about?

I think the deep logic about A* can be summarized in the following two simple points:

  • We visit the nodes in order, being this order the cost of going from the starting point to this particular node.

  • We artificially alter the cost of visiting one node taking into account how far this particular node is from the destination, making the furthest nodes more costly.

And all the stuff about the cost queue, the heap, not visiting a node already visited, what we do with nodes in the queue that have been visited.....that is important stuff but is NOT the A* algorithm: it is secondary logic and secondary problems that lead to secondary data structures.

About

A pythonic implementation of the A* algorithm.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Build StatusCoverage Status

What is THIS?

A Pythonic implementation of the famous A* algorithm.

Why ANOTHER implementation

Because coding is awesome! Also because I really dislike the mess that the usual implementations create. The A* algorithm is simple and beautiful and so must be its implementation.

How is THIS diferent?

This is different because I refactored all the logic in propositional layers. This is achieved grouping together the parts of the algorithm that has the same level of abstraction, exposing the pure logic of A* in its higher layer.

How can it be TESTED?

It comes with a maze_solving_example.py snippet to test the algorithm in a simple weighted maze ( with random weights ). If you run it you will obtain something like this:

$ python examples/maze_solving_example.py
↓ ↓ ← ← ← ← ← ← ← .
↓ ↓ ← ← ← ↑ ↑ ← ← .
↓ ↓ ← ← ← . . ↑ ← .
↓ ↓ ← ##########. . . ↑ .
→ S ← ##########. . . ↑ .
↑ ↑ ← ##########. . . ↑ .
↑ ↑ ← ##########. . → ↑ .
↑ ↑ ← ##########. . ↑ . .
↑ ↑ ← ##########. . E . .
↑ ↑ ← ##########. . . . .
5 4 5 6 7 8 9 10 11 .
4 3 4 5 10 10 10 11 12 .
3 2 3 4 11 . . 12 13 .
2 1 2 ##########. . . 14 .
1 S 1 ##########. . . 15 .
2 1 2 ##########. . . 16 .
3 2 3 ##########. . 18 17 .
4 3 4 ##########. . 19 . .
5 4 5 ##########. . E . .
6 5 6 ##########. . . . .
[...]

The first diagram represents where each point came from. Starting in the E ( standing for ENDING POINT) we backtrack each arrow untill we reach the S ( standing for STARTING POINT). This path is the shortest path. And you know what?

This is also true for every point in the diagram!!!

The arcane forces of A* make that if you start in a visited point and backtrack using the arrows you will get the shortest path.

The second diagram is the cost to reach each point from the START (S).

A third diagram, not shown here, will also be printed. This diagram shows the estimated total cost from START (S) to END (E) at each point, which is central to the efficiency of the A* algoritm as explained below.

Can you EXPLAIN the algorithm?

Yeah! The idea of the A* algorithm is that starting from the start point we visit the points that are cheaper to visit. The cost of visiting a neighbor point depends on how costly is to go from the current point to a neighbor. So we check for all the points what is the neighbor that is cheaper to visit and we visit it.

The A* algorithm is basically the following:

defa_star_search(graph, start, end):
""" Calculates the shortest path from start to end. :param graph: A graph object. The graph object can be anything that implements the following methods: graph.neighbors( (x:int, y:int) ) : Iterable( (x:int,y:int), (x:int,y:int), ...) graph.cost( (x:int,y:int) ) : int :param start: Tuple of two ints representing the starting point. :param end: Tuple of two ints representing the ending point. :returns: A DijkstraHeap object. """frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )
whilefrontier:
current_node=frontier.pop()
ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontierforneighboringraph.neighbors( current_node.point ):
cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

Lets go line by line:

frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )

This line creates a DijkstraHeap object and puts the starting point in it. We will see later how this can be implemented but the best part is that....This is not part of the algorithm! What is a DijkstraHeap then? This is a cost queue that has the following properties:

  • If we try to insert an already visited element in the queue the DijkstraHeap will do nothing.
  • The DijkstraHeap always pop the element that has the lowest cost and NEVER pops an already visited element.

Cool! So this DijkstraHeap knows the visiting order of the elements. Its like a heap but never pops an already visited element.

By the way, a Node object is a tuple of the form ( total_cost_estimate, point, point_from_we_came ).

whileTrue:

We loop until we have found a path, or failed to find one by exhausting all elements in the queue.

current_node=frontier.pop()

Each iteration we pop an element from the DijkstraHeap. This element always has the lowest cost element because the DijkstraHeap has this property ( because is a heap and heaps are awesome ).

At this point maybe you are asking yourself why the name frontier? Well, this is because when you are at the starting point and you visit neighbors, the queue of the nodes to be visited is like a expanding frontier (imagine a closed curve that becomes bigger and bigger in size). From which sides this frontier will expand first depends on the weights of the nodes among other things (like the distance to the ending point...etc).

ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontier

If we have reached the end, we stop and return the DijkstraHeap that has all the information about our path (because it knows how we reach each element).

forneighboringraph.neighbors( current_node.point ):

We get each of the current point neighbors

cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

For each neighbor we calculate the new cost of reaching this neighbor from the current point. This cost is formed by three quantities:

  1. The cost of reaching the current point, which is the stored cost estimate minus the heuristic distance at that point (explained below).
  2. The cost of going from the current point to the neighbor.
  3. The distance of the neighbor to the end point that we are looking.

Why this 3rd cost? Because we want to explore first the points that are near the end destination and expend less time in the points that are far from it. So if we artificially give the point a higher cost if the point is far from the destination it will be visited later.

The new cost is thus an estimate of the total cost, without knowing what lies ahead. It grows along the path as we encounter obstacles or higher-cost steps. It is essential that the heuristic never overestimates the remaining distance, otherwise the path is not necessarily optimal since the best path may not be visited before we find the end (and terminate).

When we have calculated this new cost estimate we insert the point in the cost queue.

But what about the MISTERIOUS DijkstraHeap?

Is like I said a heap that remembers the visited elements and where they came from and never pops an already visited element. The implementation is very simple:

classDijkstraHeap(list):
""" An augmented heap for the A* algorithm. This class encapsulated the residual logic of the A* algorithm like for example how to manage elements already visited that remain in the heap, elements already visited that are not in the heap and from where we came to a visited element. This class will have three main elements: - A heap that will act as a cost queue (self). - A visited dict that will act as a visited set and as a mapping of the form point:came_from - A costs dict that will act as a mapping of the form point:cost_so_far """def__init__(self, first_node=None):
self.visited=dict()
self.costs=dict()
iffirst_nodeisnotNone:
self.insert(first_node)
definsert(self, element):
""" Insert an element into the Dijkstra Heap. :param element: A Node object. :return: None """ifelement.pointnotinself.visited:
heapq.heappush(self,element)
defpop(self):
""" Pop an element from the Dijkstra Heap, adding it to the visited and cost dicts. :return: A Node object """whileselfandself[0].pointinself.visited:
heapq.heappop(self)
ifself:
next_elem=heapq.heappop(self)
self.visited[next_elem.point] =next_elem.came_fromself.costs[next_elem.point] =next_elem.costreturnnext_elem

So WHAT is this deep logic you talk about?

I think the deep logic about A* can be summarized in the following two simple points:

  • We visit the nodes in order, being this order the cost of going from the starting point to this particular node.

  • We artificially alter the cost of visiting one node taking into account how far this particular node is from the destination, making the furthest nodes more costly.

And all the stuff about the cost queue, the heap, not visiting a node already visited, what we do with nodes in the queue that have been visited.....that is important stuff but is NOT the A* algorithm: it is secondary logic and secondary problems that lead to secondary data structures.

About

A pythonic implementation of the A* algorithm.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Build StatusCoverage Status

What is THIS?

A Pythonic implementation of the famous A* algorithm.

Why ANOTHER implementation

Because coding is awesome! Also because I really dislike the mess that the usual implementations create. The A* algorithm is simple and beautiful and so must be its implementation.

How is THIS diferent?

This is different because I refactored all the logic in propositional layers. This is achieved grouping together the parts of the algorithm that has the same level of abstraction, exposing the pure logic of A* in its higher layer.

How can it be TESTED?

It comes with a maze_solving_example.py snippet to test the algorithm in a simple weighted maze ( with random weights ). If you run it you will obtain something like this:

$ python examples/maze_solving_example.py
↓ ↓ ← ← ← ← ← ← ← .
↓ ↓ ← ← ← ↑ ↑ ← ← .
↓ ↓ ← ← ← . . ↑ ← .
↓ ↓ ← ##########. . . ↑ .
→ S ← ##########. . . ↑ .
↑ ↑ ← ##########. . . ↑ .
↑ ↑ ← ##########. . → ↑ .
↑ ↑ ← ##########. . ↑ . .
↑ ↑ ← ##########. . E . .
↑ ↑ ← ##########. . . . .
5 4 5 6 7 8 9 10 11 .
4 3 4 5 10 10 10 11 12 .
3 2 3 4 11 . . 12 13 .
2 1 2 ##########. . . 14 .
1 S 1 ##########. . . 15 .
2 1 2 ##########. . . 16 .
3 2 3 ##########. . 18 17 .
4 3 4 ##########. . 19 . .
5 4 5 ##########. . E . .
6 5 6 ##########. . . . .
[...]

The first diagram represents where each point came from. Starting in the E ( standing for ENDING POINT) we backtrack each arrow untill we reach the S ( standing for STARTING POINT). This path is the shortest path. And you know what?

This is also true for every point in the diagram!!!

The arcane forces of A* make that if you start in a visited point and backtrack using the arrows you will get the shortest path.

The second diagram is the cost to reach each point from the START (S).

A third diagram, not shown here, will also be printed. This diagram shows the estimated total cost from START (S) to END (E) at each point, which is central to the efficiency of the A* algoritm as explained below.

Can you EXPLAIN the algorithm?

Yeah! The idea of the A* algorithm is that starting from the start point we visit the points that are cheaper to visit. The cost of visiting a neighbor point depends on how costly is to go from the current point to a neighbor. So we check for all the points what is the neighbor that is cheaper to visit and we visit it.

The A* algorithm is basically the following:

defa_star_search(graph, start, end):
""" Calculates the shortest path from start to end. :param graph: A graph object. The graph object can be anything that implements the following methods: graph.neighbors( (x:int, y:int) ) : Iterable( (x:int,y:int), (x:int,y:int), ...) graph.cost( (x:int,y:int) ) : int :param start: Tuple of two ints representing the starting point. :param end: Tuple of two ints representing the ending point. :returns: A DijkstraHeap object. """frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )
whilefrontier:
current_node=frontier.pop()
ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontierforneighboringraph.neighbors( current_node.point ):
cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

Lets go line by line:

frontier=DijkstraHeap( Node(cost_estimate=heuristic(start, end), point=start, came_from=None) )

This line creates a DijkstraHeap object and puts the starting point in it. We will see later how this can be implemented but the best part is that....This is not part of the algorithm! What is a DijkstraHeap then? This is a cost queue that has the following properties:

  • If we try to insert an already visited element in the queue the DijkstraHeap will do nothing.
  • The DijkstraHeap always pop the element that has the lowest cost and NEVER pops an already visited element.

Cool! So this DijkstraHeap knows the visiting order of the elements. Its like a heap but never pops an already visited element.

By the way, a Node object is a tuple of the form ( total_cost_estimate, point, point_from_we_came ).

whileTrue:

We loop until we have found a path, or failed to find one by exhausting all elements in the queue.

current_node=frontier.pop()

Each iteration we pop an element from the DijkstraHeap. This element always has the lowest cost element because the DijkstraHeap has this property ( because is a heap and heaps are awesome ).

At this point maybe you are asking yourself why the name frontier? Well, this is because when you are at the starting point and you visit neighbors, the queue of the nodes to be visited is like a expanding frontier (imagine a closed curve that becomes bigger and bigger in size). From which sides this frontier will expand first depends on the weights of the nodes among other things (like the distance to the ending point...etc).

ifcurrent_nodeisNone:
raiseValueError("No path exists")
ifcurrent_node.point==end:
returnfrontier

If we have reached the end, we stop and return the DijkstraHeap that has all the information about our path (because it knows how we reach each element).

forneighboringraph.neighbors( current_node.point ):

We get each of the current point neighbors

cost_so_far=current_node.cost_estimate-heuristic(current_node.point, end)
new_cost= ( cost_so_far+graph.cost(current_node.point, neighbor)
+heuristic(neighbor, end) )
new_node=Node(cost_estimate=new_cost, point=neighbor, came_from=current_node.point)
frontier.insert(new_node)

For each neighbor we calculate the new cost of reaching this neighbor from the current point. This cost is formed by three quantities:

  1. The cost of reaching the current point, which is the stored cost estimate minus the heuristic distance at that point (explained below).
  2. The cost of going from the current point to the neighbor.
  3. The distance of the neighbor to the end point that we are looking.

Why this 3rd cost? Because we want to explore first the points that are near the end destination and expend less time in the points that are far from it. So if we artificially give the point a higher cost if the point is far from the destination it will be visited later.

The new cost is thus an estimate of the total cost, without knowing what lies ahead. It grows along the path as we encounter obstacles or higher-cost steps. It is essential that the heuristic never overestimates the remaining distance, otherwise the path is not necessarily optimal since the best path may not be visited before we find the end (and terminate).

When we have calculated this new cost estimate we insert the point in the cost queue.

But what about the MISTERIOUS DijkstraHeap?

Is like I said a heap that remembers the visited elements and where they came from and never pops an already visited element. The implementation is very simple:

classDijkstraHeap(list):
""" An augmented heap for the A* algorithm. This class encapsulated the residual logic of the A* algorithm like for example how to manage elements already visited that remain in the heap, elements already visited that are not in the heap and from where we came to a visited element. This class will have three main elements: - A heap that will act as a cost queue (self). - A visited dict that will act as a visited set and as a mapping of the form point:came_from - A costs dict that will act as a mapping of the form point:cost_so_far """def__init__(self, first_node=None):
self.visited=dict()
self.costs=dict()
iffirst_nodeisnotNone:
self.insert(first_node)
definsert(self, element):
""" Insert an element into the Dijkstra Heap. :param element: A Node object. :return: None """ifelement.pointnotinself.visited:
heapq.heappush(self,element)
defpop(self):
""" Pop an element from the Dijkstra Heap, adding it to the visited and cost dicts. :return: A Node object """whileselfandself[0].pointinself.visited:
heapq.heappop(self)
ifself:
next_elem=heapq.heappop(self)
self.visited[next_elem.point] =next_elem.came_fromself.costs[next_elem.point] =next_elem.costreturnnext_elem

So WHAT is this deep logic you talk about?

I think the deep logic about A* can be summarized in the following two simple points:

  • We visit the nodes in order, being this order the cost of going from the starting point to this particular node.

  • We artificially alter the cost of visiting one node taking into account how far this particular node is from the destination, making the furthest nodes more costly.

And all the stuff about the cost queue, the heap, not visiting a node already visited, what we do with nodes in the queue that have been visited.....that is important stuff but is NOT the A* algorithm: it is secondary logic and secondary problems that lead to secondary data structures.

About

A pythonic implementation of the A* algorithm.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages