From 9b46fc5adec3e731e55facc8d49527fb724c467e Mon Sep 17 00:00:00 2001 From: Jarod Lam Date: Wed, 27 Jul 2022 14:08:05 +1000 Subject: [PATCH] Add `Dict`-based A* algorithm and max distance early exit condition --- Project.toml | 2 +- src/LightOSM.jl | 4 + src/shortest_path.jl | 97 +++++++++++++-------- src/traversal.jl | 194 +++++++++++++++++++++++++++++++++--------- src/types.jl | 10 ++- test/shortest_path.jl | 39 +++++++-- test/traversal.jl | 78 +++++++++-------- 7 files changed, 305 insertions(+), 119 deletions(-) diff --git a/Project.toml b/Project.toml index 6ef397d..fda30be 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "LightOSM" uuid = "d1922b25-af4e-4ba3-84af-fe9bea896051" authors = ["Jack Chan "] -version = "0.2.3" +version = "0.2.4" [deps] DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" diff --git a/src/LightOSM.jl b/src/LightOSM.jl index e975f81..e88cc14 100644 --- a/src/LightOSM.jl +++ b/src/LightOSM.jl @@ -23,7 +23,11 @@ export GeoLocation, Building, PathAlgorithm, Dijkstra, + DijkstraVector, + DijkstraDict, AStar, + AStarVector, + AStarDict, distance, heading, calculate_location, diff --git a/src/shortest_path.jl b/src/shortest_path.jl index b13baff..e7a5e5e 100644 --- a/src/shortest_path.jl +++ b/src/shortest_path.jl @@ -4,59 +4,84 @@ origin::Union{Integer,Node}, destination::Union{Integer,Node}, [weights::AbstractMatrix=g.weights; - cost_adjustment::Function=(u, v, parents) -> 0.0) + cost_adjustment::Function=(u, v, parents) -> 0.0), + max_distance::W=typemax(W)] Calculates the shortest path between two OpenStreetMap node ids. # Arguments -- `PathAlgorithm`: Path finding algorithm, choose either `Dijkstra` or `AStar`, defaults to `Dijkstra`. -- `g::OSMGraph`: Graph container. +- `PathAlgorithm` (optional): Path finding algorithm, possible values are: + - `Dijkstra`: Same as `DijkstraVector`. This is the default algorithm. + - `DijkstraVector`: Dijkstra's algorithm using the `Vector` implementation. + Faster for small graphs and/or long paths. + - `DijkstraDict`: Dijkstra's algorithm using the `Dict` implementation. + Faster for large graphs and/or short paths. + - `AStar`: Same as `AStarVector`. + - `AStarVector`: A* algorithm using the `Vector` implementation. + Faster for small graphs and/or long paths. + - `AStarDict`: A* algorithm using the `Dict` implementation. + Faster for large graphs and/or short paths. +- `g::OSMGraph{U,T,W}`: Graph container. - `origin::Union{Integer,Node}`: Origin OpenStreetMap node or node id. -- `destination::Union{Integer,Node},`: Destination OpenStreetMap node or node id. -- `weights`: Optional matrix of node to node edge weights, defaults to `g.weights`. If a custom weights matrix - is being used with algorithm set to `:astar`, make sure that a correct heuristic is being used. -- `cost_adjustment::Function=(u, )`: Option to pass in a function to adjust the cost between each pair - of vetices `u` and `v`, normally the cost is just the weight between `u` and `v`, `cost_adjustment` takes - in 3 arguments; `u`, `v` and `parents` to apply an additive cost to the default weight. Defaults no adjustment. - Use `restriction_cost_adjustment` to consider turn restrictions. -- `heuristic::Function=distance_heuristic(g)`: Use custom heuristic with the `AStar` algorithm only. Defaults to a - function h(u, v) -> haversine(u, v), i.e. returns the haversine distances between `u` is the current - node and `v` is the neighbouring node. If `g.weight_type` is `:time` or `:lane_efficiency` use `time_heuristic(g)` - instead. +- `destination::Union{Integer,Node},`: Destination OpenStreetMap node or node + id. +- `weights`: Optional matrix of node to node edge weights, defaults to + `g.weights`. If a custom weights matrix is being used with algorithm set to + `AStar`, make sure that a correct heuristic is being used. +- `cost_adjustment::Function=(u,v,parents) -> 0.0`: Option to pass in a function + to adjust the cost between each pair of vetices `u` and `v`, normally the + cost is just the weight between `u` and `v`, `cost_adjustment` takes in 3 + arguments; `u`, `v` and `parents`; to apply an additive cost to the default + weight. Defaults no adjustment. Use `restriction_cost_adjustment` to + consider turn restrictions. +- `heuristic::Function=distance_heuristic(g)`: Use custom heuristic with the + `AStar` algorithms only. Defaults to a function + `h(u, v) -> haversine(u, v)`, i.e. returns the haversine distances between + `u`, the current node, and `v`, the neighbouring node. If `g.weight_type` + is `:time` or `:lane_efficiency`, use `time_heuristic(g)` instead. # Return -- `Union{Nothing,Vector{T}}`: Array of OpenStreetMap node ids making up the shortest path. +- `Union{Nothing,Vector{T}}`: Array of OpenStreetMap node ids making up + the shortest path. """ -function shortest_path(::Type{Dijkstra}, - g::OSMGraph, +function shortest_path(::Type{A}, + g::OSMGraph{U,T,W}, origin::Integer, destination::Integer, - weights::AbstractMatrix; - cost_adjustment::Function=(u, v, parents) -> 0.0) + weights::AbstractMatrix{W}; + cost_adjustment::Function=(u, v, parents) -> 0.0, + max_distance::W=typemax(W) + )::Union{Nothing,Vector{T}} where {A <: Dijkstra, U, T, W} o_index = node_id_to_index(g, origin) d_index = node_id_to_index(g, destination) - path = dijkstra(g.graph, weights, o_index, d_index; cost_adjustment=cost_adjustment) + path = dijkstra(A, g.graph, weights, o_index, d_index; cost_adjustment=cost_adjustment, max_distance=max_distance) isnothing(path) && return return index_to_node_id(g, path) end -function shortest_path(::Type{AStar}, - g::OSMGraph, +function shortest_path(::Type{A}, + g::OSMGraph{U,T,W}, origin::Integer, destination::Integer, - weights::AbstractMatrix; + weights::AbstractMatrix{W}; cost_adjustment::Function=(u, v, parents) -> 0.0, - heuristic::Function=distance_heuristic(g)) + heuristic::Function=distance_heuristic(g), + max_distance::W=typemax(W) + )::Union{Nothing,Vector{T}} where {A <: AStar, U, T, W} o_index = node_id_to_index(g, origin) d_index = node_id_to_index(g, destination) - path = astar(g.graph, weights, o_index, d_index; cost_adjustment=cost_adjustment, heuristic=heuristic) + path = astar(A, g.graph, weights, o_index, d_index; cost_adjustment=cost_adjustment, heuristic=heuristic, max_distance=max_distance) isnothing(path) && return return index_to_node_id(g, path) end -shortest_path(::Type{Dijkstra}, g::OSMGraph, origin::Integer, destination::Integer; kwargs...) = shortest_path(Dijkstra, g, origin, destination, g.weights; kwargs...) -shortest_path(::Type{Dijkstra}, g::OSMGraph, origin::Node, destination::Node, args...; kwargs...) = shortest_path(Dijkstra, g, origin.id, destination.id, args...; kwargs...) -shortest_path(::Type{AStar}, g::OSMGraph, origin::Integer, destination::Integer; kwargs...) = shortest_path(AStar, g, origin, destination, g.weights; kwargs...) -shortest_path(::Type{AStar}, g::OSMGraph, origin::Node, destination::Node, args...; kwargs...) = shortest_path(AStar, g, origin.id, destination.id, args...; kwargs...) -shortest_path(g::OSMGraph, args...; kwargs...) = shortest_path(Dijkstra, g, args...; kwargs...) +function shortest_path(::Type{A}, g::OSMGraph{U,T,W}, origin::Integer, destination::Integer; kwargs...)::Union{Nothing,Vector{T}} where {A <: PathAlgorithm, U, T, W} + return shortest_path(A, g, origin, destination, g.weights; kwargs...) +end +function shortest_path(::Type{A}, g::OSMGraph{U,T,W}, origin::Node{<:Integer}, destination::Node{<:Integer}, args...; kwargs...)::Union{Nothing,Vector{T}} where {A <: PathAlgorithm, U, T, W} + return shortest_path(A, g, origin.id, destination.id, args...; kwargs...) +end +function shortest_path(g::OSMGraph{U,T,W}, args...; kwargs...)::Union{Nothing,Vector{T}} where {U, T, W} + return shortest_path(Dijkstra, g, args...; kwargs...) +end """ set_dijkstra_state!(g::OSMGraph, src::Union{Integer,Vecotr{<:Integer}, weights::AbstractMatrix; cost_adjustment::Function=(u, v, parents) -> 0.0) @@ -99,7 +124,7 @@ function shortest_path_from_dijkstra_state(g::OSMGraph, origin::Integer, destina end """ - is_restricted(restriction_ll::MutableLinkedList{V}, u::U, v::U, parents::Vector{U})::Bool where {U <: Integer,V <: Integer} + is_restricted(restriction_ll::MutableLinkedList{V}, u::U, v::U, parents::P)::Bool where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer,V <: Integer} Given parents, returns `true` if path between `u` and `v` is restricted by the restriction linked list, `false` otherwise. @@ -107,12 +132,12 @@ Given parents, returns `true` if path between `u` and `v` is restricted by the r - `restriction_ll::MutableLinkedList{V}`: Linked list holding vertices in order of v -> parents. - `u::U`: Current vertex visiting. - `v::U`: Current neighbour vertex. -- `parents::Vector{U}`: Array of shortest path parents. +- `parents::Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}`: Mapping of shortest path children to parents. # Return - `Bool`: Returns true if path between `u` and `v` is restricted. """ -function is_restricted(restriction_ll::MutableLinkedList{V}, u::U, v::U, parents::Vector{U})::Bool where {U <: Integer,V <: Integer} +function is_restricted(restriction_ll::MutableLinkedList{V}, u::U, v::U, parents::P)::Bool where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer,V <: Integer} current = restriction_ll.node.next if v != current.data @@ -125,7 +150,7 @@ function is_restricted(restriction_ll::MutableLinkedList{V}, u::U, v::U, parents current = current.next if u == current.data - u = parents[u] + u = get(parents, u, zero(U)) else return false end @@ -145,12 +170,12 @@ Given parents, returns `Inf64` if path between `u` and `v` is restricted by the - `restrictions::AbstractDict{V,Vector{MutableLinkedList{V}}}`: Set of linked lists holding vertices in order of v -> parents. - `u::U`: Current vertex visiting. - `v::U`: Current neighbour vertex. -- `parents::Vector{U}`: Array of shortest path parents. +- `parents::Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}`: Mapping of shortest path children to parents. # Return - `Float64`: Returns `Inf64` if path between u and v is restricted, `0.0` otherwise. """ -function restriction_cost(restrictions::AbstractDict{V,Vector{MutableLinkedList{V}}}, u::U, v::U, parents::Vector{U})::Float64 where {U <: Integer,V <: Integer} +function restriction_cost(restrictions::AbstractDict{V,Vector{MutableLinkedList{V}}}, u::U, v::U, parents::P)::Float64 where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer,V <: Integer} !haskey(restrictions, u) && return 0.0 for ll in restrictions[u] diff --git a/src/traversal.jl b/src/traversal.jl index c7a259d..87bd263 100644 --- a/src/traversal.jl +++ b/src/traversal.jl @@ -1,40 +1,66 @@ """ - astar(g::AbstractGraph{U}, + astar([::Type{<:AStar},] + g::AbstractGraph{U}, weights::AbstractMatrix{T}, src::W, goal::W; heuristic::Function=(u, v) -> 0.0, - cost_adjustment::Function=(u, v, parents) -> 0.0 + cost_adjustment::Function=(u, v, parents) -> 0.0, + max_distance::T=typemax(T) ) where {T <: Real, U <: Integer, W <: Integer} -A* shortest path algorithm. Implemented with a min heap. Using a min heap is faster than using -a priority queue given the sparse nature of OpenStreetMap data, i.e. vertices far outnumber edges. - -Compared to `jl`, this version improves runtime, memory usage, has a flexible heuristic -function, and accounts for OpenStreetMap turn restrictions through the `cost_adjustment` function. - -**Note**: A heuristic that does not accurately estimate the remaining cost to `goal` (i.e. overestimating -heuristic) will result in a non-optimal path (i.e. not the shortest), dijkstra on the other hand -guarantees the optimal path as the heuristic cost is zero. +A* shortest path algorithm. Implemented with a min heap. Using a min heap is +faster than using a priority queue given the sparse nature of OpenStreetMap +data, i.e. vertices far outnumber edges. + +There are two implementations: +- `AStarVector` is faster for small graphs and/or long paths. This is default. + It pre-allocates vectors at the start of the algorithm to store + distances, parents and visited nodes. This speeds up graph traversal at the + cost of large memory usage. +- `AStarDict` is faster for large graphs and/or short paths. + It dynamically allocates memory during traversal to store distances, + parents and visited nodes. This is faster compared to `AStarVector` when + the graph contains a large number of nodes and/or not much traversal is + required. + +Compared to `jl`, this version improves runtime, memory usage, has a flexible +heuristic function, and accounts for OpenStreetMap turn restrictions through +the `cost_adjustment` function. + +**Note**: A heuristic that does not accurately estimate the remaining cost to +`goal` (i.e. overestimating heuristic) will result in a non-optimal path +(i.e. not the shortest), dijkstra on the other hand guarantees the optimal path +as the heuristic cost is zero. # Arguments +- `::Type{<:AStar}`: Implementation to use, either `AStarVector` (default) or + `AStarDict`. - `g::AbstractGraph{U}`: Graphs abstract graph object. - `weights::AbstractMatrix{T}`: Edge weights matrix. - `src::W`: Source vertex. - `goal::W`: Goal vertex. -- `heuristic::Function=h(u, v) = 0.0`: Heuristic cost function, takes a source and target vertex, default is 0. -- `cost_adjustment:::Function=r(u, v, parents) = 0.0`: Optional cost adjustment function for use cases such as turn restrictions, takes a source and target vertex, defaults to 0. +- `heuristic::Function=h(u, v) = 0.0`: Heuristic cost function, takes a source + and target vertex, default is 0. +- `cost_adjustment:::Function=r(u, v, parents) = 0.0`: Optional cost adjustment + function for use cases such as turn restrictions, takes a source and target + vertex, defaults to 0. +- `max_distance::T=typemax(T)`: Maximum weight to traverse the graph, returns + `nothing` if this is reached. # Return -- `Union{Nothing,Vector{U}}`: Array veritces represeting shortest path between `src` to `goal`. +- `Union{Nothing,Vector{U}}`: Array veritces represeting shortest path from + `src` to `goal`. """ -function astar(g::AbstractGraph{U}, +function astar(::Type{A}, + g::AbstractGraph{U}, weights::AbstractMatrix{T}, src::W, goal::W; heuristic::Function=(u, v) -> 0.0, - cost_adjustment::Function=(u, v, parents) -> 0.0 - ) where {T <: Real, U <: Integer, W <: Integer} + cost_adjustment::Function=(u, v, parents) -> 0.0, + max_distance::T=typemax(T) + ) where {A <: AStar, T <: Real, U <: Integer, W <: Integer} # Preallocate heap = BinaryHeap{Tuple{T, U, U}}(FastMin) # (f = g + h, current, path length) dists = fill(typemax(T), nv(g)) @@ -53,6 +79,7 @@ function astar(g::AbstractGraph{U}, len += one(U) u == goal && break # optimal path to goal found d = dists[u] + d > max_distance && return # reached max distance for v in outneighbors(g, u) visited[v] && continue @@ -68,34 +95,123 @@ function astar(g::AbstractGraph{U}, return path_from_parents(parents, goal, len) end +function astar(::Type{AStarDict}, + g::AbstractGraph{U}, + weights::AbstractMatrix{T}, + src::W, + goal::W; + heuristic::Function=(u, v) -> 0.0, + cost_adjustment::Function=(u, v, parents) -> 0.0, + max_distance::T=typemax(T) + ) where {T <: Real, U <: Integer, W <: Integer} + # Preallocate + heap = BinaryHeap{Tuple{T, U, U}}(FastMin) # (f = g + h, current, path length) + dists = Dict{U, T}() + parents = Dict{U, U}() + visited = Set{U}() + len = zero(U) -""" - dijkstra(g::AbstractGraph{U}, - weights::AbstractMatrix{T}, - src::W, - goal::W; - cost_adjustment::Function=(u, v, parents) -> 0.0 - ) where {T <: Real, U <: Integer} + # Initialize src + dists[src] = zero(T) + push!(heap, (zero(T), src, len)) + + while !isempty(heap) + _, u, len = pop!(heap) # (f = g + h, current, path length) + u in visited && continue + push!(visited, u) + len += one(U) + u == goal && break # optimal path to goal found + d = get(dists, u, typemax(T)) + d > max_distance && return # reached max distance + + for v in outneighbors(g, u) + v in visited && continue + alt = d + weights[u, v] + cost_adjustment(u, v, parents) # turn restriction would imply `Inf` cost adjustment + + if alt < get(dists, v, typemax(T)) + dists[v] = alt + parents[v] = u + push!(heap, (alt + heuristic(v, goal), v, len)) + end + end + end + + return path_from_parents(parents, goal, len) +end +function astar(g::AbstractGraph{U}, + weights::AbstractMatrix{T}, + src::W, + goal::W; + kwargs... + ) where {T <: Real, U <: Integer, W <: Integer} + return astar(AStarVector, g, weights, src, goal; kwargs...) +end -Dijkstra's shortest path algorithm with an early exit condition, is the same as astar with heuristic cost as 0. +""" +dijkstra([::Type{<:Dijkstra},] + g::AbstractGraph{U}, + weights::AbstractMatrix{T}, + src::W, + goal::W; + cost_adjustment::Function=(u, v, parents) -> 0.0, + max_distance::T=typemax(T) + ) where {T <: Real, U <: Integer, W <: Integer} + +Dijkstra's shortest path algorithm with an early exit condition, is the same as +astar with heuristic cost as 0. + +There are two implementations: +- `DijkstraVector` is faster for small graphs and/or long paths. This is default. + It pre-allocates vectors at the start of the algorithm to store + distances, parents and visited nodes. This speeds up graph traversal at the + cost of large memory usage. +- `DijkstraDict` is faster for large graphs and/or short paths. + It dynamically allocates memory during traversal to store distances, + parents and visited nodes. This is faster compared to `AStarVector` when + the graph contains a large number of nodes and/or not much traversal is + required. # Arguments +- `::Type{<:Dijkstra}`: Implementation to use, either `DijkstraVector` + (default) or `DijkstraDict`. - `g::AbstractGraph{U}`: Graphs abstract graph object. - `weights::AbstractMatrix{T}`: Edge weights matrix. - `src::W`: Source vertex. - `goal::W`: Goal vertex. -- `cost_adjustment:::Function=r(u, v, parents) = 0.0`: Optional cost adjustment function for use cases such as turn restrictions, takes a source and target vertex, defaults to 0. +- `cost_adjustment:::Function=r(u, v, parents) = 0.0`: Optional cost adjustment + function for use cases such as turn restrictions, takes a source and target + vertex, defaults to 0. +- `max_distance::T=typemax(T)`: Maximum weight to traverse the graph, returns + `nothing` if this is reached. # Return - `Union{Nothing,Vector{U}}`: Array veritces represeting shortest path between `src` to `goal`. """ +function dijkstra(::Type{A}, + g::AbstractGraph{U}, + weights::AbstractMatrix{T}, + src::W, + goal::W; + kwargs... + ) where {A <: Dijkstra, T <: Real, U <: Integer, W <: Integer} + return astar(AStarVector, g, weights, src, goal; kwargs...) +end +function dijkstra(::Type{DijkstraDict}, + g::AbstractGraph{U}, + weights::AbstractMatrix{T}, + src::W, + goal::W; + kwargs... + ) where {T <: Real, U <: Integer, W <: Integer} + return astar(AStarDict, g, weights, src, goal; kwargs...) +end function dijkstra(g::AbstractGraph{U}, weights::AbstractMatrix{T}, src::W, goal::W; - cost_adjustment::Function=(u, v, parents) -> 0.0 + kwargs... ) where {T <: Real, U <: Integer, W <: Integer} - return astar(g, weights, src, goal; cost_adjustment=cost_adjustment) + return dijkstra(DijkstraVector, g, weights, src, goal; kwargs...) end """ @@ -146,7 +262,7 @@ function dijkstra(g::AbstractGraph{U}, alt = d + weights[u, v] + cost_adjustment(u, v, parents) # turn restriction would imply `Inf` cost adjustment if alt < dists[v] - dists[v] = alt + dists[v] = alt parents[v] = u push!(heap, (alt, v)) end @@ -157,18 +273,19 @@ function dijkstra(g::AbstractGraph{U}, end """ - path_from_parents(parents::Vector{<:U}, goal::V)::Vector{U} where {U <: Integer, V <: Integer} + path_from_parents(parents::P, goal::V) where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer, V <: Integer} Extracts shortest path given dijkstra parents of a given source. # Arguments -- `parents::Vector{U}`: Array of dijkstra parent states. +- `parents::Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}`: Mapping of + dijkstra parent states. - `goal::V`: Goal vertex. # Return - `Union{Nothing,Vector{U}}`: Array veritces represeting shortest path to `goal`. """ -function path_from_parents(parents::Vector{<:U}, goal::V) where {U <: Integer, V <: Integer} +function path_from_parents(parents::P, goal::V) where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer, V <: Integer} parents[goal] == 0 && return pointer = goal @@ -183,29 +300,30 @@ function path_from_parents(parents::Vector{<:U}, goal::V) where {U <: Integer, V end """ - path_from_parents(parents::Vector{<:U}, goal::V, path_length::N)::Vector{U} where {U <: Integer, V <: Integer, N <: Integer} + path_from_parents(parents::P, goal::V, path_length::N) where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer, V <: Integer, N <: Integer} Extracts shortest path given dijkstra parents of a given source, providing `path_length` allows preallocation of the array and avoids the need to reverse the path. # Arguments -- `parents::Vector{U}`: Array of dijkstra parent states. +- `parents::Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}`: Mapping of dijkstra parent states. - `goal::V`: Goal vertex. - `path_kength::N`: Known length of the return path, allows preallocation of final path array. # Return - `Union{Nothing,Vector{U}}`: Array veritces represeting shortest path to `goal`. """ -function path_from_parents(parents::Vector{<:U}, goal::V, path_length::N) where {U <: Integer, V <: Integer, N <: Integer} - parents[goal] == 0 && return +function path_from_parents(parents::P, goal::V, path_length::N) where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer, V <: Integer, N <: Integer} + get(parents, goal, zero(U)) == 0 && return pointer = goal path = Vector{U}(undef, path_length) - for i in one(U):path_length + for i in one(U):(path_length - 1) path[path_length - i + one(U)] = pointer pointer = parents[pointer] end + path[1] = pointer return path -end \ No newline at end of file +end diff --git a/src/types.jl b/src/types.jl index 5a0a975..834bc5e 100644 --- a/src/types.jl +++ b/src/types.jl @@ -167,10 +167,14 @@ end """ PathAlgorithm. -Abstract type for path finding algorithms, concrete types are: +Abstract type for path finding algorithms: - `Dijkstra` - `AStar` """ abstract type PathAlgorithm end -struct Dijkstra <: PathAlgorithm end -struct AStar <: PathAlgorithm end \ No newline at end of file +abstract type Dijkstra <: PathAlgorithm end +abstract type DijkstraVector <: Dijkstra end +abstract type DijkstraDict <: Dijkstra end +abstract type AStar <: PathAlgorithm end +abstract type AStarVector <: AStar end +abstract type AStarDict <: AStar end diff --git a/test/shortest_path.jl b/test/shortest_path.jl index e8cdad2..cc162cc 100644 --- a/test/shortest_path.jl +++ b/test/shortest_path.jl @@ -17,9 +17,11 @@ path_from_nodes = shortest_path(g_distance, g_distance.nodes[node1_id], g_distan path_with_weights = shortest_path(g_distance, g_distance.nodes[node1_id], g_distance.nodes[node2_id], g_distance.weights) @test path_with_weights == path -# Also test astar doesn't error -path_astar = shortest_path(AStar, g_distance, node1_id, node2_id) -@test path_astar==path +# Also test other algorithms +for T in (AStar, AStarVector, AStarDict, Dijkstra, DijkstraVector, DijkstraDict) + path_T = shortest_path(T, g_distance, node1_id, node2_id) + @test path_T==path +end # Test edge weight sum equals the weight in g_distance.weights @test total_path_weight(g_distance, path) == g_distance.weights[g_distance.node_to_index[node1_id], g_distance.node_to_index[node2_id]] @@ -31,10 +33,12 @@ ones_weights = ones(n_nodes, n_nodes) # Test time weights path_time_weights = shortest_path(g_time, node1_id, node2_id) -path_time_weights_astar = shortest_path(AStar, g_time, node1_id, node2_id) @test path_time_weights[1] == node1_id @test path_time_weights[2] == node2_id -@test path_time_weights == path_time_weights_astar +for T in (AStar, AStarVector, AStarDict, Dijkstra, DijkstraVector, DijkstraDict) + path_time_weights_T = shortest_path(T, g_time, node1_id, node2_id) + @test path_time_weights_T == path_time_weights +end edge_speed = g_distance.ways[g_distance.edge_to_way[edge]].tags["maxspeed"] @test isapprox(total_path_weight(g_distance, path) / total_path_weight(g_time, path), edge_speed) @@ -57,6 +61,27 @@ g_temp.weights[g_temp.node_to_index[1004], g_temp.node_to_index[1003]] = 100 path = shortest_path(g_temp, 1007, 1003; cost_adjustment=(u, v, parents) -> 0.0) @test path == [1007, 1006, 1001, 1002, 1003] - # Test no path returns nothing -@test isnothing(shortest_path(basic_osm_graph_stub(), 1007, 1008)) \ No newline at end of file +@test isnothing(shortest_path(basic_osm_graph_stub(), 1007, 1008)) + +# Test above with all algorithms +for T in (AStar, AStarVector, AStarDict, Dijkstra, DijkstraVector, DijkstraDict) + local path_T + if T <: AStar + path_T = shortest_path(T, g_time, 1001, 1004, heuristic=time_heuristic(g_time)) + else + path_T = shortest_path(T, g_time, 1001, 1004) + end + @test path_T == [1001, 1006, 1007, 1004] + path_T = shortest_path(T, g_distance, 1001, 1004) + @test path_T == [1001, 1002, 1003, 1004] + path_T = shortest_path(T, g_distance, 1007, 1003; cost_adjustment=(u, v, parents) -> 0.0) + @test path_T == [1007, 1004, 1003] + path_T = shortest_path(T, g_distance, 1007, 1003; cost_adjustment=restriction_cost_adjustment(g_distance)) + @test path_T == [1007, 1006, 1001, 1002, 1003] + g_temp_T = deepcopy(g_distance) + g_temp_T.weights[g_temp.node_to_index[1004], g_temp.node_to_index[1003]] = 100 + path_T = shortest_path(T, g_temp, 1007, 1003; cost_adjustment=(u, v, parents) -> 0.0) + @test path_T == [1007, 1006, 1001, 1002, 1003] + @test isnothing(shortest_path(T, basic_osm_graph_stub(), 1007, 1008)) +end diff --git a/test/traversal.jl b/test/traversal.jl index c031700..b93d7fc 100644 --- a/test/traversal.jl +++ b/test/traversal.jl @@ -11,6 +11,11 @@ g3, w3 = stub_graph3() @test LightOSM.astar(g1, w1, 1, 5) == LightOSM.dijkstra(g1, w1, 1, 5) == [1, 4, 5] @test LightOSM.astar(g2, w2, 1, 6) == LightOSM.dijkstra(g2, w2, 1, 6) == [1, 4, 7, 6] @test LightOSM.astar(g3, w3, 1, 3) == LightOSM.dijkstra(g3, w3, 1, 3) == [1, 4, 2, 3] +for (T1, T2) in Base.product((AStar, AStarVector, AStarDict), (Dijkstra, DijkstraVector, DijkstraVector)) + @test LightOSM.astar(T1, g1, w1, 1, 5) == LightOSM.dijkstra(T2, g1, w1, 1, 5) == [1, 4, 5] + @test LightOSM.astar(T1, g2, w2, 1, 6) == LightOSM.dijkstra(T2, g2, w2, 1, 6) == [1, 4, 7, 6] + @test LightOSM.astar(T1, g3, w3, 1, 3) == LightOSM.dijkstra(T2, g3, w3, 1, 3) == [1, 4, 2, 3] +end # Construct shortest path from parents @test LightOSM.path_from_parents(LightOSM.dijkstra(g1, w1, 1), 5) == [1, 4, 5] @@ -20,40 +25,45 @@ g3, w3 = stub_graph3() # astar with heuristic g = basic_osm_graph_stub() -LightOSM.astar( - g.graph, - g.weights, - g.node_to_index[1008], - g.node_to_index[1003]; - heuristic=LightOSM.distance_heuristic(g) -) == [ - g.node_to_index[1008], - g.node_to_index[1007], - g.node_to_index[1004], - g.node_to_index[1003] -] - -@test LightOSM.astar( - g.graph, - g.weights, - g.node_to_index[1008], - g.node_to_index[1002]; - heuristic=LightOSM.distance_heuristic(g) -) == [ - g.node_to_index[1008], - g.node_to_index[1007], - g.node_to_index[1004], - g.node_to_index[1003], - g.node_to_index[1002] -] - -@test LightOSM.astar( - g.graph, - g.weights, - g.node_to_index[1003], - g.node_to_index[1008]; - heuristic=LightOSM.distance_heuristic(g) -) === nothing +for T in (AStar, AStarVector, AStarDict) + LightOSM.astar( + T, + g.graph, + g.weights, + g.node_to_index[1008], + g.node_to_index[1003]; + heuristic=LightOSM.distance_heuristic(g) + ) == [ + g.node_to_index[1008], + g.node_to_index[1007], + g.node_to_index[1004], + g.node_to_index[1003] + ] + + @test LightOSM.astar( + T, + g.graph, + g.weights, + g.node_to_index[1008], + g.node_to_index[1002]; + heuristic=LightOSM.distance_heuristic(g) + ) == [ + g.node_to_index[1008], + g.node_to_index[1007], + g.node_to_index[1004], + g.node_to_index[1003], + g.node_to_index[1002] + ] + + @test LightOSM.astar( + T, + g.graph, + g.weights, + g.node_to_index[1003], + g.node_to_index[1008]; + heuristic=LightOSM.distance_heuristic(g) + ) === nothing +end # download graph, pick random nodes and test dijkstra and astar equality data = HTTP.get("https://raw.githubusercontent.com/captchanjack/LightOSMFiles.jl/main/maps/south-yarra.json")