Closed WJSchakel closed 1 year ago
I tested it with RouteWeightedGraph
that is used to determine lane change info. The weights are cached nowhere. It is arguable whether it is worthwhile to cache the weights. The above calculations are not complex (e.g. instanceof Connector check, pythagoras, and an addition). For large networks, a look-up in a map may be slower. Caching in RouteWeightedGraph
is more difficult, as it uses a Route
that can be changed at any time.
Using the end-node distance towards the destination is a particular heuristic to determine this potential, but it makes a lot of sense.
One small correction. This heuristic works if:
As we cannot affect the algorithm at that level (as we use a tool for Dijkstra) we must affect the link weights themselves. To guarantee finding the shortest path, the heuristic to add to link weight should be: end-node potential minus start-node potential. This means that both potentials have to be calculated, not just one.
Note that jgrapht (which we use for Dijkstra) has an A* implementation, see https://jgrapht.org/javadoc-1.3.1/org/jgrapht/alg/shortestpath/AStarShortestPath.html. It's quite a flexible implementation.
Wonderful! All we might need is a simple Pythagorean AStarAdmissibleHeuristic
.
Indeed. For a simple test network of 20x20 nodes, the code below makes a difference from ~25ms for Dijkstra, to ~4ms for A*.
/**
* Heuristic for the A* algorithm that uses Euclidean distance.
*/
AStarAdmissibleHeuristic<Node> EUCLIDEAN_DISTANCE = new AStarAdmissibleHeuristic<>()
{
/** {@inheritDoc} */
@Override
public double getCostEstimate(final Node sourceVertex, final Node targetVertex)
{
return sourceVertex.getPoint().distanceSI(targetVertex.getPoint());
}
};
In getShortestRouteBetween()
:
GraphPath<Node, LinkEdge<Link>> path =
linkWeight.getAStarHeuristic() == null ? DijkstraShortestPath.findPathBetween(graph, nodeFrom, nodeTo)
: new AStarShortestPath<>(graph, linkWeight.getAStarHeuristic()).getPath(nodeFrom, nodeTo);
This shows an equivalent test in Matlab.
Legend: Red: covered by Dijkstra Yellow: covered by A* Green/blue: resulting routes from both algorithms
the code below makes a difference from ~25ms for Dijkstra, to ~4ms for A*.
That is a sizable difference! Does that include the calculation of the potentials?
It does but... it also includes generation of a graph that was cached during the Dijkstra call, and re-used by the A* call. Creating the graph was the majority of the Dijkstra time.
A benchmark with 100 random networks gives A about a 20% decrease in run time. The potentials are a minute part, because letting the code calculate that 10 times (unnecessarily) does nothing about this performance gain. In my Matlab tests, A has about a 13% run time relative to Dijkstra. My guess is that the Dijkstra implementation from jgrapht is highly optimized, where A perhaps is less suitable for such optimization. In my Matlab code they run on the same algorithm, with an if-statement for 1 line of code that is different for Dijkstra and A.
But these algorithms are really quite fast either way, as setting up the test network is the majority of the running time of the benchmark code. But at least we now have A* available, which is useful for large real-world networks.
AStarAdmissibleHeuristic<Node> EUCLIDEAN_DISTANCE
was added to LinkWeight
. This implements the Euclidian heuristic for A*.LinkWeight ASTAR_LENGTH_NO_CONNECTORS
was added to LinkWeight
. This can be used with OtsNetwork.getShortestRouteBetween(...)
to get a shortest path.OtsNetwork.getShortestRouteBetween(...)
now use 1 implementation, which allows for via-nodes. The list of via-nodes may however be empty.LinkEdge
as it only wrapped a Link
. The graphs are now directly based on Link
.
Introduction Recently I saw the YouTube video The hidden beauty of the A* algorithm which gives a wonderfully intuitive explanation of the A* algorithm. Simply put, suppose that the Dijkstra algorithm has two open nodes in its process:
Dijkstra's algorithm will search further from Berlin, because 600km < 800km, so there may be a shorter route to Rome. But clearly there isn't, because Berlin is more than 200km away from Rome even in a straight line. So what A* does when it needs to choose the next link from a node, is to define the link cost as:
With this approach, much less links need to be considered before the shortest path is found. The algorithm uses "node potential" to favor links towards nodes that make much more sense to prioritize in the algorithm, i.e. nodes that have lower potential (as a more physical term). Using the end-node distance towards the destination is a particular heuristic to determine this potential, but it makes a lot of sense. There is much more to this algorithm, as different heuristics can be used, and there are certain constraints to prevent negative weights and to guarantee that the path found is the shortest. The distance-to-destination heuristic meets these requirements.
The link weight can of course also be something other than distance. In that case a sensible heuristic is required. If the weight is travel time, the potential could be defined as: xdestination/vmax, where:
xdestination: euclidean distance from the end-node to the destination vmax: maximum speed that can be achieved (e.g. per GTU type, or for a specific vehicle)
Implementation We currently use
LinkWeight
to determine the weight of links in the Dijkstra algorithm. There is a staticLENGTH
and a staticLENGTH_NO_CONNECTORS
version. We can additionally define instances using a destinationNode
that report the weight as described above. There could also be a version that uses aNode
andSpeed
for the travel time heuristic.Just-in-time weight calculation The video mentioned above also shows, in simple code, the A* algorithm implemented. Two optimizations are mentioned:
In
OtsNetwork.buildGraph()
all links are looped, and a weight for them is set, prior to the shortest path algorithm being invoked. Instead we could use an extension of the graph:JitWeightedGraph extends SimpleDirectedWeightedGraph<Node, Link>
. This class would override thedouble getEdgeWeight(Link e)
method, and use aLinkWeight
to return the weight. Perhaps it should cache the calculated weights, I'm not sure whether any of the super class functionality already takes care of that.Note that an extension of
SimpleDirectedWeightedGraph
is already used for the algorithm to determine lane change information inOtsRoadNetwork
.Current code optimization Removing LinkEdge Currently
SimpleDirectedWeightedGraph<Node, LinkEdge<Link>>
is being used, whereLinkEdge
wraps aLink
and extendsorg.jgrapht.graph.DefaultWeightedEdge
. But the second generics arguments may be anything. It seems that the default edge types are useful to use default edge suppliers, which are used forE Graph.addEgde(V sourceVertex, V targetVertex)
, but we useboolean Graph.addEdge(V sourceVertex, V targetVertex, E e)
. The use ofLinkEdge
in OTS is always with a call togetLink()
. Hence there is no need to wrap aLink
,LinkEdge
can be removed, and we can useSimpleDirectedWeightedGraph<Node, Link>
.Algorithm code duplication We have three methods in
OtsNetwork
to obtain a shortest path:getShortestRouteBetween(gtuType, nodeFrom, nodeTo, linkWeight)
,getShortestRouteBetween(gtuType, nodeFrom, nodeTo, viaNodes)
, andgetShortestRouteBetween(gtuType, nodeFrom, nodeTo, viaNodes, linkWeight)
The second refers to the third usingLinkWeight.LENGTH_NO_CONNECTORS
. The first and second have a full implementation, which look highly similar. The first can refer to the third using an emptyArrayList<Node>
forviaNodes
. In the third method there is a lineDijkstraShortestPath<Node, LinkEdge<Link>> dijkstra = new DijkstraShortestPath<>(graph);
inside a for-loop. It appears this can done once before the for-loop, as thegraph
does not change when looping over de nodes in the route.Graph cache The method
OtsNetwork.getGraph()
may retrieve a path from cache, but only forLinkWeight.LENGTH
. The only method writing in the cache isOtsNetwork.buildGraph(GtuType)
, which usesLinkWeight.LENGTH_NO_CONNECTORS
to build a graph. So not only does this not cache correctly, it also is unable to cache for otherLinkWeight
s. But we cannot cache always. If link weights are dynamic or have a random component, the graph is invalid for a second use. We can addLinkWeight.isStaticWeight()
to know whether we can cache the graph, and have a cache not only per GTU type, but also per (static)LinkWeight
.