A* Algorithm in Julia. Other State Space Search algorithms are also implemented as a baseline.
This package exports the astar
function that provides a generic implementation of the algorithm.
The type of the state is totally unrestricted, just provide the functions that give neighbour states and optionally an heuristic given a state and the goal and the algorithm will find the best path.
Other implemented and exported functions to explore your state space are also exported: depthfirst
, breadthfirst
, iterative_deepening
. These other functions are intended to be almost a transparent drop in replacement for the astar
function, but they won't be able to use the heuristic.
In the Julia Pkg REPL, type: add AStarSearch
astar(neighbours, start, goal; heuristic=defaultheuristic, cost=defaultcost, isgoal=defaultisgoal, hashfn=hash, timeout=Inf, maxcost=Inf)
Execute the A* algorithm to get the best path from the start state to reach a goal condition. Only the first 3 arguments are mandatory, all the others are optional.
It returns a structure in which the status
field is a Symbol that can be either:
:success
: the algorithm found a path from start to goal:timeout
: the algorithm timed out, a partial path to the best state is returned in thepath
field:nopath
: the algorithm didn't find any path to a goal, the path to the best state is still returned
The other fields are:
path
: an array of states from the start state to the goal or the best found statecost
: the cost of the returned pathclosedsetsize
: how many states the algorithm tested if they were a goal (size of the closed set)opensetsize
: how many states were still in the open set when the algorithm ended
neighbours
: a function that takes a state and returns the neighbour states as an array (or iterable)start
: the starting state, the type of the state is completely unrestrictedgoal
: the goal state, the type is unrestricted, usually it's the same as the startheuristic
: a function that given a state and the goal returns an estimate of the cost to reach goal. This estimate should be optimistic if you want to be sure to get the best path. Notice that the best path could be very expensive to find, so if you want a good but not guaranteed optimal path, you could multiply your heuristic by a constant, the algorithm will usually be much fastercost
: a function that takes the current state and a neighbour and returns the cost to do that state transition. By default all transitions cost 1isgoal
: a function that takes a state and the goal and evaluates if the goal is reached (by default ==)hashfn
: a function that takes a state and returns a compact representation to use as dictionary key (usually one of UInt, Int, String), by default it is the base hash function. This is a very important field for composite states in order to avoid duplications. WARNING states with arrays as fields might return a different hash every time! If this is the case, please pass an hashfn that always returns the same value for the same state!timeout
: timeout in number of seconds after which the algorithm stops returning the best partial path to the state with the lowest heuristic, by default it is unrestricted. Please notice that the algorithm wil run AT LEAST the specified time.maxcost
: a maximum bound of the accumulated cost of the path, this can result in a :nopath result even if a path to the goal (with a greater cost) exists. By default it is Infenable_closedset
: keep track of already visited nodes to avoid visiting them again, you might want to disable this if you know there isn't any loop in the state space graph (by default true)
This package exports also some uninformed search algorithms to compare the performance of astar, or to use when you don't have an explicit heuristic or cost. The interface of each function is almost identical, you can use any algorithm as a drop-in replacement for astar
, but the parameters about cost and heuristic wil be ignored (heuristic
, cost
, maxcost
), they have instead a maxdepth
parameter to limit the depth of the search.
The exported functions are:
depthfirst
breadthfirst
iterative_deepening
This algorithm expands the neighbours as far as it can until there are no more neighbours for a state or the maxdepth
limit is reached. The enable_closedset
is enabled by default to avoid loops, but it might hide some paths to the goal if multiple paths are possible.
This algorithm searches first all the states at the same depth, ensuring that the best solution is always found, but it is usually slower.
This algorithm iteratively executes depth first search at different levels of maxdepth
having the same properties as breadth first, with almost the speed of depth first search. But also here the enable_closedset
is disabled by default as it uses depth first internally.
It's a very general algorithm so you can solve shortest paths in mazes but also all sorts of puzzles such as the 15 Puzzle.
Both the maze example and the 15 Puzzle solver are in the test
folder.
If you want to find the best path in a maze using the manhattan heuristic you can do the following:
using Test
using AStarSearch
# Directions are seen as cartesian indexes so that they can be added to a position to get the next position
const UP = CartesianIndex(-1, 0)
const DOWN = CartesianIndex(1, 0)
const LEFT = CartesianIndex(0, -1)
const RIGHT = CartesianIndex(0, 1)
const DIRECTIONS = [UP, DOWN, LEFT, RIGHT]
# manhattan distance between positions in the maze matrix
manhattan(a::CartesianIndex, b::CartesianIndex) = sum(abs.((b - a).I))
# check to be in the maze and filter out moves that go into walls
function mazeneighbours(maze, p)
res = CartesianIndex[]
for d in DIRECTIONS
n = p + d
if 1 ≤ n[1] ≤ size(maze)[1] && 1 ≤ n[2] ≤ size(maze)[2] && !maze[n]
push!(res, n)
end
end
return res
end
function solvemaze(maze, start, goal)
currentmazeneighbours(state) = mazeneighbours(maze, state)
# Here you can use any of the exported search functions, they all share the same interface, but they won't use the heuristic and the cost
return astar(currentmazeneighbours, start, goal, heuristic=manhattan)
end
# 0 = free cell, 1 = wall
maze = [0 0 1 0 0;
0 1 0 0 0;
0 1 0 0 1;
0 0 0 1 1;
1 0 1 0 0] .== 1
start = CartesianIndex(1, 1)
goal = CartesianIndex(1, 5)
res = solvemaze(maze, start, goal)
@test res.status == :success
@test res.path == CartesianIndex{2}[
CartesianIndex(1, 1),
CartesianIndex(2, 1),
CartesianIndex(3, 1),
CartesianIndex(4, 1),
CartesianIndex(4, 2),
CartesianIndex(4, 3),
CartesianIndex(3, 3),
CartesianIndex(2, 3),
CartesianIndex(2, 4),
CartesianIndex(1, 4),
CartesianIndex(1, 5)]
@test res.cost == 10
Removed maxdepth
parameter, to improve memory usage, as maxcost
is more powerful.
Removed the AbstractAStarSearch
base struct, the astar
function is now the only supported interface by this package.
Since this release the base astar
API changes, requiring the neighbours
function as the first argument, and the second and third argument are the starting state and the goal state. All the other functions, heuristic
, cost
, and isgoal
, are optional keyword arguments, and they now all expect 2 arguments (current state and goal/next state in the case of the cost
function).
The subtyping API is the same, but the main method was renamed astar
, instead of search
The 0.3.0 release introduces a more strict type checking, requiring uniformity of types between the cost and the heuristics, to improve performance.
If you get type errors, it will probably be because by default the cost is Int64, and you provided a Float heuristic.
You can either provide the cost function that returns a float, or cast the heuristic to Int64.