Skip to content

Commit 794e108

Browse files
committed
refactor: enhance OD pair generation with new modes and statistics
1 parent 24fa8d8 commit 794e108

1 file changed

Lines changed: 230 additions & 15 deletions

File tree

benchmark/src/main/java/org/matsim/benchmark/RouterBenchmark.java

Lines changed: 230 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import java.nio.file.StandardCopyOption;
5757
import java.time.Duration;
5858
import java.util.ArrayList;
59+
import java.util.Arrays;
5960
import java.util.List;
6061
import java.util.Locale;
6162
import java.util.Random;
@@ -76,11 +77,20 @@
7677
* <li>CH Time-Dependent (Z-order)</li>
7778
* </ol>
7879
*
80+
* <p>OD pair generation modes:
81+
* <ul>
82+
* <li><b>mid</b> (default) — Distance-weighted sampling inspired by
83+
* MiD 2017 (Mobilität in Deutschland). Beeline distances follow a
84+
* log-normal distribution (median ≈ 5 km, mean ≈ 10 km).</li>
85+
* <li><b>random</b> — Uniform random origin and destination nodes.</li>
86+
* </ul>
87+
*
7988
* <p>Run with sufficient heap, e.g.:
8089
* <pre>
8190
* java -Xmx8G -cp ... org.matsim.benchmark.RouterBenchmark \
8291
* [--network path/to/network.xml.gz | berlin | duesseldorf] \
83-
* [--queries 2000] [--warmup 200] [--landmarks 16]
92+
* [--queries 2000] [--warmup 200] [--landmarks 16] \
93+
* [--od-mode mid|random]
8494
* </pre>
8595
*
8696
* <p>If {@code --network} is omitted, the Berlin v7.0 network is used.
@@ -100,9 +110,38 @@ public class RouterBenchmark {
100110
private static int warmupQueries = 200;
101111
private static int benchmarkQueries = 2_000;
102112
private static int altLandmarks = 16;
113+
private static String odMode = "mid";
103114

104115
record RouterEntry(String name, LeastCostPathCalculator router) {}
105116

117+
/**
118+
* Result of OD pair generation, including beeline distance statistics.
119+
*/
120+
record ODPairResult(int[][] pairs, double[] beelineDistances, double metersPerUnit) {
121+
double medianKm() { return percentileKm(50); }
122+
double meanKm() {
123+
double sum = 0;
124+
for (double d : beelineDistances) sum += d;
125+
return (sum / beelineDistances.length) * metersPerUnit / 1000.0;
126+
}
127+
double percentileKm(int p) {
128+
double[] sorted = beelineDistances.clone();
129+
Arrays.sort(sorted);
130+
int idx = Math.min((int) (sorted.length * p / 100.0), sorted.length - 1);
131+
return sorted[idx] * metersPerUnit / 1000.0;
132+
}
133+
double minKm() {
134+
double min = Double.MAX_VALUE;
135+
for (double d : beelineDistances) min = Math.min(min, d);
136+
return min * metersPerUnit / 1000.0;
137+
}
138+
double maxKm() {
139+
double max = 0;
140+
for (double d : beelineDistances) max = Math.max(max, d);
141+
return max * metersPerUnit / 1000.0;
142+
}
143+
}
144+
106145
public static void main(String[] args) {
107146
Locale.setDefault(Locale.US);
108147

@@ -119,15 +158,23 @@ public static void main(String[] args) {
119158
case "--queries" -> benchmarkQueries = Integer.parseInt(args[++i]);
120159
case "--warmup" -> warmupQueries = Integer.parseInt(args[++i]);
121160
case "--landmarks" -> altLandmarks = Integer.parseInt(args[++i]);
161+
case "--od-mode" -> odMode = args[++i].toLowerCase(Locale.ROOT);
122162
default -> {
123163
System.err.println("Unknown argument: " + args[i]);
124164
System.err.println("Usage: java ... RouterBenchmark "
125-
+ "[--network <path|berlin|duesseldorf>] [--queries <n>] [--warmup <n>] [--landmarks <n>]");
165+
+ "[--network <path|berlin|duesseldorf>] [--queries <n>] [--warmup <n>] "
166+
+ "[--landmarks <n>] [--od-mode mid|random]");
126167
System.exit(1);
127168
}
128169
}
129170
}
130171

