Skip to content

Commit bd272a9

Browse files
committed
no static factor for large areas to store landmark weights more precise
1 parent 385178d commit bd272a9

File tree

3 files changed

+120
-71
lines changed

3 files changed

+120
-71
lines changed

core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java

Lines changed: 113 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import com.graphhopper.storage.*;
3737
import com.graphhopper.util.*;
3838
import com.graphhopper.util.exceptions.ConnectionNotFoundException;
39-
import com.graphhopper.util.shapes.BBox;
4039
import com.graphhopper.util.shapes.GHPoint;
4140
import org.slf4j.Logger;
4241
import org.slf4j.LoggerFactory;
@@ -227,28 +226,6 @@ public void createLandmarks() {
227226
landmarkWeightDA.setShort(pointer, (short) SHORT_INFINITY);
228227
}
229228

230-
String additionalInfo = "";
231-
// guess the factor
232-
if (factor <= 0) {
233-
// A 'factor' is necessary to store the weight in just a short value but without loosing too much precision.
234-
// This factor is rather delicate to pick, we estimate it through the graph boundaries its maximum distance.
235-
// For small areas we use max_bounds_dist*X and otherwise we use a big fixed value for this distance.
236-
// If we would pick the distance too big for small areas this could lead to (slightly) suboptimal routes as there
237-
// will be too big rounding errors. But picking it too small is dangerous regarding performance
238-
// e.g. for Germany at least 1500km is very important otherwise speed is at least twice as slow e.g. for just 1000km
239-
BBox bounds = graph.getBounds();
240-
double distanceInMeter = Helper.DIST_EARTH.calcDist(bounds.maxLat, bounds.maxLon, bounds.minLat, bounds.minLon) * 7;
241-
if (distanceInMeter > 50_000 * 7 || /* for tests and convenience we do for now: */ !bounds.isValid())
242-
distanceInMeter = 30_000_000;
243-
244-
double maxWeight = weighting.getMinWeight(distanceInMeter);
245-
setMaximumWeight(maxWeight);
246-
additionalInfo = ", maxWeight:" + maxWeight + ", from max distance:" + distanceInMeter / 1000f + "km";
247-
}
248-
249-
if (logDetails)
250-
LOGGER.info("init landmarks for subnetworks with node count greater than " + minimumNodes + " with factor:" + factor + additionalInfo);
251-
252229
int[] empty = new int[landmarks];
253230
Arrays.fill(empty, UNSET_SUBNETWORK);
254231
landmarkIDs.add(empty);
@@ -277,12 +254,30 @@ public void createLandmarks() {
277254
LOGGER.info("Calculated " + graphComponents.size() + " subnetworks via tarjan in " + sw.stop().getSeconds() + "s, " + Helper.getMemInfo());
278255

279256
EdgeExplorer tmpExplorer = graph.createEdgeExplorer(new RequireBothDirectionsEdgeFilter(encoder));
257+
String additionalInfo = "";
258+
// guess the factor
259+
if (factor <= 0) {
260+
// A 'factor' is necessary to store the weight in just a short value but without loosing too much precision.
261+
// This factor is rather delicate to pick, we estimate it from an exploration with some "test landmarks",
262+
// see estimateMaxWeight. If we pick the distance too big for small areas this could lead to (slightly)
263+
// suboptimal routes as there will be too big rounding errors. But picking it too small is bad for performance
264+
// e.g. for Germany at least 1500km is very important otherwise speed is at least twice as slow e.g. for 1000km
265+
double maxWeight = estimateMaxWeight(tmpExplorer, graphComponents, blockedEdges);
266+
setMaximumWeight(maxWeight);
267+
additionalInfo = ", maxWeight:" + maxWeight + " from quick estimation";
268+
}
269+
270+
if (logDetails)
271+
LOGGER.info("init landmarks for subnetworks with node count greater than " + minimumNodes + " with factor:" + factor + additionalInfo);
280272

281273
int nodes = 0;
282274
for (IntArrayList subnetworkIds : graphComponents) {
283275
nodes += subnetworkIds.size();
284276
if (subnetworkIds.size() < minimumNodes)
285277
continue;
278+
if (factor <= 0)
279+
throw new IllegalStateException("factor wasn't initialized " + factor + ", subnetworks:"
280+
+ graphComponents.size() + ", minimumNodes:" + minimumNodes + ", current size:" + subnetworkIds.size());
286281

287282
int index = subnetworkIds.size() - 1;
288283
// ensure start node is reachable from both sides and no subnetwork is associated
@@ -335,6 +330,56 @@ public void createLandmarks() {
335330
initialized = true;
336331
}
337332

333+
/**
334+
* This method returns the maximum weight for the graph starting from the landmarks
335+
*/
336+
private double estimateMaxWeight(EdgeExplorer requireBothDirExplorer, List<IntArrayList> graphComponents, IntHashSet blockedEdges) {
337+
double maxWeight = 0;
338+
int searchedSubnetworks = 0;
339+
for (IntArrayList subnetworkIds : graphComponents) {
340+
if (subnetworkIds.size() < minimumNodes)
341+
continue;
342+
343+
searchedSubnetworks++;
344+
int index = subnetworkIds.size() - 1;
345+
SUBNETWORK:
346+
for (; index >= 0; index--) {
347+
int nextStartNode = subnetworkIds.get(index);
348+
if (GHUtility.count(requireBothDirExplorer.setBaseNode(nextStartNode)) > 0) {
349+
Weighting initWeighting = lmSelectionWeighting;
350+
int startNode = nextStartNode;
351+
352+
for (int i = 0; i < landmarks; i++) {
353+
// search potential landmark
354+
LandmarkExplorer explorer = new LandmarkExplorer(graph, this, initWeighting, traversalMode, true);
355+
explorer.setStartNode(startNode);
356+
explorer.setFilter(blockedEdges, true, true);
357+
explorer.runAlgo();
358+
if (explorer.getFromCount() < minimumNodes)
359+
continue;
360+
361+
// from landmark we can explore the graph and get the max weight
362+
int potentialLandmarkNodeId = startNode = explorer.getLastEntry().adjNode;
363+
explorer = new LandmarkExplorer(graph, this, weighting, traversalMode, true);
364+
explorer.setStartNode(potentialLandmarkNodeId);
365+
explorer.setFilter(blockedEdges, true, true);
366+
explorer.runAlgo();
367+
maxWeight = Math.max(maxWeight, explorer.getLastEntry().weight);
368+
break SUBNETWORK;
369+
}
370+
}
371+
}
372+
}
373+
374+
if (maxWeight <= 0 && searchedSubnetworks > 0)
375+
throw new IllegalStateException("max weight wasn't set although " + searchedSubnetworks + " subnetworks were searched (total " + graphComponents.size() + "), minimumNodes:" + minimumNodes);
376+
377+
// we have to increase maxWeight slightly as it is only an approximation towards the maximum weight
378+
// TODO NOW but why is this necessary when we loop through all the landmarks and only those weights are used? We shouldn't need to determine All-to-All
379+
// a value too small <= 1.002 or too high >= 1.05 will let GraphHopperIT.testImportThenLoadLM fail
380+
return maxWeight * 1.01;
381+
}
382+
338383
/**
339384
* This method creates landmarks for the specified subnetwork (integer list)
340385
*
@@ -370,40 +415,14 @@ private boolean createLandmarksForSubnetwork(final int startNode, final byte[] s
370415
}
371416

372417
if (pickedPrecalculatedLandmarks) {
373-
LOGGER.info("Picked " + tmpLandmarkNodeIds.length + " landmark suggestions, skipped expensive landmark determination");
418+
LOGGER.info("Picked " + tmpLandmarkNodeIds.length + " landmark suggestions, skip finding landmarks");
374419
} else {
375-
// 1a) pick landmarks via special weighting for a better geographical spreading
376-
Weighting initWeighting = lmSelectionWeighting;
377-
LandmarkExplorer explorer = new LandmarkExplorer(graph, this, initWeighting, traversalMode, true);
378-
explorer.setStartNode(startNode);
379-
explorer.setFilter(blockedEdges, true, true);
380-
explorer.runAlgo();
381-
420+
LandmarkExplorer explorer = findLandmarks(tmpLandmarkNodeIds, startNode, blockedEdges, logOffset);
382421
if (explorer.getFromCount() < minimumNodes) {
383422
// too small subnetworks are initialized with special id==0
384423
explorer.setSubnetworks(subnetworks, UNCLEAR_SUBNETWORK);
385424
return false;
386425
}
387-
388-
// 1b) we have one landmark, now determine the other landmarks
389-
tmpLandmarkNodeIds[0] = explorer.getLastNode();
390-
for (int lmIdx = 0; lmIdx < tmpLandmarkNodeIds.length - 1; lmIdx++) {
391-
if (Thread.currentThread().isInterrupted()) {
392-
throw new RuntimeException("Thread was interrupted");
393-
}
394-
explorer = new LandmarkExplorer(graph, this, initWeighting, traversalMode, true);
395-
explorer.setFilter(blockedEdges, true, true);
396-
// set all current landmarks as start so that the next getLastNode is hopefully a "far away" node
397-
for (int j = 0; j < lmIdx + 1; j++) {
398-
explorer.setStartNode(tmpLandmarkNodeIds[j]);
399-
}
400-
explorer.runAlgo();
401-
tmpLandmarkNodeIds[lmIdx + 1] = explorer.getLastNode();
402-
if (logDetails && lmIdx % logOffset == 0)
403-
LOGGER.info("Finding landmarks [" + lmConfig + "] in network [" + explorer.getVisitedNodes() + "]. "
404-
+ "Progress " + (int) (100.0 * lmIdx / tmpLandmarkNodeIds.length) + "%, " + Helper.getMemInfo());
405-
}
406-
407426
if (logDetails)
408427
LOGGER.info("Finished searching landmarks for subnetwork " + subnetworkId + " of size " + explorer.getVisitedNodes());
409428
}
@@ -521,7 +540,7 @@ int getToWeight(int landmarkIndex, int node) {
521540
final boolean setWeight(long pointer, double value) {
522541
double tmpVal = value / factor;
523542
if (tmpVal > Integer.MAX_VALUE)
524-
throw new UnsupportedOperationException("Cannot store infinity explicitely, pointer=" + pointer + ", value: " + value);
543+
throw new UnsupportedOperationException("Cannot store infinity explicitly, pointer=" + pointer + ", value=" + value + ", factor=" + factor);
525544

526545
if (tmpVal >= SHORT_MAX) {
527546
landmarkWeightDA.setShort(pointer, (short) SHORT_MAX);
@@ -551,7 +570,8 @@ boolean chooseActiveLandmarks(int fromNode, int toNode, int[] activeLandmarkIndi
551570
if (subnetworkFrom <= UNCLEAR_SUBNETWORK || subnetworkTo <= UNCLEAR_SUBNETWORK)
552571
return false;
553572
if (subnetworkFrom != subnetworkTo) {
554-
throw new ConnectionNotFoundException("Connection between locations not found. Different subnetworks " + subnetworkFrom + " vs. " + subnetworkTo, new HashMap<String, Object>());
573+
throw new ConnectionNotFoundException("Connection between locations not found. Different subnetworks " + subnetworkFrom
574+
+ " vs. " + subnetworkTo, new HashMap<String, Object>());
555575
}
556576

557577
int[] tmpIDs = landmarkIDs.get(subnetworkFrom);
@@ -714,15 +734,46 @@ int getBaseNodes() {
714734
return graph.getNodes();
715735
}
716736

737+
private LandmarkExplorer findLandmarks(int[] landmarkNodeIdsToReturn, int startNode, IntHashSet blockedEdges, int logOffset) {
738+
// 1a) pick landmarks via special weighting for a better geographical spreading
739+
Weighting initWeighting = lmSelectionWeighting;
740+
LandmarkExplorer explorer = new LandmarkExplorer(graph, this, initWeighting, traversalMode, true);
741+
explorer.setStartNode(startNode);
742+
explorer.setFilter(blockedEdges, true, true);
743+
explorer.runAlgo();
744+
745+
if (explorer.getFromCount() >= minimumNodes) {
746+
// 1b) we have one landmark, now determine the other landmarks
747+
landmarkNodeIdsToReturn[0] = explorer.getLastEntry().adjNode;
748+
for (int lmIdx = 0; lmIdx < landmarkNodeIdsToReturn.length - 1; lmIdx++) {
749+
if (Thread.currentThread().isInterrupted()) {
750+
throw new RuntimeException("Thread was interrupted");
751+
}
752+
explorer = new LandmarkExplorer(graph, this, initWeighting, traversalMode, true);
753+
explorer.setFilter(blockedEdges, true, true);
754+
// set all current landmarks as start so that the next getLastNode is hopefully a "far away" node
755+
for (int j = 0; j < lmIdx + 1; j++) {
756+
explorer.setStartNode(landmarkNodeIdsToReturn[j]);
757+
}
758+
explorer.runAlgo();
759+
landmarkNodeIdsToReturn[lmIdx + 1] = explorer.getLastEntry().adjNode;
760+
if (logDetails && lmIdx % logOffset == 0)
761+
LOGGER.info("Finding landmarks [" + lmConfig + "] in network [" + explorer.getVisitedNodes() + "]. "
762+
+ "Progress " + (int) (100.0 * lmIdx / landmarkNodeIdsToReturn.length) + "%, " + Helper.getMemInfo());
763+
}
764+
}
765+
return explorer;
766+
}
767+
717768
/**
718769
* This class is used to calculate landmark location (equally distributed).
719770
* It derives from DijkstraBidirectionRef, but is only used as forward or backward search.
720771
*/
721772
private static class LandmarkExplorer extends DijkstraBidirectionRef {
722-
private int lastNode;
723773
// todo: rename 'from' to 'reverse' (and flip it) ? 'from' is used in many places for node ids and 'reverse' is mostly used for the direction
724774
private boolean from;
725775
private final LandmarkStorage lms;
776+
private SPTEntry lastEntry;
726777

727778
public LandmarkExplorer(Graph g, LandmarkStorage lms, Weighting weighting, TraversalMode tMode, boolean from) {
728779
super(g, weighting, tMode);
@@ -754,25 +805,23 @@ int getFromCount() {
754805
return bestWeightMapFrom.size();
755806
}
756807

757-
int getToCount() {
758-
return bestWeightMapTo.size();
759-
}
760-
761-
public int getLastNode() {
762-
return lastNode;
763-
}
764-
765808
public void runAlgo() {
766809
super.runAlgo();
767810
}
768811

812+
SPTEntry getLastEntry() {
813+
if (!finished())
814+
throw new IllegalStateException("Cannot get max weight if not yet finished");
815+
return lastEntry;
816+
}
817+
769818
@Override
770819
public boolean finished() {
771820
if (from) {
772-
lastNode = currFrom.adjNode;
821+
lastEntry = currFrom;
773822
return finishedFrom;
774823
} else {
775-
lastNode = currTo.adjNode;
824+
lastEntry = currTo;
776825
return finishedTo;
777826
}
778827
}

core/src/test/java/com/graphhopper/routing/lm/PrepareLandmarksTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public void testLandmarkStorageAndRouting() {
157157

158158
assertEquals(expectedPath.getWeight(), path.getWeight(), .1);
159159
assertEquals(expectedPath.calcNodes(), path.calcNodes());
160-
assertEquals(expectedAlgo.getVisitedNodes() - 133, oneDirAlgoWithLandmarks.getVisitedNodes());
160+
assertEquals(expectedAlgo.getVisitedNodes() - 135, oneDirAlgoWithLandmarks.getVisitedNodes());
161161

162162
// landmarks with bidir A*
163163
RoutingAlgorithm biDirAlgoWithLandmarks = prepare.getRoutingAlgorithmFactory().createAlgo(graph,
@@ -180,7 +180,7 @@ public void testLandmarkStorageAndRouting() {
180180
expectedPath = expectedAlgo.calcPath(fromQR.getClosestNode(), toQR.getClosestNode());
181181
assertEquals(expectedPath.getWeight(), path.getWeight(), .1);
182182
assertEquals(expectedPath.calcNodes(), path.calcNodes());
183-
assertEquals(expectedAlgo.getVisitedNodes() - 133, qGraphOneDirAlgo.getVisitedNodes());
183+
assertEquals(expectedAlgo.getVisitedNodes() - 135, qGraphOneDirAlgo.getVisitedNodes());
184184
}
185185

186186
@Test
@@ -202,7 +202,7 @@ public void testStoreAndLoad() {
202202
assertEquals(Arrays.toString(new int[]{
203203
2, 0
204204
}), Arrays.toString(plm.getLandmarkStorage().getLandmarks(1)));
205-
assertEquals(4791, Math.round(plm.getLandmarkStorage().getFromWeight(0, 1) * expectedFactor));
205+
assertEquals(4800, Math.round(plm.getLandmarkStorage().getFromWeight(0, 1) * expectedFactor));
206206

207207
dir = new RAMDirectory(fileStr, true);
208208
plm = new PrepareLandmarks(dir, graph, lmConfig, 2);
@@ -211,7 +211,7 @@ public void testStoreAndLoad() {
211211
assertEquals(Arrays.toString(new int[]{
212212
2, 0
213213
}), Arrays.toString(plm.getLandmarkStorage().getLandmarks(1)));
214-
assertEquals(4791, Math.round(plm.getLandmarkStorage().getFromWeight(0, 1) * expectedFactor));
214+
assertEquals(4800, Math.round(plm.getLandmarkStorage().getFromWeight(0, 1) * expectedFactor));
215215

216216
Helper.removeDir(new File(fileStr));
217217
}

reader-osm/src/test/java/com/graphhopper/GraphHopperIT.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,13 +1408,13 @@ public void testCrossQuery() {
14081408
testCrossQueryAssert(profile3, hopper, 815.4, 146, true);
14091409

14101410
// LM (should be the same as flex, but with less visited nodes!)
1411-
testCrossQueryAssert(profile1, hopper, 528.3, 106, false);
1412-
testCrossQueryAssert(profile2, hopper, 636.0, 78, false);
1411+
testCrossQueryAssert(profile1, hopper, 528.3, 74, false);
1412+
testCrossQueryAssert(profile2, hopper, 636.0, 84, false);
14131413
// this is actually interesting: the number of visited nodes *increases* once again (while it strictly decreases
14141414
// with rising distance factor for flex): cross-querying 'works', but performs *worse*, because the landmarks
14151415
// were not customized for the weighting in use. Creating a separate LM preparation for profile3 yields 74
14161416
// instead of 124 visited nodes (not shown here)
1417-
testCrossQueryAssert(profile3, hopper, 815.4, 124, false);
1417+
testCrossQueryAssert(profile3, hopper, 815.4, 128, false);
14181418
}
14191419

14201420
private void testCrossQueryAssert(String profile, GraphHopper hopper, double expectedWeight, int expectedVisitedNodes, boolean disableLM) {

0 commit comments

Comments
 (0)