5656import java .nio .file .StandardCopyOption ;
5757import java .time .Duration ;
5858import java .util .ArrayList ;
59+ import java .util .Arrays ;
5960import java .util .List ;
6061import java .util .Locale ;
6162import java .util .Random ;
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 < 3 km (walking, cycling)</li>
417+ * <li>~50% are < 6 km</li>
418+ * <li>~80% are < 15 km</li>
419+ * <li>~95% are < 40 km</li>
420+ * <li>~99% are < 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