172+
if (!odMode.equals("mid") && !odMode.equals("random")) {
173+
System.err.println("ERROR: --od-mode must be 'mid' or 'random' (got '" + odMode + "')");
174+
System.exit(1);
175+
}
176+
177+
// ...existing code... (heap check, network loading, graph building, CH/ALT build)
131178
long maxHeapMB = Runtime.getRuntime().maxMemory() / (1024 * 1024);
132179
System.out.printf("JVM max heap: %,d MB%n", maxHeapMB);
133180
if (maxHeapMB < 3000) {
@@ -179,21 +226,31 @@ public static void main(String[] args) {
179226
System.out.println();
180227
System.out.println("Building SpeedyALT landmarks ...");
181228
long altStart = System.nanoTime();
182-
SpeedyALTData altData = new SpeedyALTData(graph, Math.min(effectiveLandmarks, graph.getNodeCount()), tc, 1);
229+
int altThreads = Runtime.getRuntime().availableProcessors();
230+
SpeedyALTData altData = new SpeedyALTData(graph, Math.min(effectiveLandmarks, graph.getNodeCount()), tc, altThreads);
183231
long altMs = (System.nanoTime() - altStart) / 1_000_000;
184-
System.out.printf(" ALT build: %,6d ms (%d landmarks)%n", altMs, effectiveLandmarks);
232+
System.out.printf(" ALT build: %,6d ms (%d landmarks, %d threads)%n", altMs, effectiveLandmarks, altThreads);
185233

186234
List<RouterEntry> routers = buildRouters(altData, chGraph, tc);
187235

188236
List<Node> nodeList = new ArrayList<>(network.getNodes().values());
189-
int n = nodeList.size();
237+
238+
// ---- Generate OD pairs ----
239+
System.out.println();
240+
System.out.printf("Generating OD pairs (mode=%s) ...%n", odMode);
241+
ODPairResult warmupOD = generateODPairs(nodeList, warmupQueries, new Random(42), odMode);
242+
ODPairResult benchOD = generateODPairs(nodeList, benchmarkQueries, new Random(123), odMode);
243+
244+
System.out.printf(" Benchmark beeline distances: min=%.1f km, median=%.1f km, mean=%.1f km, "
245+
+ "P90=%.1f km, P95=%.1f km, max=%.1f km%n",
246+
benchOD.minKm(), benchOD.medianKm(), benchOD.meanKm(),
247+
benchOD.percentileKm(90), benchOD.percentileKm(95), benchOD.maxKm());
190248

191249
System.out.println();
192250
System.out.printf("Warming up (%d queries per router) ...%n", warmupQueries);
193-
Random rng = new Random(42);
194251
for (int i = 0; i < warmupQueries; i++) {
195-
Node s = nodeList.get(rng.nextInt(n));
196-
Node d = nodeList.get(rng.nextInt(n));
252+
Node s = nodeList.get(warmupOD.pairs()[i][0]);
253+
Node d = nodeList.get(warmupOD.pairs()[i][1]);
197254
double depTime = 8.0 * 3600;
198255
for (RouterEntry entry : routers) {
199256
entry.router().calcLeastCostPath(s, d, depTime, null, null);
@@ -208,12 +265,7 @@ public static void main(String[] args) {
208265
chRouter.resetStats();
209266

210267
System.out.printf("Running benchmark (%,d queries per router) ...%n", benchmarkQueries);
211-
rng = new Random(123);
212-
int[][] pairs = new int[benchmarkQueries][2];
213-
for (int i = 0; i < benchmarkQueries; i++) {
214-
pairs[i][0] = rng.nextInt(n);
215-
pairs[i][1] = rng.nextInt(n);
216-
}
268+
int[][] pairs = benchOD.pairs();
217269

218270
long[] elapsedNs = new long[routers.size()];
219271
for (int r = 0; r < routers.size(); r++) {
@@ -262,6 +314,14 @@ public static void main(String[] args) {
262314
resultRows.add(new String[]{ " Est. diameter", String.valueOf(profile.estimatedDiameter()) });
263315
resultRows.add(new String[]{ " Components", String.valueOf(profile.connectedComponents()) });
264316
resultRows.add(null);
317+
resultRows.add(new String[]{ "OD Pairs", String.format("(mode=%s)", odMode) });
318+
resultRows.add(new String[]{ " Beeline min", String.format("%.1f km", benchOD.minKm()) });
319+
resultRows.add(new String[]{ " Beeline median", String.format("%.1f km", benchOD.medianKm()) });
320+
resultRows.add(new String[]{ " Beeline mean", String.format("%.1f km", benchOD.meanKm()) });
321+
resultRows.add(new String[]{ " Beeline P90", String.format("%.1f km", benchOD.percentileKm(90)) });
322+
resultRows.add(new String[]{ " Beeline P95", String.format("%.1f km", benchOD.percentileKm(95)) });
323+
resultRows.add(new String[]{ " Beeline max", String.format("%.1f km", benchOD.maxKm()) });
324+
resultRows.add(null);
265325
resultRows.add(new String[]{ "Preprocessing" });
266326
resultRows.add(new String[]{ " ND Order", String.format("%,d ms", orderMs) });
267327
resultRows.add(new String[]{ " CH build", String.format("%,d ms (incl. order)", totalBuildMs) });
@@ -289,7 +349,6 @@ public static void main(String[] args) {
289349
double avgBwd = (double) chRouter.getTotalBwdSettled() / chQueries;
290350
double avgTotal = avgFwd + avgBwd;
291351
double searchSpaceRatio = avgTotal / nodeCount * 100.0;
292-
// Approximate Dijkstra speedup: Dijkstra settles all nodeCount nodes
293352
double dijkstraSpeedup = nodeCount / Math.max(1, avgTotal);
294353
resultRows.add(null);
295354
resultRows.add(new String[]{ "CH Quality (search space, lower = better)" });
@@ -304,6 +363,160 @@ public static void main(String[] args) {
304363
printBox("Routing Benchmark — ALT vs CH Comparison", resultRows.toArray(String[][]::new));
305364
}
306365

366+
// ---- OD pair generation ----
367+
368+
/**
369+
* Generates OD pairs according to the selected mode.
370+
*
371+
* @param nodeList all network nodes
372+
* @param count number of pairs to generate
373+
* @param rng random number generator (seeded for reproducibility)
374+
* @param mode "mid" for MiD-inspired distribution, "random" for uniform
375+
* @return OD pairs with beeline distance statistics
376+
*/
377+
private static ODPairResult generateODPairs(List<Node> nodeList, int count, Random rng, String mode) {
378+
return switch (mode) {
379+
case "mid" -> generateMiDPairs(nodeList, count, rng);
380+
case "random" -> generateRandomPairs(nodeList, count, rng);
381+
default -> throw new IllegalArgumentException("Unknown OD mode: " + mode);
382+
};
383+
}
384+
385+
/**
386+
* Generates uniform random OD pairs (original behavior).
387+
*/
388+
private static ODPairResult generateRandomPairs(List<Node> nodeList, int count, Random rng) {
389+
int n = nodeList.size();
390+
double metersPerUnit = detectMetersPerUnit(nodeList);
391+
392+
int[][] pairs = new int[count][2];
393+
double[] distances = new double[count];
394+
for (int i = 0; i < count; i++) {
395+
int o = rng.nextInt(n);
396+
int d = rng.nextInt(n);
397+
pairs[i][0] = o;
398+
pairs[i][1] = d;
399+
distances[i] = beeline(nodeList.get(o), nodeList.get(d));
400+
}
401+
return new ODPairResult(pairs, distances, metersPerUnit);
402+
}
403+
404+
/**
405+
* Generates OD pairs with a distance distribution inspired by
406+
* <b>MiD 2017</b> (Mobilität in Deutschland).
407+
*
408+
* <p>Beeline distances are sampled from a <b>log-normal distribution</b>:
409+
* <ul>
410+
* <li>Median ≈ 5 km (μ = ln(5000) ≈ 8.52 in meters)</li>
411+
* <li>σ = 1.1 → Mean ≈ 9.3 km, P90 ≈ 21 km, P99 ≈ 72 km</li>
412+
* </ul>
413+
*
414+
* <p>This approximates the MiD 2017 findings where:
415+
* <ul>
416+
* <li>~30% of trips are &lt; 3 km (walking, cycling)</li>
417+
* <li>~50% are &lt; 6 km</li>
418+
* <li>~80% are &lt; 15 km</li>
419+
* <li>~95% are &lt; 40 km</li>
420+
* <li>~99% are &lt; 100 km</li>
421+
* </ul>
422+
*
423+
* <p>For each pair, a random origin is selected and a destination is found
424+
* by picking the best match among 50 random candidates whose beeline
425+
* distance is closest to the sampled target distance. This is efficient
426+
* (no spatial index needed) and produces a smooth distribution.
427+
*
428+
* @see <a href="https://www.mobilitaet-in-deutschland.de/">MiD 2017</a>
429+
*/
430+
private static ODPairResult generateMiDPairs(List<Node> nodeList, int count, Random rng) {
431+
int n = nodeList.size();
432+
double metersPerUnit = detectMetersPerUnit(nodeList);
433+
434+
// Log-normal parameters (in coordinate units):
435+
// median = 5000 m / metersPerUnit
436+
double medianCU = 5_000.0 / metersPerUnit;
437+
double muLn = Math.log(medianCU);
438+
double sigmaLn = 1.1;
439+
440+
// Network extent for clamping
441+
double diagonal = networkDiagonal(nodeList);
442+
double minDist = 200.0 / metersPerUnit; // minimum 200 m beeline
443+
double maxDist = diagonal * 0.9; // cap at 90% of diagonal
444+
445+
int candidates = 50; // candidates per OD pair for distance matching
446+
447+
int[][] pairs = new int[count][2];
448+
double[] distances = new double[count];
449+
450+
for (int i = 0; i < count; i++) {
451+
int origin = rng.nextInt(n);
452+
453+
// Sample target beeline distance from log-normal
454+
double targetDist = Math.exp(muLn + sigmaLn * rng.nextGaussian());
455+
targetDist = Math.max(minDist, Math.min(maxDist, targetDist));
456+
457+
// Find best matching destination among candidates
458+
int bestDest = (origin + 1) % n; // fallback
459+
double bestDiff = Double.MAX_VALUE;
460+
for (int c = 0; c < candidates; c++) {
461+
int dest = rng.nextInt(n);
462+
double dist = beeline(nodeList.get(origin), nodeList.get(dest));
463+
double diff = Math.abs(dist - targetDist);
464+
if (diff < bestDiff) {
465+
bestDiff = diff;
466+
bestDest = dest;
467+
}
468+
}
469+
470+
pairs[i][0] = origin;
471+
pairs[i][1] = bestDest;
472+
distances[i] = beeline(nodeList.get(origin), nodeList.get(bestDest));
473+
}
474+
475+
return new ODPairResult(pairs, distances, metersPerUnit);
476+
}
477+
478+
/**
479+
* Detects whether coordinates are in meters (projected CRS like UTM) or
480+
* degrees (WGS84) by examining the bounding box diagonal.
481+
*
482+
* @return approximate meters per coordinate unit
483+
*/
484+
private static double detectMetersPerUnit(List<Node> nodeList) {
485+
double diagonal = networkDiagonal(nodeList);
486+
// If diagonal > 10 km worth of units, assume meters.
487+
// Typical German UTM network: diagonal 50k-900k (meters).
488+
// WGS84 for Germany: diagonal ~5-12 (degrees).
489+
if (diagonal > 10_000) {
490+
return 1.0; // already in meters
491+
} else if (diagonal > 100) {
492+
return 100.0; // some intermediate CRS (rare)
493+
} else {
494+
return 111_000.0; // degrees → approximate meters at ~50°N
495+
}
496+
}
497+
498+
private static double networkDiagonal(List<Node> nodeList) {
499+
double minX = Double.MAX_VALUE, maxX = -Double.MAX_VALUE;
500+
double minY = Double.MAX_VALUE, maxY = -Double.MAX_VALUE;
501+
for (Node node : nodeList) {
502+
double x = node.getCoord().getX();
503+
double y = node.getCoord().getY();
504+
if (x < minX) minX = x;
505+
if (x > maxX) maxX = x;
506+
if (y < minY) minY = y;
507+
if (y > maxY) maxY = y;
508+
}
509+
return Math.sqrt((maxX - minX) * (maxX - minX) + (maxY - minY) * (maxY - minY));
510+
}
511+
512+
private static double beeline(Node a, Node b) {
513+
double dx = a.getCoord().getX() - b.getCoord().getX();
514+
double dy = a.getCoord().getY() - b.getCoord().getY();
515+
return Math.sqrt(dx * dx + dy * dy);
516+
}
517+
518+
// ---- Benchmarking ----
519+
307520
private static long benchmarkRouter(LeastCostPathCalculator router,
308521
List<Node> nodeList, int[][] pairs, String label) {
309522
int total = pairs.length;
@@ -325,6 +538,8 @@ private static long benchmarkRouter(LeastCostPathCalculator router,
325538
return elapsedNs;
326539
}
327540

541+
// ---- Network loading ----
542+
328543
private static Network loadNetwork(String networkPath) {
329544
if (networkPath == null || "berlin".equalsIgnoreCase(networkPath)) {
330545
return downloadAndLoadNetwork(BERLIN_NETWORK_URL, "berlin-v7.0-network.xml.gz", "Berlin v7.0");

0 commit comments

Comments
 (0)