Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes edge-based CH problem with u-turns at virtual edges, fixes #1593 #1596

Merged
merged 14 commits into from Apr 11, 2019
Expand Up @@ -25,6 +25,7 @@
import com.graphhopper.routing.weighting.TurnWeighting;
import com.graphhopper.storage.Graph;
import com.graphhopper.storage.SPTEntry;
import com.graphhopper.storage.TurnCostExtension;
import com.graphhopper.util.EdgeExplorer;
import com.graphhopper.util.EdgeIterator;
import com.graphhopper.util.EdgeIteratorState;
Expand All @@ -37,13 +38,18 @@ public abstract class AbstractBidirectionEdgeCHNoSOD extends AbstractBidirAlgo {
private final EdgeExplorer innerInExplorer;
private final EdgeExplorer innerOutExplorer;
private final TurnWeighting turnWeighting;
private final TurnCostExtension turnCostExtension;

public AbstractBidirectionEdgeCHNoSOD(Graph graph, TurnWeighting weighting) {
super(graph, weighting, TraversalMode.EDGE_BASED_2DIR);
this.turnWeighting = weighting;
// we need extra edge explorers, because they get called inside a loop that already iterates over edges
innerInExplorer = graph.createEdgeExplorer(DefaultEdgeFilter.inEdges(flagEncoder));
innerOutExplorer = graph.createEdgeExplorer(DefaultEdgeFilter.outEdges(flagEncoder));
if (!(graph.getExtension() instanceof TurnCostExtension)) {
throw new IllegalArgumentException("edge-based CH algorithms require a turn cost extension");
}
turnCostExtension = (TurnCostExtension) graph.getExtension();
}

@Override
Expand Down Expand Up @@ -93,6 +99,7 @@ protected void updateBestPath(EdgeIteratorState edgeState, SPTEntry entry, int t
}
}

