Skip to content

Commit e9b5973

Browse files
authored
Make node-based CH more robust for different weightings (graphhopper#2514)
* search radius is based on mean graph degree again, but the factor used to determine the maximum settled nodes is now configurable * allow limiting the maximum number of neighbor updates per node * use neighbor update heuristic by default
1 parent b4df8ce commit e9b5973

File tree

5 files changed

+138
-20
lines changed

5 files changed

+138
-20
lines changed

core/src/main/java/com/graphhopper/routing/ch/CHParameters.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ public final class CHParameters {
77
public static final String PERIODIC_UPDATES = Parameters.CH.PREPARE + "updates.periodic";
88
public static final String LAST_LAZY_NODES_UPDATES = Parameters.CH.PREPARE + "updates.lazy";
99
public static final String NEIGHBOR_UPDATES = Parameters.CH.PREPARE + "updates.neighbor";
10+
public static final String NEIGHBOR_UPDATES_MAX = Parameters.CH.PREPARE + "updates.neighbor_max";
1011
public static final String CONTRACTED_NODES = Parameters.CH.PREPARE + "contracted_nodes";
1112
public static final String LOG_MESSAGES = Parameters.CH.PREPARE + "log_messages";
1213
// node contraction, node-based
1314
public static final String EDGE_DIFFERENCE_WEIGHT = Parameters.CH.PREPARE + "node.edge_difference_weight";
1415
public static final String ORIGINAL_EDGE_COUNT_WEIGHT = Parameters.CH.PREPARE + "node.original_edge_count_weight";
15-
public static final String MAX_VISITED_NODES_HEURISTIC = Parameters.CH.PREPARE + "node.max_visited_nodes_heuristic";
16-
public static final String MAX_VISITED_NODES_CONTRACTION = Parameters.CH.PREPARE + "node.max_visited_nodes_contraction";
16+
public static final String MAX_POLL_FACTOR_HEURISTIC_NODE = Parameters.CH.PREPARE + "node.max_poll_factor_heuristic";
17+
public static final String MAX_POLL_FACTOR_CONTRACTION_NODE = Parameters.CH.PREPARE + "node.max_poll_factor_contraction";
1718
// node contraction, edge-based
1819
public static final String EDGE_QUOTIENT_WEIGHT = Parameters.CH.PREPARE + "edge.edge_quotient_weight";
1920
public static final String ORIGINAL_EDGE_QUOTIENT_WEIGHT = Parameters.CH.PREPARE + "edge.original_edge_quotient_weight";

core/src/main/java/com/graphhopper/routing/ch/NodeBasedNodeContractor.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ class NodeBasedNodeContractor implements NodeContractor {
5858
private void extractParams(PMap pMap) {
5959
params.edgeDifferenceWeight = pMap.getFloat(EDGE_DIFFERENCE_WEIGHT, params.edgeDifferenceWeight);
6060
params.originalEdgesCountWeight = pMap.getFloat(ORIGINAL_EDGE_COUNT_WEIGHT, params.originalEdgesCountWeight);
61-
params.maxVisitedNodesHeuristic = pMap.getInt(MAX_VISITED_NODES_HEURISTIC, params.maxVisitedNodesHeuristic);
62-
params.maxVisitedNodesContraction = pMap.getInt(MAX_VISITED_NODES_CONTRACTION, params.maxVisitedNodesContraction);
61+
params.maxPollFactorHeuristic = pMap.getDouble(MAX_POLL_FACTOR_HEURISTIC_NODE, params.maxPollFactorHeuristic);
62+
params.maxPollFactorContraction = pMap.getDouble(MAX_POLL_FACTOR_CONTRACTION_NODE, params.maxPollFactorContraction);
6363
}
6464

6565
@Override
@@ -97,7 +97,7 @@ public float calculatePriority(int node) {
9797
// originalEdgesCount = σ(v) := sum_{ (u,w) ∈ shortcuts(v) } of r(u, w)
9898
shortcutsCount = 0;
9999
originalEdgesCount = 0;
100-
findAndHandleShortcuts(node, this::countShortcuts, params.maxVisitedNodesHeuristic);
100+
findAndHandleShortcuts(node, this::countShortcuts, (int) (meanDegree * params.maxPollFactorHeuristic));
101101

102102
// from shortcuts we can compute the edgeDifference
103103
// # low influence: with it the shortcut creation is slightly faster
@@ -116,7 +116,7 @@ public float calculatePriority(int node) {
116116

117117
@Override
118118
public IntContainer contractNode(int node) {
119-
long degree = findAndHandleShortcuts(node, this::addOrUpdateShortcut, params.maxVisitedNodesContraction);
119+
long degree = findAndHandleShortcuts(node, this::addOrUpdateShortcut, (int) (meanDegree * params.maxPollFactorContraction));
120120
insertShortcuts(node);
121121
// put weight factor on meanDegree instead of taking the average => meanDegree is more stable
122122
meanDegree = (meanDegree * 2 + degree) / 3;
@@ -310,11 +310,10 @@ public static class Params {
310310
private float edgeDifferenceWeight = 10;
311311
private float originalEdgesCountWeight = 1;
312312
// these values seemed to work best for planet (fast prep without compromising too much for the query time)
313-
// higher values can further decrease the number of shortcuts and improve the query time, but only by a few
314-
// percent and at the cost of a longer preparation. for smaller maps smaller values can work even better, i.e.
315-
// try 20/100 or similar.
316-
private int maxVisitedNodesHeuristic = 30;
317-
private int maxVisitedNodesContraction = 200;
313+
// higher values can further decrease the number of shortcuts and improve the query time, but normally at the
314+
// cost of a longer preparation (see #2514)
315+
private double maxPollFactorHeuristic = 5;
316+
private double maxPollFactorContraction = 200;
318317
}
319318

320319
private static class Shortcut {

core/src/main/java/com/graphhopper/routing/ch/PrepareContractionHierarchies.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public PrepareContractionHierarchies setParams(PMap pMap) {
9797
params.setPeriodicUpdatesPercentage(pMap.getInt(PERIODIC_UPDATES, params.getPeriodicUpdatesPercentage()));
9898
params.setLastNodesLazyUpdatePercentage(pMap.getInt(LAST_LAZY_NODES_UPDATES, params.getLastNodesLazyUpdatePercentage()));
9999
params.setNeighborUpdatePercentage(pMap.getInt(NEIGHBOR_UPDATES, params.getNeighborUpdatePercentage()));
100+
params.setMaxNeighborUpdates(pMap.getInt(NEIGHBOR_UPDATES_MAX, params.getMaxNeighborUpdates()));
100101
params.setNodesContractedPercentage(pMap.getInt(CONTRACTED_NODES, params.getNodesContractedPercentage()));
101102
params.setLogMessagesPercentage(pMap.getInt(LOG_MESSAGES, params.getLogMessagesPercentage()));
102103
return this;
@@ -289,13 +290,14 @@ private void contractNodesUsingHeuristicNodeOrdering() {
289290
// skipped nodes are already set to maxLevel
290291
break;
291292

293+
int neighborCount = 0;
292294
// there might be multiple edges going to the same neighbor nodes -> only calculate priority once per node
293295
for (IntCursor neighbor : neighbors) {
294-
int nn = neighbor.value;
295-
if (neighborUpdate && rand.nextInt(100) < params.getNeighborUpdatePercentage()) {
296+
if (neighborUpdate && (params.getMaxNeighborUpdates() < 0 || neighborCount < params.getMaxNeighborUpdates()) && rand.nextInt(100) < params.getNeighborUpdatePercentage()) {
297+
neighborCount++;
296298
neighborUpdateSW.start();
297-
float priority = calculatePriority(nn);
298-
sortedNodes.update(nn, priority);
299+
float priority = calculatePriority(neighbor.value);
300+
sortedNodes.update(neighbor.value, priority);
299301
neighborUpdateSW.stop();
300302
}
301303
}
@@ -508,6 +510,11 @@ private static class Params {
508510
* times, but the node priorities will be more up-to-date.
509511
*/
510512
private int neighborUpdatePercentage;
513+
/**
514+
* Specifies the maximum number of neighbor updates per contracted node. For example for the foot profile we
515+
* see a large number of neighbor updates that can be limited with this setting. -1 means unlimited.
516+
*/
517+
private int maxNeighborUpdates;
511518
/**
512519
* Defines how many nodes (percentage) should be contracted. A value of 20 means only the first 20% of all nodes
513520
* will be contracted. Higher values here mean longer preparation times, but faster queries (because the
@@ -524,17 +531,18 @@ private static class Params {
524531
static Params forTraversalMode(TraversalMode traversalMode) {
525532
if (traversalMode.isEdgeBased()) {
526533
// todo: optimize
527-
return new Params(0, 100, 0, 100, 5);
534+
return new Params(0, 100, 0, -1, 100, 5);
528535
} else {
529-
return new Params(0, 0, 100, 100, 20);
536+
return new Params(0, 100, 100, 2, 100, 20);
530537
}
531538
}
532539

533-
private Params(int periodicUpdatesPercentage, int lastNodesLazyUpdatePercentage, int neighborUpdatePercentage,
540+
private Params(int periodicUpdatesPercentage, int lastNodesLazyUpdatePercentage, int neighborUpdatePercentage, int maxNeighborUpdates,
534541
int nodesContractedPercentage, int logMessagesPercentage) {
535542
setPeriodicUpdatesPercentage(periodicUpdatesPercentage);
536543
setLastNodesLazyUpdatePercentage(lastNodesLazyUpdatePercentage);
537544
setNeighborUpdatePercentage(neighborUpdatePercentage);
545+
setMaxNeighborUpdates(maxNeighborUpdates);
538546
setNodesContractedPercentage(nodesContractedPercentage);
539547
setLogMessagesPercentage(logMessagesPercentage);
540548
}
@@ -566,6 +574,14 @@ void setNeighborUpdatePercentage(int neighborUpdatePercentage) {
566574
this.neighborUpdatePercentage = neighborUpdatePercentage;
567575
}
568576

577+
int getMaxNeighborUpdates() {
578+
return maxNeighborUpdates;
579+
}
580+
581+
void setMaxNeighborUpdates(int maxNeighborUpdates) {
582+
this.maxNeighborUpdates = maxNeighborUpdates;
583+
}
584+
569585
int getNodesContractedPercentage() {
570586
return nodesContractedPercentage;
571587
}

core/src/test/java/com/graphhopper/GraphHopperTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ public void setup() {
9595
ASTAR + ",false,444",
9696
DIJKSTRA_BI + ",false,228",
9797
ASTAR_BI + ",false,184",
98-
ASTAR_BI + ",true,67",
99-
DIJKSTRA_BI + ",true,65"
98+
ASTAR_BI + ",true,61",
99+
DIJKSTRA_BI + ",true,61"
100100
})
101101
public void testMonacoDifferentAlgorithms(String algo, boolean withCH, int expectedVisitedNodes) {
102102
final String vehicle = "car";
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Licensed to GraphHopper GmbH under one or more contributor
3+
* license agreements. See the NOTICE file distributed with this work for
4+
* additional information regarding copyright ownership.
5+
*
6+
* GraphHopper GmbH licenses this file to you under the Apache License,
7+
* Version 2.0 (the "License"); you may not use this file except in
8+
* compliance with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.graphhopper.tools;
20+
21+
import com.graphhopper.GHRequest;
22+
import com.graphhopper.GHResponse;
23+
import com.graphhopper.GraphHopper;
24+
import com.graphhopper.GraphHopperConfig;
25+
import com.graphhopper.config.CHProfile;
26+
import com.graphhopper.config.Profile;
27+
import com.graphhopper.routing.ch.CHParameters;
28+
import com.graphhopper.routing.util.countryrules.CountryRuleFactory;
29+
import com.graphhopper.util.MiniPerfTest;
30+
import com.graphhopper.util.PMap;
31+
import com.graphhopper.util.exceptions.ConnectionNotFoundException;
32+
import com.graphhopper.util.exceptions.PointNotFoundException;
33+
import com.graphhopper.util.shapes.BBox;
34+
import com.graphhopper.util.shapes.GHPoint;
35+
36+
import java.util.Arrays;
37+
import java.util.Collections;
38+
import java.util.Random;
39+
import java.util.concurrent.atomic.AtomicInteger;
40+
41+
public class CHImportTest {
42+
public static void main(String[] args) {
43+
System.out.println("running for args: " + Arrays.toString(args));
44+
PMap map = PMap.read(args);
45+
String vehicle = map.getString("vehicle", "car");
46+
GraphHopperConfig config = new GraphHopperConfig(map);
47+
config.putObject("datareader.file", map.getString("pbf", "map-matching/files/leipzig_germany.osm.pbf"));
48+
config.putObject("graph.location", map.getString("gh", "ch-import-test-gh"));
49+
config.setProfiles(Arrays.asList(
50+
new Profile(vehicle).setVehicle(vehicle).setWeighting("fastest")
51+
));
52+
config.setCHProfiles(Collections.singletonList(new CHProfile(vehicle)));
53+
config.putObject(CHParameters.PERIODIC_UPDATES, map.getInt("periodic", 0));
54+
config.putObject(CHParameters.LAST_LAZY_NODES_UPDATES, map.getInt("lazy", 100));
55+
config.putObject(CHParameters.NEIGHBOR_UPDATES, map.getInt("neighbor", 100));
56+
config.putObject(CHParameters.NEIGHBOR_UPDATES_MAX, map.getInt("neighbor_max", 2));
57+
config.putObject(CHParameters.CONTRACTED_NODES, map.getInt("contracted", 100));
58+
config.putObject(CHParameters.LOG_MESSAGES, map.getInt("logs", 20));
59+
config.putObject(CHParameters.EDGE_DIFFERENCE_WEIGHT, map.getDouble("edge_diff", 10));
60+
config.putObject(CHParameters.ORIGINAL_EDGE_COUNT_WEIGHT, map.getDouble("orig_edge", 1));
61+
config.putObject(CHParameters.MAX_POLL_FACTOR_HEURISTIC_NODE, map.getDouble("mpf_heur", 5));
62+
config.putObject(CHParameters.MAX_POLL_FACTOR_CONTRACTION_NODE, map.getDouble("mpf_contr", 200));
63+
GraphHopper hopper = new GraphHopper();
64+
hopper.init(config);
65+
if (map.getBool("use_country_rules", false))
66+
// note that using this requires a new import of the base graph!
67+
hopper.setCountryRuleFactory(new CountryRuleFactory());
68+
hopper.importOrLoad();
69+
runQueries(hopper, vehicle);
70+
}
71+
72+
private static void runQueries(GraphHopper hopper, String profile) {
73+
// Bavaria, but trying to avoid regions that are not covered
74+
BBox bounds = new BBox(10.508422, 12.326602, 47.713457, 49.940615);
75+
int numQueries = 10_000;
76+
long seed = 123;
77+
Random rnd = new Random(seed);
78+
AtomicInteger notFoundCount = new AtomicInteger();
79+
MiniPerfTest test = new MiniPerfTest().setIterations(numQueries).start((warmup, run) -> {
80+
GHPoint from = getRandomPoint(rnd, bounds);
81+
GHPoint to = getRandomPoint(rnd, bounds);
82+
GHRequest req = new GHRequest(from, to).setProfile(profile);
83+
GHResponse rsp = hopper.route(req);
84+
if (rsp.hasErrors()) {
85+
if (rsp.getErrors().stream().anyMatch(t -> !(t instanceof PointNotFoundException || t instanceof ConnectionNotFoundException)))
86+
throw new IllegalStateException("Unexpected error: " + rsp.getErrors().toString());
87+
notFoundCount.incrementAndGet();
88+
return 0;
89+
} else {
90+
return (int) rsp.getBest().getRouteWeight();
91+
}
92+
});
93+
System.out.println("Total queries: " + numQueries + ", Failed queries: " + notFoundCount.get());
94+
System.out.println(test.getReport());
95+
}
96+
97+
private static GHPoint getRandomPoint(Random rnd, BBox bounds) {
98+
double lat = bounds.minLat + rnd.nextDouble() * (bounds.maxLat - bounds.minLat);
99+
double lon = bounds.minLon + rnd.nextDouble() * (bounds.maxLon - bounds.minLon);
100+
return new GHPoint(lat, lon);
101+
}
102+
}

0 commit comments

Comments
 (0)