scgraph 3.4.1__tar.gz → 3.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {scgraph-3.4.1 → scgraph-3.5.0}/CMakeLists.txt +1 -0
  2. {scgraph-3.4.1 → scgraph-3.5.0}/PKG-INFO +83 -7
  3. {scgraph-3.4.1 → scgraph-3.5.0}/README.md +82 -6
  4. {scgraph-3.4.1 → scgraph-3.5.0}/pyproject.toml +1 -1
  5. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/__init__.py +87 -7
  6. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/contraction_hierarchies.py +297 -118
  7. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/cpp/bindings/graph_bindings.cpp +117 -12
  8. scgraph-3.5.0/scgraph/cpp/src/contraction_hierarchies.cpp +494 -0
  9. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/cpp/src/contraction_hierarchies.hpp +24 -6
  10. scgraph-3.5.0/scgraph/cpp/src/graph.cpp +778 -0
  11. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/cpp/src/graph.hpp +14 -6
  12. scgraph-3.5.0/scgraph/cpp/src/graph_reducer.cpp +402 -0
  13. scgraph-3.5.0/scgraph/cpp/src/graph_reducer.hpp +54 -0
  14. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/cpp/src/graph_utils.cpp +17 -3
  15. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/cpp/src/graph_utils.hpp +3 -1
  16. scgraph-3.5.0/scgraph/cpp/src/transit_node_routing.cpp +486 -0
  17. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/cpp/src/transit_node_routing.hpp +11 -2
  18. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/geograph.py +189 -7
  19. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/graph.py +277 -60
  20. scgraph-3.5.0/scgraph/graph_reducer.py +531 -0
  21. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/graph_utils.py +98 -52
  22. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/transit_node_routing.py +188 -80
  23. scgraph-3.4.1/scgraph/cpp/src/contraction_hierarchies.cpp +0 -316
  24. scgraph-3.4.1/scgraph/cpp/src/graph.cpp +0 -472
  25. scgraph-3.4.1/scgraph/cpp/src/transit_node_routing.cpp +0 -290
  26. {scgraph-3.4.1 → scgraph-3.5.0}/LICENSE +0 -0
  27. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/cpp/src/bmssp.hpp +0 -0
  28. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/grid.py +0 -0
  29. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/helpers/__init__.py +0 -0
  30. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/helpers/geojson.py +0 -0
  31. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/helpers/visvalingam.py +0 -0
  32. {scgraph-3.4.1 → scgraph-3.5.0}/scgraph/utils.py +0 -0
@@ -72,6 +72,7 @@ else()
72
72
  scgraph/cpp/bindings/graph_bindings.cpp
73
73
  scgraph/cpp/src/graph.cpp
74
74
  scgraph/cpp/src/graph_utils.cpp
75
+ scgraph/cpp/src/graph_reducer.cpp
75
76
  scgraph/cpp/src/contraction_hierarchies.cpp
76
77
  scgraph/cpp/src/transit_node_routing.cpp
77
78
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scgraph
3
- Version: 3.4.1
3
+ Version: 3.5.0
4
4
  Summary: Determine an approximate distance and route between two points on earth.
5
5
  Author-Email: Connor Makowski <conmak@mit.edu>
6
6
  License-Expression: MIT
@@ -35,7 +35,7 @@ Description-Content-Type: text/markdown
35
35
 