// todo: it would be sufficient (and maybe more efficient) to use an original edge explorer here ?
EdgeIterator iter = reverse ?
innerInExplorer.setBaseNode(edgeState.getAdjNode()) :
innerOutExplorer.setBaseNode(edgeState.getAdjNode());
Expand All @@ -102,7 +109,7 @@ protected void updateBestPath(EdgeIteratorState edgeState, SPTEntry entry, int t
while (iter.next()) {
final int edgeId = getOrigEdgeId(iter, !reverse);
final int prevOrNextOrigEdgeId = getOrigEdgeId(edgeState, reverse);
if (!traversalMode.hasUTurnSupport() && edgeId == prevOrNextOrigEdgeId) {
if (!traversalMode.hasUTurnSupport() && turnCostExtension.isUTurn(edgeId, prevOrNextOrigEdgeId)) {
continue;
}
int key = GHUtility.getEdgeKey(graph, edgeId, iter.getBaseNode(), !reverse);
Expand Down Expand Up @@ -149,8 +156,11 @@ protected int getTraversalId(EdgeIteratorState edge, int origEdgeId, boolean rev

@Override
protected boolean accept(EdgeIteratorState edge, SPTEntry currEdge, boolean reverse) {
int edgeId = getOrigEdgeId(edge, !reverse);
if (!traversalMode.hasUTurnSupport() && edgeId == getIncomingEdge(currEdge))
final int incEdge = getIncomingEdge(currEdge);
if (incEdge == EdgeIterator.NO_EDGE)
return true;
final int prevOrNextEdgeId = getOrigEdgeId(edge, !reverse);
if (!traversalMode.hasUTurnSupport() && turnCostExtension.isUTurn(incEdge, prevOrNextEdgeId))
return false;

return additionalEdgeFilter == null || additionalEdgeFilter.accept(edge);
Expand Down
22 changes: 19 additions & 3 deletions core/src/main/java/com/graphhopper/routing/QueryGraph.java
Expand Up @@ -775,16 +775,32 @@ public long getTurnCostFlags(int edgeFrom, int nodeVia, int edgeTo) {
return 0;
} else if (isVirtualEdge(edgeFrom) || isVirtualEdge(edgeTo)) {
if (isVirtualEdge(edgeFrom)) {
edgeFrom = queryResults.get((edgeFrom - mainEdges) / 4).getClosestEdge().getEdge();
edgeFrom = getOriginalEdge(edgeFrom);
}
if (isVirtualEdge(edgeTo)) {
edgeTo = queryResults.get((edgeTo - mainEdges) / 4).getClosestEdge().getEdge();
edgeTo = getOriginalEdge(edgeTo);
}
return mainTurnExtension.getTurnCostFlags(edgeFrom, nodeVia, edgeTo);

} else {
return mainTurnExtension.getTurnCostFlags(edgeFrom, nodeVia, edgeTo);
}
}

@Override
public boolean isUTurn(int edgeFrom, int edgeTo) {
// detecting a u-turn from a virtual to a non-virtual edge requires looking at the original edge of the
// virtual edge. however when we are turning between virtual edges we need to compare the virtual edge ids
// see #1593
if (isVirtualEdge(edgeFrom) && !isVirtualEdge(edgeTo)) {
edgeFrom = getOriginalEdge(edgeFrom);
} else if (!isVirtualEdge(edgeFrom) && isVirtualEdge(edgeTo)) {
edgeTo = getOriginalEdge(edgeTo);
}
return mainTurnExtension.isUTurn(edgeFrom, edgeTo);
}

private int getOriginalEdge(int edgeFrom) {
return queryResults.get((edgeFrom - mainEdges) / 4).getClosestEdge().getEdge();
}
}
}
Expand Up @@ -134,11 +134,11 @@ public float calculatePriority(int node) {
float priority = params.edgeQuotientWeight * edgeQuotient +
params.originalEdgeQuotientWeight * origEdgeQuotient +
params.hierarchyDepthWeight * hierarchyDepth;
LOGGER.trace("node: %d, eq: %d / %d = %f, oeq: %d / %d = %f, depth: %d --> %f\n",
LOGGER.trace(String.format(Locale.ROOT, "node: %d, eq: %d / %d = %f, oeq: %d / %d = %f, depth: %d --> %f\n",
node,
numShortcuts, numPrevEdges, edgeQuotient,
numOrigEdges, numPrevOrigEdges, origEdgeQuotient,
hierarchyDepth, priority);
hierarchyDepth, priority));
return priority;
}

Expand Down
Expand Up @@ -44,7 +44,7 @@ public class TurnWeighting implements Weighting {
* @param turnCostExt the turn cost storage to be used
*/
public TurnWeighting(Weighting superWeighting, TurnCostExtension turnCostExt) {
this.turnCostEncoder = (TurnCostEncoder) superWeighting.getFlagEncoder();
this.turnCostEncoder = superWeighting.getFlagEncoder();
this.superWeighting = superWeighting;
this.turnCostExt = turnCostExt;

Expand All @@ -57,7 +57,7 @@ public TurnWeighting(Weighting superWeighting, TurnCostExtension turnCostExt) {
* 'tricking' other turn costs or restrictions.
*/
public TurnWeighting setDefaultUTurnCost(double costInSeconds) {
this.defaultUTurnCost = costInSeconds;
defaultUTurnCost = costInSeconds;
return this;
}

Expand Down
Expand Up @@ -197,6 +197,10 @@ public long getTurnCostFlags(int edgeFrom, int nodeVia, int edgeTo) {
return nextCostFlags(edgeFrom, nodeVia, edgeTo);
}

public boolean isUTurn(int edgeFrom, int edgeTo) {
return edgeFrom == edgeTo;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make more sense in the TurnWeighting as you wrote in #1593?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I was tempted to do it, especially since AbstractBidirectionEdgeCHNoSOD already has a reference to a TurnWeighting. Then again to make it really consistent we should also use isUTurn in AbstractRoutingAlgorithm#accept (but I haven't), but this one only has a reference to Weighting (while it could obtain a TurnCostExtension reference from Graph.getExtension). In any case TurnCostExtension needs this method, so we can override it in QueryGraphTurnExt. The routing algorithms could call it via something like TurnWeighting#isUTurn. I don't know. Either way is fine for me. Maybe postpone this decision until doing a proper u-turn clean up in #1520 ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation - makes sense

Maybe postpone this decision until doing a proper u-turn clean up in #1520 ?

👍


private long nextCostFlags(int edgeFrom, int nodeVia, int edgeTo) {
int turnCostIndex = nodeAccess.getAdditionalNodeField(nodeVia);
int i = 0;
Expand Down
19 changes: 13 additions & 6 deletions core/src/main/java/com/graphhopper/util/GHUtility.java
Expand Up @@ -136,6 +136,7 @@ public static void printGraphForUnitTest(Graph g, FlagEncoder encoder) {
}

public static void printGraphForUnitTest(Graph g, FlagEncoder encoder, BBox bBox) {
System.out.println("WARNING: printGraphForUnitTest does not pay attention to custom edge speeds at the moment");
NodeAccess na = g.getNodeAccess();
for (int node = 0; node < g.getNodes(); ++node) {
if (bBox.contains(na.getLat(node), na.getLon(node))) {
Expand Down Expand Up @@ -164,11 +165,12 @@ private static void printUnitTestEdge(FlagEncoder encoder, EdgeIteratorState edg
"graph.edge(%d, %d, %f, %s);\n", from, to, edge.getDistance(), fwd && bwd ? "true" : "false");
}

public static void buildRandomGraph(Graph graph, long seed, int numNodes, double meanDegree, boolean allowLoops, boolean allowZeroDistance, double pBothDir, double pRandomOffset) {
public static void buildRandomGraph(Graph graph, Random random, int numNodes, double meanDegree, boolean allowLoops,
boolean allowZeroDistance, DecimalEncodedValue randomSpeedEnc,
double pNonZeroLoop, double pBothDir, double pRandomOffset) {
if (numNodes < 2 || meanDegree < 1) {
throw new IllegalArgumentException("numNodes must be >= 2, meanDegree >= 1");
}
Random random = new Random(seed);
for (int i = 0; i < numNodes; ++i) {
double lat = 49.4 + (random.nextDouble() * 0.01);
double lon = 9.7 + (random.nextDouble() * 0.01);
Expand All @@ -186,7 +188,7 @@ public static void buildRandomGraph(Graph graph, long seed, int numNodes, double
}
double distance = GHUtility.getDistance(from, to, graph.getNodeAccess());
// allow loops with non-zero distance
if (from == to && random.nextDouble() < 0.7) {
if (from == to && random.nextDouble() < pNonZeroLoop) {
distance = random.nextDouble() * 1000;
}
if (!allowZeroDistance) {
Expand All @@ -199,7 +201,13 @@ public static void buildRandomGraph(Graph graph, long seed, int numNodes, double
maxDist = Math.max(maxDist, distance);
// using bidirectional edges will increase mean degree of graph above given value
boolean bothDirections = random.nextDouble() < pBothDir;
graph.edge(from, to, distance, bothDirections);
EdgeIteratorState edge = graph.edge(from, to, distance, bothDirections);
double fwdSpeed = 10 + random.nextDouble() * 120;
double bwdSpeed = 10 + random.nextDouble() * 120;
if (randomSpeedEnc != null) {
edge.set(randomSpeedEnc, fwdSpeed);
edge.setReverse(randomSpeedEnc, bwdSpeed);
}
numEdges++;
}
LOGGER.debug(String.format(Locale.ROOT, "Finished building random graph" +
Expand Down Expand Up @@ -238,8 +246,7 @@ public static void addRandomTurnCosts(Graph graph, long seed, FlagEncoder encode
restricted = true;
}
double cost = restricted ? 0 : random.nextDouble() * maxTurnCost;
turnCostExtension.addTurnInfo(inIter.getEdge(), node, outIter.getEdge(),
encoder.getTurnFlags(restricted, cost));
turnCostExtension.addTurnInfo(inIter.getEdge(), node, outIter.getEdge(), encoder.getTurnFlags(restricted, cost));
}
}
}
Expand Down
113 changes: 91 additions & 22 deletions core/src/test/java/com/graphhopper/routing/RandomCHRoutingTest.java
Expand Up @@ -7,6 +7,7 @@
import com.graphhopper.routing.util.EncodingManager;
import com.graphhopper.routing.util.TraversalMode;
import com.graphhopper.routing.weighting.FastestWeighting;
import com.graphhopper.routing.weighting.TurnWeighting;
import com.graphhopper.routing.weighting.Weighting;
import com.graphhopper.storage.*;
import com.graphhopper.storage.index.LocationIndexTree;
Expand All @@ -15,8 +16,11 @@
import com.graphhopper.util.GHUtility;
import com.graphhopper.util.PMap;
import com.graphhopper.util.shapes.BBox;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.util.ArrayList;
import java.util.List;
Expand All @@ -25,67 +29,128 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

@RunWith(Parameterized.class)
public class RandomCHRoutingTest {
private final TraversalMode traversalMode = TraversalMode.NODE_BASED;
private final TraversalMode traversalMode;
private final int maxTurnCosts;
private Directory dir;
private CarFlagEncoder encoder;
private Weighting weighting;
private GraphHopperStorage graph;
private LocationIndexTree locationIndex;
private CHGraph chGraph;

@Parameterized.Parameters(name = "{0}")
public static Object[] params() {
return new Object[]{
TraversalMode.NODE_BASED,
TraversalMode.EDGE_BASED_2DIR
};
}

public RandomCHRoutingTest(TraversalMode traversalMode) {
this.traversalMode = traversalMode;
this.maxTurnCosts = 10;
}

@Before
public void init() {
dir = new RAMDirectory();
encoder = new CarFlagEncoder();
encoder = new CarFlagEncoder(5, 5, maxTurnCosts);
EncodingManager em = EncodingManager.create(encoder);
weighting = new FastestWeighting(encoder);
graph = new GraphBuilder(em).setCHGraph(weighting).create();
GraphBuilder graphBuilder = new GraphBuilder(em);
graphBuilder.setEdgeBasedCH(traversalMode.isEdgeBased());
graph = graphBuilder.setCHGraph(weighting).create();
chGraph = graph.getGraph(CHGraph.class);
}


/**
* Runs random routing queries on a random query/CH graph with random speeds and adding random virtual edges and
* nodes.
*/
@Test
public void issues1574_1581_random() {
public void random() {
// you might have to keep this test running in an infinite loop for several minutes to find potential routing
// bugs (e.g. use intellij 'run until stop/failure').
int numNodes = 50;
long seed = System.nanoTime();
// for example these used to fail before fixing #1574 and/or #1581
// seed = 9348906923700L;
// seed = 9376976930825L;
// seed = 9436934744695L;
// seed = 10093639220394L;
// seed = 10785899964423L;

System.out.println("seed: " + seed);
Random rnd = new Random(seed);
buildRandomGraph(rnd, numNodes, 2.5, true, true, 0.9);
// we may not use an offset when query graph is involved, otherwise traveling via virtual edges will not be
// the same as taking the direct edge!
double pOffset = 0;
GHUtility.buildRandomGraph(graph, rnd, numNodes, 2.5, true, true, encoder.getAverageSpeedEnc(), 0.7, 0.9, pOffset);
if (traversalMode.isEdgeBased()) {
GHUtility.addRandomTurnCosts(graph, seed, encoder, maxTurnCosts, (TurnCostExtension) graph.getExtension());
}
runRandomTest(rnd);
}

@Test
public void issue1574_1() {
Assume.assumeFalse(traversalMode.isEdgeBased());
Random rnd = new Random(9348906923700L);
buildRandomGraphLegacy(rnd, 50, 2.5, false, true, 0.9);
runRandomTest(rnd);
}

@Test
public void issue1574_2() {
Assume.assumeFalse(traversalMode.isEdgeBased());
Random rnd = new Random(10093639220394L);
buildRandomGraphLegacy(rnd, 50, 2.5, false, true, 0.9);
runRandomTest(rnd);
}

@Test
public void issue1583() {
Assume.assumeFalse(traversalMode.isEdgeBased());
Random rnd = new Random(10785899964423L);
buildRandomGraphLegacy(rnd, 50, 2.5, true, true, 0.9);
runRandomTest(rnd);
}

@Test
public void issue1593() {
Assume.assumeTrue(traversalMode.isEdgeBased());
long seed = 60643479675316L;
Random rnd = new Random(seed);
GHUtility.buildRandomGraph(graph, rnd, 50, 2.5, true, true, encoder.getAverageSpeedEnc(), 0.7, 0.9, 0.0);
GHUtility.addRandomTurnCosts(graph, seed, encoder, maxTurnCosts, (TurnCostExtension) graph.getExtension());
runRandomTest(rnd);
}

private void runRandomTest(Random rnd) {
locationIndex = new LocationIndexTree(graph, dir);
locationIndex.prepareIndex();

graph.freeze();
PrepareContractionHierarchies pch = new PrepareContractionHierarchies(chGraph, weighting, traversalMode);
pch.doWork();

int numQueryGraph = 50;
int numQueryGraph = 25;
for (int j = 0; j < numQueryGraph; j++) {
QueryGraph queryGraph = new QueryGraph(graph);
QueryGraph chQueryGraph = new QueryGraph(chGraph);
addVirtualNodesAndEdges(rnd, queryGraph, chQueryGraph);
// add virtual nodes and edges, because they can change the routing behavior and/or produce bugs, e.g.
// when via-points are used
int numVirtualNodes = 20;
addVirtualNodesAndEdges(rnd, queryGraph, chQueryGraph, numVirtualNodes);

int numQueries = 100;
int numPathsNotFound = 0;
for (int i = 0; i < numQueries; i++) {
assertEquals("queryGraph and chQueryGraph should have equal number of nodes", queryGraph.getNodes(), chQueryGraph.getNodes());
int from = rnd.nextInt(queryGraph.getNodes());
int to = rnd.nextInt(queryGraph.getNodes());
DijkstraBidirectionRef refAlgo = new DijkstraBidirectionRef(queryGraph, weighting, TraversalMode.NODE_BASED);
Weighting w = traversalMode.isEdgeBased()
? new TurnWeighting(weighting, (TurnCostExtension) queryGraph.getExtension())
: weighting;
// using plain dijkstra instead of bidirectional, because of #1592
RoutingAlgorithm refAlgo = new Dijkstra(queryGraph, w, traversalMode);
Path refPath = refAlgo.calcPath(from, to);
double refWeight = refPath.getWeight();
if (!refPath.isFound()) {
numPathsNotFound++;
continue;
Expand All @@ -94,12 +159,13 @@ public void issues1574_1581_random() {
RoutingAlgorithm algo = pch.createAlgo(chQueryGraph, AlgorithmOptions.start().hints(new PMap().put("stall_on_demand", true)).build());
Path path = algo.calcPath(from, to);
if (!path.isFound()) {
fail("path not found for for " + from + "->" + to + ", expected weight: " + path.getWeight());
fail("path not found for " + from + "->" + to + ", expected weight: " + refWeight);
}

double weight = path.getWeight();
double refWeight = refPath.getWeight();
if (Math.abs(refWeight - weight) > 1) {
if (Math.abs(refWeight - weight) > 1.e-1) {
System.out.println("expected: " + refPath.calcNodes());
System.out.println("given: " + path.calcNodes());
fail("wrong weight: " + from + "->" + to + ", dijkstra: " + refWeight + " vs. ch: " + path.getWeight());
}
}
Expand All @@ -109,9 +175,8 @@ public void issues1574_1581_random() {
}
}

private void addVirtualNodesAndEdges(Random rnd, QueryGraph queryGraph, QueryGraph chQueryGraph) {
private void addVirtualNodesAndEdges(Random rnd, QueryGraph queryGraph, QueryGraph chQueryGraph, int numVirtualNodes) {
BBox bbox = graph.getBounds();
int numVirtualNodes = 20;
int count = 0;
List<QueryResult> qrs = new ArrayList<>(numVirtualNodes);
while (qrs.size() < numVirtualNodes) {
Expand Down Expand Up @@ -140,7 +205,11 @@ private double randomDoubleInRange(Random rnd, double min, double max) {
return min + rnd.nextDouble() * (max - min);
}

private void buildRandomGraph(Random random, int numNodes, double meanDegree, boolean allowLoops, boolean allowZeroDistance, double pBothDir) {
/**
* More or less does the same as {@link GHUtility#buildRandomGraph}, but since some special seeds
* are used in a few tests above this code is kept here. Do not use it for new tests.
*/
private void buildRandomGraphLegacy(Random random, int numNodes, double meanDegree, boolean allowLoops, boolean allowZeroDistance, double pBothDir) {
for (int i = 0; i < numNodes; ++i) {
double lat = 49.4 + (random.nextDouble() * 0.0001);
double lon = 9.7 + (random.nextDouble() * 0.0001);
Expand Down