36
36
  ![scgraph](https://raw.githubusercontent.com/connor-makowski/scgraph/main/static/scgraph.png)
37
37
 
38
- SCGraph provides fast, flexible shortest path routing for road, rail, maritime, and custom networks. It ships with prebuilt geographic networks, supports arbitrary graph construction and OSMNx integration, and scales from simple point-to-point queries to massive distance matrices. It also supports optional C++ acceleration and Contraction Hierarchy preprocessing for large-scale applications.
38
+ SCGraph provides fast, flexible shortest path routing for road, rail, maritime, and custom networks. It ships with prebuilt geographic networks, supports arbitrary graph construction and OSMNx integration, and scales from simple point-to-point queries to massive distance matrices. It also supports graph reduction, lazy loading, optional C++ acceleration, and Contraction Hierarchy preprocessing for large-scale applications.
39
39
 
40
40
  - **Docs**: https://connor-makowski.github.io/scgraph/scgraph.html
41
41
  - **Paper**: https://ssrn.com/abstract=5388845
@@ -201,6 +201,7 @@ All algorithms are available on `Graph` objects and accessible from `GeoGraph` v
201
201
  | `algorithm_fn` | Description | Time Complexity |
202
202
  |---|---|---|
203
203
  | `'dijkstra'` | Standard Dijkstra; general purpose, non-negative weights (default) | O((n+m) log n) |
204
+ | `'bidirectional_dijkstra'` | Bidirectional Dijkstra; simultaneous search from origin and destination | O((n+m) log n) |
204
205
  | `'dijkstra_buckets'` | Dijkstra with buckets (Dial's algorithm); efficient for non-negative weights (ideally >= 1) | O(n+m+W) |
205
206
  | `'dijkstra_negative'` | Dijkstra with cycle detection; supports negative weights | O(n·m) |
206
207
  | `'a_star'` | A* with optional heuristic; faster than Dijkstra with a good heuristic | O((n+m) log n) |
@@ -214,12 +215,14 @@ All algorithms are available on `Graph` objects and accessible from `GeoGraph` v
214
215
 
215
216
  | Scenario | Recommended Approach |
216
217
  |---|---|
217
- | Single query | `dijkstra` (default) |
218
+ | Single query | `dijkstra` (default) or `bidirectional_dijkstra` |
219
+ | Sparse networks (road/rail/maritime) | Reduce with `geograph.reduce()` for 2–4x faster queries |
218
220
  | Weights generally >= 1 | `dijkstra_buckets` |
219
221
  | Repeated queries from one origin | `cached_shortest_path` |
220
222
  | Large distance matrix (same graph) | `distance_matrix` method |
221
223
  | Many arbitrary queries on a fixed graph | `contraction_hierarchy` or `tnr` |
222
224
  | Graph with negative weights | `dijkstra_negative` |
225
+ | Fast app startup / multi-network loading | `GeoGraph.load_geograph(..., lazy=True)` |
223
226
 
224
227
  ## Heuristic Functions (for A*)
225
228
 
@@ -297,14 +300,14 @@ from scgraph import GeoGraph
297
300
 
298
301
  marnet_geograph = GeoGraph.load_geograph("marnet")
299
302
 
300
- # First call: computes and caches the shortest path tree (~same cost as dijkstra)
303
+ # First call: computes and caches the shortest path tree (~same cost as dijkstra without an early termination)
301
304
  output1 = marnet_geograph.get_shortest_path(
302
305
  origin_node={"latitude": 31.23, "longitude": 121.47}, # Shanghai
303
306
  destination_node={"latitude": 32.08, "longitude": -81.09}, # Savannah, GA
304
307
  algorithm_fn='cached_shortest_path',
305
308
  )
306
309
 
307
- # Subsequent calls to the same origin are near-instant
310
+ # Subsequent calls to the same origin are near-instant (O(1) lookup)
308
311
  output2 = marnet_geograph.get_shortest_path(
309
312
  origin_node={"latitude": 31.23, "longitude": 121.47}, # Shanghai (same)
310
313
  destination_node={"latitude": 51.50, "longitude": -0.13}, # London
@@ -339,6 +342,53 @@ matrix = us_freeway_geograph.distance_matrix(cities, output_units='km')
339
342
 
340
343
  For large matrices, throughput can approach 500 nanoseconds per distance query.
341
344
 
345
+ ## Graph Reduction
346
+
347
+ Sparse networks (highways, rail lines, shipping corridors) often contain long chains of intermediate nodes with only two connections (degree 2). Graph reduction compresses these chains into single composite edges while preserving exact distances and full path reconstruction:
348
+
349
+ ```py
350
+ from scgraph import GeoGraph
351
+
352
+ us_freeway = GeoGraph.load_geograph("us_freeway")
353
+
354
+ # Reduce the graph by bypassing pass-through chains (e.g. simplifies ~88% of nodes on us_freeway)
355
+ us_freeway.reduce(iterations=1)
356
+
357
+ # All standard algorithms (dijkstra, bidirectional_dijkstra, a_star, etc.)
358
+ # automatically execute on the reduced graph and expand paths back to original coordinates:
359
+ output = us_freeway.get_shortest_path(
360
+ origin_node={"latitude": 34.05, "longitude": -118.24}, # Los Angeles
361
+ destination_node={"latitude": 40.71, "longitude": -74.01}, # New York City
362
+ algorithm_fn='bidirectional_dijkstra',
363
+ )
364
+ print(output['length'])
365
+ ```
366
+
367
+ Key features of graph reduction:
368
+ - **Zero inaccuracy**: Paths and lengths are guaranteed to be mathematically identical to the unreduced graph.
369
+ - **Same-chain routing**: If origin and destination fall on the same contracted chain, the original graph is used with the standard dijkstra algorithm.
370
+ - **Iterative reduction**: Pass `iterations=-1` to reduce iteratively until full convergence (simplifying sub-chains created after initial contraction).
371
+
372
+ ## Lazy Loading
373
+
374
+ To avoid upfront downloading, parsing, or memory overhead when defining multiple networks or optimizing startup latency, use `lazy=True`:
375
+
376
+ ```py
377
+ from scgraph import GeoGraph
378
+
379
+ # Returns a LazyGeoGraph proxy immediately without downloading or parsing
380
+ world_net = GeoGraph.load_geograph("world_highways_and_marnet", lazy=True)
381
+
382
+ # The network is downloaded and instantiated only when first queried:
383
+ output = world_net.get_shortest_path(
384
+ origin_node={"latitude": 31.23, "longitude": 121.47},
385
+ destination_node={"latitude": 32.08, "longitude": -81.09},
386
+ )
387
+
388
+ # Or explicitly load the graph at any time:
389
+ world_net.load()
390
+ ```
391
+
342
392
  ## Node Addition Options
343
393
 
344
394
  Control how origin/destination are connected to the graph:
@@ -376,7 +426,7 @@ output = marnet_geograph.get_shortest_path(
376
426
 
377
427
  ## Built-in Geographs: Cache Management
378
428
 
379
- Built-in geographs are downloaded from GitHub on first use and stored in a local cache. Subsequent loads are instant and require no network access.
429
+ Built-in geographs are downloaded from GitHub on first use and stored in a local cache. Subsequent loads only require reading from the local cache and do not require network access.
380
430
 
381
431
  ```py
382
432
  from scgraph import GeoGraph
@@ -411,6 +461,16 @@ The cache location defaults to the platform-appropriate directory:
411
461
  | macOS | `~/Library/Caches/scgraph/` |
412
462
  | Windows | `%LOCALAPPDATA%\scgraph\` |
413
463
 
464
+ ### `load_geograph` Parameters
465
+
466
+ | Parameter | Default | Description |
467
+ |---|---|---|
468
+ | `name` | required | Name of the built-in geograph (e.g. `'marnet'`, `'us_freeway'`) |
469
+ | `cache_dir` | `None` | Custom directory for downloaded files; defaults to the OS cache dir above |
470
+ | `geograph_url` | `'https://raw.githubusercontent.com/...'` | Base URL for fetching geographs |
471
+ | `reduce_iterations` | `0` | Number of reduction iterations to perform upon loading (`0` = none, `1` = single pass, `-1` = full convergence) |
472
+ | `lazy` | `False` | Defer loading/fetching until the geograph is first accessed |
473
+
414
474
  ## Loading from OSMNx
415
475
 
416
476
  SCGraph integrates directly with [OSMNx](https://osmnx.readthedocs.io/) — load any OpenStreetMap network and convert it to a `GeoGraph` in two lines:
@@ -445,6 +505,8 @@ print(output['length'])
445
505
  | `off_graph_travel_speed` | `None` | Speed (km/h) for off-graph connections; used to convert time-based weights to distances |
446
506
  | `load_intermediate_nodes` | `True` | Load intermediate shape points for accurate path visualization |
447
507
  | `silent` | `False` | Suppress progress output |
508
+ | `reduce_iterations` | `0` | Number of reduction iterations to perform upon loading (`0` = none, `1` = single pass, `-1` = full convergence) |
509
+ | `lazy` | `False` | Defer loading until the geograph is first accessed |
448
510
 
449
511
  ## Building from OSM Data (Without OSMNx)
450
512
 
@@ -547,6 +609,20 @@ graph.validate()
547
609
 
548
610
  output = graph.dijkstra(origin_id=0, destination_id=5)
549
611
  print(output) #=> {'path': [0, 2, 1, 3, 5], 'length': 10}
612
+
613
+ # Reduce the graph (contracts pass-through nodes):
614
+ graph.reduce(iterations=1)
615
+
616
+ # Custom algorithm decorator with automated reduced graph support:
617
+ @Graph.algorithm
618
+ def custom_solver(self, origin_id: int, destination_id: int) -> dict:
619
+ # Standard single-sided algorithm implementation using self.graph
620
+ ...
621
+
622
+ @Graph.algorithm(bidirectional=True)
623
+ def custom_bidirectional_solver(self, origin_id: int, destination_id: int) -> dict:
624
+ # Bidirectional algorithm using self.graph and self.inverse_graph
625
+ ...
550
626
  ```
551
627
 
552
628
  ## Custom GeoGraph
@@ -755,7 +831,7 @@ uv sync --extra dev
755
831
  | `uv run pytest test/NN_*.py` | Run a specific test file |
756
832
  | `uv run nox` | Run tests (C++ then no-C++) across Python 3.11–3.14 |
757
833
  | `uv run nox -s tests-3.14` | Run both build variants on a single Python version |
758
- | `uv run utils/benchmark.py` | Run all benchmarks, output `benchmark_results.json` |
834
+ | `uv run utils/bench.py` | Run benchmarks (<30s), output `benchmark.md` |
759
835
  | `uv run utils/prettify.py` | Format with autoflake + black |
760
836
 
761
837
  For full developer documentation see [DEVELOPMENT.md](DEVELOPMENT.md).
@@ -7,7 +7,7 @@
7
7
 
8
8
  ![scgraph](https://raw.githubusercontent.com/connor-makowski/scgraph/main/static/scgraph.png)
9
9
 
10
- SCGraph provides fast, flexible shortest path routing for road, rail, maritime, and custom networks. It ships with prebuilt geographic networks, supports arbitrary graph construction and OSMNx integration, and scales from simple point-to-point queries to massive distance matrices. It also supports optional C++ acceleration and Contraction Hierarchy preprocessing for large-scale applications.
10
+ SCGraph provides fast, flexible shortest path routing for road, rail, maritime, and custom networks. It ships with prebuilt geographic networks, supports arbitrary graph construction and OSMNx integration, and scales from simple point-to-point queries to massive distance matrices. It also supports graph reduction, lazy loading, optional C++ acceleration, and Contraction Hierarchy preprocessing for large-scale applications.
11
11
 
12
12
  - **Docs**: https://connor-makowski.github.io/scgraph/scgraph.html
13
13
  - **Paper**: https://ssrn.com/abstract=5388845
@@ -173,6 +173,7 @@ All algorithms are available on `Graph` objects and accessible from `GeoGraph` v
173
173
  | `algorithm_fn` | Description | Time Complexity |
174
174
  |---|---|---|
175
175
  | `'dijkstra'` | Standard Dijkstra; general purpose, non-negative weights (default) | O((n+m) log n) |
176
+ | `'bidirectional_dijkstra'` | Bidirectional Dijkstra; simultaneous search from origin and destination | O((n+m) log n) |
176
177
  | `'dijkstra_buckets'` | Dijkstra with buckets (Dial's algorithm); efficient for non-negative weights (ideally >= 1) | O(n+m+W) |
177
178
  | `'dijkstra_negative'` | Dijkstra with cycle detection; supports negative weights | O(n·m) |
178
179
  | `'a_star'` | A* with optional heuristic; faster than Dijkstra with a good heuristic | O((n+m) log n) |
@@ -186,12 +187,14 @@ All algorithms are available on `Graph` objects and accessible from `GeoGraph` v
186
187
 
187
188
  | Scenario | Recommended Approach |
188
189
  |---|---|
189
- | Single query | `dijkstra` (default) |
190
+ | Single query | `dijkstra` (default) or `bidirectional_dijkstra` |
191
+ | Sparse networks (road/rail/maritime) | Reduce with `geograph.reduce()` for 2–4x faster queries |
190
192
  | Weights generally >= 1 | `dijkstra_buckets` |
191
193
  | Repeated queries from one origin | `cached_shortest_path` |
192
194
  | Large distance matrix (same graph) | `distance_matrix` method |
193
195
  | Many arbitrary queries on a fixed graph | `contraction_hierarchy` or `tnr` |
194
196
  | Graph with negative weights | `dijkstra_negative` |
197
+ | Fast app startup / multi-network loading | `GeoGraph.load_geograph(..., lazy=True)` |
195
198
 
196
199
  ## Heuristic Functions (for A*)
197
200
 
@@ -269,14 +272,14 @@ from scgraph import GeoGraph
269
272
 
270
273
  marnet_geograph = GeoGraph.load_geograph("marnet")
271
274
 
272
- # First call: computes and caches the shortest path tree (~same cost as dijkstra)
275
+ # First call: computes and caches the shortest path tree (~same cost as dijkstra without an early termination)
273
276
  output1 = marnet_geograph.get_shortest_path(
274
277
  origin_node={"latitude": 31.23, "longitude": 121.47}, # Shanghai
275
278
  destination_node={"latitude": 32.08, "longitude": -81.09}, # Savannah, GA
276
279
  algorithm_fn='cached_shortest_path',
277
280
  )
278
281
 
279
- # Subsequent calls to the same origin are near-instant
282
+ # Subsequent calls to the same origin are near-instant (O(1) lookup)
280
283
  output2 = marnet_geograph.get_shortest_path(
281
284
  origin_node={"latitude": 31.23, "longitude": 121.47}, # Shanghai (same)
282
285
  destination_node={"latitude": 51.50, "longitude": -0.13}, # London
@@ -311,6 +314,53 @@ matrix = us_freeway_geograph.distance_matrix(cities, output_units='km')
311
314
 
312
315
  For large matrices, throughput can approach 500 nanoseconds per distance query.
313
316
 
317
+ ## Graph Reduction
318
+
319
+ Sparse networks (highways, rail lines, shipping corridors) often contain long chains of intermediate nodes with only two connections (degree 2). Graph reduction compresses these chains into single composite edges while preserving exact distances and full path reconstruction:
320
+
321
+ ```py
322
+ from scgraph import GeoGraph
323
+
324
+ us_freeway = GeoGraph.load_geograph("us_freeway")
325
+
326
+ # Reduce the graph by bypassing pass-through chains (e.g. simplifies ~88% of nodes on us_freeway)
327
+ us_freeway.reduce(iterations=1)
328
+
329
+ # All standard algorithms (dijkstra, bidirectional_dijkstra, a_star, etc.)
330
+ # automatically execute on the reduced graph and expand paths back to original coordinates:
331
+ output = us_freeway.get_shortest_path(
332
+ origin_node={"latitude": 34.05, "longitude": -118.24}, # Los Angeles
333
+ destination_node={"latitude": 40.71, "longitude": -74.01}, # New York City
334
+ algorithm_fn='bidirectional_dijkstra',
335
+ )
336
+ print(output['length'])
337
+ ```
338
+
339
+ Key features of graph reduction:
340
+ - **Zero inaccuracy**: Paths and lengths are guaranteed to be mathematically identical to the unreduced graph.
341
+ - **Same-chain routing**: If origin and destination fall on the same contracted chain, the original graph is used with the standard dijkstra algorithm.
342
+ - **Iterative reduction**: Pass `iterations=-1` to reduce iteratively until full convergence (simplifying sub-chains created after initial contraction).
343
+
344
+ ## Lazy Loading
345
+
346
+ To avoid upfront downloading, parsing, or memory overhead when defining multiple networks or optimizing startup latency, use `lazy=True`:
347
+
348
+ ```py
349
+ from scgraph import GeoGraph
350
+
351
+ # Returns a LazyGeoGraph proxy immediately without downloading or parsing
352
+ world_net = GeoGraph.load_geograph("world_highways_and_marnet", lazy=True)
353
+
354
+ # The network is downloaded and instantiated only when first queried:
355
+ output = world_net.get_shortest_path(
356
+ origin_node={"latitude": 31.23, "longitude": 121.47},
357
+ destination_node={"latitude": 32.08, "longitude": -81.09},
358
+ )
359
+
360
+ # Or explicitly load the graph at any time:
361
+ world_net.load()
362
+ ```
363
+
314
364
  ## Node Addition Options
315
365
 
316
366
  Control how origin/destination are connected to the graph:
@@ -348,7 +398,7 @@ output = marnet_geograph.get_shortest_path(
348
398
 
349
399
  ## Built-in Geographs: Cache Management
350
400
 
351
- Built-in geographs are downloaded from GitHub on first use and stored in a local cache. Subsequent loads are instant and require no network access.
401
+ Built-in geographs are downloaded from GitHub on first use and stored in a local cache. Subsequent loads only require reading from the local cache and do not require network access.
352
402
 
353
403
  ```py
354
404
  from scgraph import GeoGraph
@@ -383,6 +433,16 @@ The cache location defaults to the platform-appropriate directory:
383
433
  | macOS | `~/Library/Caches/scgraph/` |
384
434
  | Windows | `%LOCALAPPDATA%\scgraph\` |
385
435
 
436
+ ### `load_geograph` Parameters
437
+
438
+ | Parameter | Default | Description |
439
+ |---|---|---|
440
+ | `name` | required | Name of the built-in geograph (e.g. `'marnet'`, `'us_freeway'`) |
441
+ | `cache_dir` | `None` | Custom directory for downloaded files; defaults to the OS cache dir above |
442
+ | `geograph_url` | `'https://raw.githubusercontent.com/...'` | Base URL for fetching geographs |
443
+ | `reduce_iterations` | `0` | Number of reduction iterations to perform upon loading (`0` = none, `1` = single pass, `-1` = full convergence) |
444
+ | `lazy` | `False` | Defer loading/fetching until the geograph is first accessed |
445
+
386
446
  ## Loading from OSMNx
387
447
 
388
448
  SCGraph integrates directly with [OSMNx](https://osmnx.readthedocs.io/) — load any OpenStreetMap network and convert it to a `GeoGraph` in two lines:
@@ -417,6 +477,8 @@ print(output['length'])
417
477
  | `off_graph_travel_speed` | `None` | Speed (km/h) for off-graph connections; used to convert time-based weights to distances |
418
478
  | `load_intermediate_nodes` | `True` | Load intermediate shape points for accurate path visualization |
419
479
  | `silent` | `False` | Suppress progress output |
480
+ | `reduce_iterations` | `0` | Number of reduction iterations to perform upon loading (`0` = none, `1` = single pass, `-1` = full convergence) |
481
+ | `lazy` | `False` | Defer loading until the geograph is first accessed |
420
482
 
421
483
  ## Building from OSM Data (Without OSMNx)
422
484
 
@@ -519,6 +581,20 @@ graph.validate()
519
581
 
520
582
  output = graph.dijkstra(origin_id=0, destination_id=5)
521
583
  print(output) #=> {'path': [0, 2, 1, 3, 5], 'length': 10}
584
+
585
+ # Reduce the graph (contracts pass-through nodes):
586
+ graph.reduce(iterations=1)
587
+
588
+ # Custom algorithm decorator with automated reduced graph support:
589
+ @Graph.algorithm
590
+ def custom_solver(self, origin_id: int, destination_id: int) -> dict:
591
+ # Standard single-sided algorithm implementation using self.graph
592
+ ...
593
+
594
+ @Graph.algorithm(bidirectional=True)
595
+ def custom_bidirectional_solver(self, origin_id: int, destination_id: int) -> dict:
596
+ # Bidirectional algorithm using self.graph and self.inverse_graph
597
+ ...
522
598
  ```
523
599
 
524
600
  ## Custom GeoGraph
@@ -727,7 +803,7 @@ uv sync --extra dev
727
803
  | `uv run pytest test/NN_*.py` | Run a specific test file |
728
804
  | `uv run nox` | Run tests (C++ then no-C++) across Python 3.11–3.14 |
729
805
  | `uv run nox -s tests-3.14` | Run both build variants on a single Python version |
730
- | `uv run utils/benchmark.py` | Run all benchmarks, output `benchmark_results.json` |
806
+ | `uv run utils/bench.py` | Run benchmarks (<30s), output `benchmark.md` |
731
807
  | `uv run utils/prettify.py` | Format with autoflake + black |
732
808
 
733
809
  For full developer documentation see [DEVELOPMENT.md](DEVELOPMENT.md).
@@ -30,7 +30,7 @@ include = [
30
30
 
31
31
  [project]
32
32
  name = "scgraph"
33
- version = "3.4.1"
33
+ version = "3.5.0"
34
34
  description = "Determine an approximate distance and route between two points on earth."
35
35
  authors = [
36
36
  {name="Connor Makowski", email="conmak@mit.edu"}
@@ -8,7 +8,7 @@
8
8
 
9
9
  ![scgraph](https://raw.githubusercontent.com/connor-makowski/scgraph/main/static/scgraph.png)
10
10
 
11
- SCGraph provides fast, flexible shortest path routing for road, rail, maritime, and custom networks. It ships with prebuilt geographic networks, supports arbitrary graph construction and OSMNx integration, and scales from simple point-to-point queries to massive distance matrices. It also supports optional C++ acceleration and Contraction Hierarchy preprocessing for large-scale applications.
11
+ SCGraph provides fast, flexible shortest path routing for road, rail, maritime, and custom networks. It ships with prebuilt geographic networks, supports arbitrary graph construction and OSMNx integration, and scales from simple point-to-point queries to massive distance matrices. It also supports graph reduction, lazy loading, optional C++ acceleration, and Contraction Hierarchy preprocessing for large-scale applications.
12
12
 
13
13
  - **Docs**: https://connor-makowski.github.io/scgraph/scgraph.html
14
14
  - **Paper**: https://ssrn.com/abstract=5388845
@@ -174,6 +174,7 @@ All algorithms are available on `Graph` objects and accessible from `GeoGraph` v
174
174
  | `algorithm_fn` | Description | Time Complexity |
175
175
  |---|---|---|
176
176
  | `'dijkstra'` | Standard Dijkstra; general purpose, non-negative weights (default) | O((n+m) log n) |
177
+ | `'bidirectional_dijkstra'` | Bidirectional Dijkstra; simultaneous search from origin and destination | O((n+m) log n) |
177
178
  | `'dijkstra_buckets'` | Dijkstra with buckets (Dial's algorithm); efficient for non-negative weights (ideally >= 1) | O(n+m+W) |
178
179
  | `'dijkstra_negative'` | Dijkstra with cycle detection; supports negative weights | O(n·m) |
179
180
  | `'a_star'` | A* with optional heuristic; faster than Dijkstra with a good heuristic | O((n+m) log n) |
@@ -187,12 +188,14 @@ All algorithms are available on `Graph` objects and accessible from `GeoGraph` v
187
188
 
188
189
  | Scenario | Recommended Approach |
189
190
  |---|---|
190
- | Single query | `dijkstra` (default) |
191
+ | Single query | `dijkstra` (default) or `bidirectional_dijkstra` |
192
+ | Sparse networks (road/rail/maritime) | Reduce with `geograph.reduce()` for 2–4x faster queries |
191
193
  | Weights generally >= 1 | `dijkstra_buckets` |
192
194
  | Repeated queries from one origin | `cached_shortest_path` |
193
195
  | Large distance matrix (same graph) | `distance_matrix` method |
194
196
  | Many arbitrary queries on a fixed graph | `contraction_hierarchy` or `tnr` |
195
197
  | Graph with negative weights | `dijkstra_negative` |
198
+ | Fast app startup / multi-network loading | `GeoGraph.load_geograph(..., lazy=True)` |
196
199
 
197
200
  ## Heuristic Functions (for A*)
198
201
 
@@ -270,14 +273,14 @@ from scgraph import GeoGraph
270
273
 
271
274
  marnet_geograph = GeoGraph.load_geograph("marnet")
272
275
 
273
- # First call: computes and caches the shortest path tree (~same cost as dijkstra)
276
+ # First call: computes and caches the shortest path tree (~same cost as dijkstra without an early termination)
274
277
  output1 = marnet_geograph.get_shortest_path(
275
278
  origin_node={"latitude": 31.23, "longitude": 121.47}, # Shanghai
276
279
  destination_node={"latitude": 32.08, "longitude": -81.09}, # Savannah, GA
277
280
  algorithm_fn='cached_shortest_path',
278
281
  )
279
282
 
280
- # Subsequent calls to the same origin are near-instant
283
+ # Subsequent calls to the same origin are near-instant (O(1) lookup)
281
284
  output2 = marnet_geograph.get_shortest_path(
282
285
  origin_node={"latitude": 31.23, "longitude": 121.47}, # Shanghai (same)
283
286
  destination_node={"latitude": 51.50, "longitude": -0.13}, # London
@@ -312,6 +315,53 @@ matrix = us_freeway_geograph.distance_matrix(cities, output_units='km')
312
315
 
313
316
  For large matrices, throughput can approach 500 nanoseconds per distance query.
314
317
 
318
+ ## Graph Reduction
319
+
320
+ Sparse networks (highways, rail lines, shipping corridors) often contain long chains of intermediate nodes with only two connections (degree 2). Graph reduction compresses these chains into single composite edges while preserving exact distances and full path reconstruction:
321
+
322
+ ```py
323
+ from scgraph import GeoGraph
324
+
325
+ us_freeway = GeoGraph.load_geograph("us_freeway")
326
+
327
+ # Reduce the graph by bypassing pass-through chains (e.g. simplifies ~88% of nodes on us_freeway)
328
+ us_freeway.reduce(iterations=1)
329
+
330
+ # All standard algorithms (dijkstra, bidirectional_dijkstra, a_star, etc.)
331
+ # automatically execute on the reduced graph and expand paths back to original coordinates:
332
+ output = us_freeway.get_shortest_path(
333
+ origin_node={"latitude": 34.05, "longitude": -118.24}, # Los Angeles
334
+ destination_node={"latitude": 40.71, "longitude": -74.01}, # New York City
335
+ algorithm_fn='bidirectional_dijkstra',
336
+ )
337
+ print(output['length'])
338
+ ```
339
+
340
+ Key features of graph reduction:
341
+ - **Zero inaccuracy**: Paths and lengths are guaranteed to be mathematically identical to the unreduced graph.
342
+ - **Same-chain routing**: If origin and destination fall on the same contracted chain, the original graph is used with the standard dijkstra algorithm.
343
+ - **Iterative reduction**: Pass `iterations=-1` to reduce iteratively until full convergence (simplifying sub-chains created after initial contraction).
344
+
345
+ ## Lazy Loading
346
+
347
+ To avoid upfront downloading, parsing, or memory overhead when defining multiple networks or optimizing startup latency, use `lazy=True`:
348
+
349
+ ```py
350
+ from scgraph import GeoGraph
351
+
352
+ # Returns a LazyGeoGraph proxy immediately without downloading or parsing
353
+ world_net = GeoGraph.load_geograph("world_highways_and_marnet", lazy=True)
354
+
355
+ # The network is downloaded and instantiated only when first queried:
356
+ output = world_net.get_shortest_path(
357
+ origin_node={"latitude": 31.23, "longitude": 121.47},
358
+ destination_node={"latitude": 32.08, "longitude": -81.09},
359
+ )
360
+
361
+ # Or explicitly load the graph at any time:
362
+ world_net.load()
363
+ ```
364
+
315
365
  ## Node Addition Options
316
366
 
317
367
  Control how origin/destination are connected to the graph:
@@ -349,7 +399,7 @@ output = marnet_geograph.get_shortest_path(
349
399
 
350
400
  ## Built-in Geographs: Cache Management
351
401
 
352
- Built-in geographs are downloaded from GitHub on first use and stored in a local cache. Subsequent loads are instant and require no network access.
402
+ Built-in geographs are downloaded from GitHub on first use and stored in a local cache. Subsequent loads only require reading from the local cache and do not require network access.
353
403
 
354
404
  ```py
355
405
  from scgraph import GeoGraph
@@ -384,6 +434,16 @@ The cache location defaults to the platform-appropriate directory:
384
434
  | macOS | `~/Library/Caches/scgraph/` |
385
435
  | Windows | `%LOCALAPPDATA%\\scgraph\\` |
386
436
 
437
+ ### `load_geograph` Parameters
438
+
439
+ | Parameter | Default | Description |
440
+ |---|---|---|
441
+ | `name` | required | Name of the built-in geograph (e.g. `'marnet'`, `'us_freeway'`) |
442
+ | `cache_dir` | `None` | Custom directory for downloaded files; defaults to the OS cache dir above |
443
+ | `geograph_url` | `'https://raw.githubusercontent.com/...'` | Base URL for fetching geographs |
444
+ | `reduce_iterations` | `0` | Number of reduction iterations to perform upon loading (`0` = none, `1` = single pass, `-1` = full convergence) |
445
+ | `lazy` | `False` | Defer loading/fetching until the geograph is first accessed |
446
+
387
447
  ## Loading from OSMNx
388
448
 
389
449
  SCGraph integrates directly with [OSMNx](https://osmnx.readthedocs.io/) — load any OpenStreetMap network and convert it to a `GeoGraph` in two lines:
@@ -418,6 +478,8 @@ print(output['length'])
418
478
  | `off_graph_travel_speed` | `None` | Speed (km/h) for off-graph connections; used to convert time-based weights to distances |
419
479
  | `load_intermediate_nodes` | `True` | Load intermediate shape points for accurate path visualization |
420
480
  | `silent` | `False` | Suppress progress output |
481
+ | `reduce_iterations` | `0` | Number of reduction iterations to perform upon loading (`0` = none, `1` = single pass, `-1` = full convergence) |
482
+ | `lazy` | `False` | Defer loading until the geograph is first accessed |
421
483
 
422
484
  ## Building from OSM Data (Without OSMNx)
423
485
 
@@ -520,6 +582,20 @@ graph.validate()
520
582
 
521
583
  output = graph.dijkstra(origin_id=0, destination_id=5)
522
584
  print(output) #=> {'path': [0, 2, 1, 3, 5], 'length': 10}
585
+
586
+ # Reduce the graph (contracts pass-through nodes):
587
+ graph.reduce(iterations=1)
588
+
589
+ # Custom algorithm decorator with automated reduced graph support:
590
+ @Graph.algorithm
591
+ def custom_solver(self, origin_id: int, destination_id: int) -> dict:
592
+ # Standard single-sided algorithm implementation using self.graph
593
+ ...
594
+
595
+ @Graph.algorithm(bidirectional=True)
596
+ def custom_bidirectional_solver(self, origin_id: int, destination_id: int) -> dict:
597
+ # Bidirectional algorithm using self.graph and self.inverse_graph
598
+ ...
523
599
  ```
524
600
 
525
601
  ## Custom GeoGraph
@@ -728,7 +804,7 @@ uv sync --extra dev
728
804
  | `uv run pytest test/NN_*.py` | Run a specific test file |
729
805
  | `uv run nox` | Run tests (C++ then no-C++) across Python 3.11–3.14 |
730
806
  | `uv run nox -s tests-3.14` | Run both build variants on a single Python version |
731
- | `uv run utils/benchmark.py` | Run all benchmarks, output `benchmark_results.json` |
807
+ | `uv run utils/bench.py` | Run benchmarks (<30s), output `benchmark.md` |
732
808
  | `uv run utils/prettify.py` | Format with autoflake + black |
733
809
 
734
810
  For full developer documentation see [DEVELOPMENT.md](DEVELOPMENT.md).
@@ -741,12 +817,16 @@ Originally inspired by [searoute](https://github.com/genthalili/searoute-py), in
741
817
 
742
818
  """
743
819
 
820
+ from scgraph.graph_reducer import algorithm
821
+
744
822
  try:
745
823
  from scgraph.cpp import Graph, CHGraph
824
+ # Assign the python algorithm function to the cpp graph class
825
+ Graph.algorithm = staticmethod(algorithm)
746
826
  except ImportError:
747
827
  from scgraph.graph import Graph
748
828
  from scgraph.contraction_hierarchies import CHGraph
749
-
829
+
750
830
  from scgraph.geograph import GeoGraph
751
831
  from scgraph.grid import GridGraph
752
832