You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add multi_stop_search for multi-waypoint routing (#935) (#937)
Routes through N waypoints by calling a_star_search for each consecutive
pair and stitching segments into a single cumulative-cost surface.
Supports optional TSP-based reordering of interior waypoints (exact
Held-Karp for N<=12, nearest-neighbor + 2-opt otherwise).
Copy file name to clipboardExpand all lines: docs/source/user_guide/pathfinding.ipynb
+31-2Lines changed: 31 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -3,7 +3,7 @@
3
3
{
4
4
"cell_type": "markdown",
5
5
"metadata": {},
6
-
"source": "## Pathfinding\n\nXarray-spatial provides A\\* pathfinding for finding optimal routes through raster surfaces.\nPaths can be computed using geometric distance alone (unweighted) or weighted by a\nfriction/cost surface, which makes the algorithm find the *least-cost* path rather\nthan the geometrically shortest one.\n\nAll four backends are supported:\n\n| Backend | Strategy |\n|---------|----------|\n| **NumPy** | Numba-jitted kernel (fast, in-memory) |\n| **Dask** | Sparse Python A\\* with LRU chunk cache — loads chunks on demand so the full grid never needs to fit in RAM |\n| **CuPy** | CPU fallback (transfers to numpy, runs the numba kernel, transfers back) |\n| **Dask+CuPy** | Same sparse A\\* as Dask, with automatic cupy-to-numpy chunk conversion |\n\n**Note:** `snap_start` and `snap_goal` are not supported with Dask-backed arrays\nbecause the brute-force nearest-pixel scan is O(h*w). Ensure the start and goal\npixels are valid before calling `a_star_search`.\n\n- [Unweighted A\\*](#Unweighted-A*): find the shortest geometric path through a line network\n- [Weighted A\\*](#Weighted-A*): find the least-cost path through a friction surface\n- [Dask (out-of-core)](#Dask-(out-of-core)): pathfinding on datasets that don't fit in RAM"
6
+
"source": "## Pathfinding\n\nXarray-spatial provides A\\* pathfinding for finding optimal routes through raster surfaces.\nPaths can be computed using geometric distance alone (unweighted) or weighted by a\nfriction/cost surface, which makes the algorithm find the *least-cost* path rather\nthan the geometrically shortest one.\n\n`multi_stop_search` extends A\\* to visit a sequence of waypoints, stitching segments\ninto a single cumulative-cost surface. It can optionally reorder interior waypoints\nto minimize total travel cost (TSP).\n\nAll four backends are supported:\n\n| Backend | Strategy |\n|---------|----------|\n| **NumPy** | Numba-jitted kernel (fast, in-memory) |\n| **Dask** | Sparse Python A\\* with LRU chunk cache — loads chunks on demand so the full grid never needs to fit in RAM |\n| **CuPy** | CPU fallback (transfers to numpy, runs the numba kernel, transfers back) |\n| **Dask+CuPy** | Same sparse A\\* as Dask, with automatic cupy-to-numpy chunk conversion |\n\n**Note:** `snap_start` and `snap_goal` are not supported with Dask-backed arrays\nbecause the brute-force nearest-pixel scan is O(h*w). Ensure the start and goal\npixels are valid before calling `a_star_search`.\n\n- [Unweighted A\\*](#Unweighted-A*): find the shortest geometric path through a line network\n- [Weighted A\\*](#Weighted-A*): find the least-cost path through a friction surface\n- [Multi-Stop Search](#Multi-Stop-Search): route through multiple waypoints with optional reordering\n- [Dask (out-of-core)](#Dask-(out-of-core)): pathfinding on datasets that don't fit in RAM"
7
7
},
8
8
{
9
9
"cell_type": "markdown",
@@ -17,7 +17,7 @@
17
17
"execution_count": null,
18
18
"metadata": {},
19
19
"outputs": [],
20
-
"source": "import numpy as np\nimport pandas as pd\nimport xarray as xr\n\nimport datashader as ds\nfrom datashader.transfer_functions import shade\nfrom datashader.transfer_functions import stack\nfrom datashader.transfer_functions import dynspread\nfrom datashader.transfer_functions import set_background\nfrom datashader.colors import Elevation\n\nimport xrspatial\nfrom xrspatial import a_star_search, generate_terrain, slope, cost_distance"
20
+
"source": "import numpy as np\nimport pandas as pd\nimport xarray as xr\n\nimport datashader as ds\nfrom datashader.transfer_functions import shade\nfrom datashader.transfer_functions import stack\nfrom datashader.transfer_functions import dynspread\nfrom datashader.transfer_functions import set_background\nfrom datashader.colors import Elevation\n\nimport xrspatial\nfrom xrspatial import a_star_search, multi_stop_search, generate_terrain, slope, cost_distance"
"source": "### Multi-Stop Search\n\n`multi_stop_search` routes through a list of waypoints in order, calling A\\* for\neach consecutive pair and stitching the segments into one cumulative-cost surface.\n\nSet `optimize_order=True` to let the solver reorder the interior waypoints (keeping\nthe first and last fixed) to minimize total travel cost. For up to 12 waypoints it\nuses exact Held-Karp; beyond that it falls back to nearest-neighbor + 2-opt.",
258
+
"metadata": {}
259
+
},
260
+
{
261
+
"cell_type": "markdown",
262
+
"source": "#### Route through three waypoints\n\nWe pick three waypoints across the terrain and let `multi_stop_search` find\nthe least-cost route visiting them in order.",
263
+
"metadata": {}
264
+
},
265
+
{
266
+
"cell_type": "code",
267
+
"source": "# Three waypoints across the terrain (re-using the terrain and friction from above)\nwp_start = (400.0, 50.0)\nwp_mid = (250.0, 250.0)\nwp_end = (100.0, 450.0)\n\n# Naive order\npath_multi = multi_stop_search(\n terrain, [wp_start, wp_mid, wp_end], friction=friction\n)\n\nprint(f\"Segment costs: {path_multi.attrs['segment_costs']}\")\nprint(f\"Total cost: {path_multi.attrs['total_cost']:.2f}\")\n\n# Visualise the multi-stop path\nterrain_shaded = shade(terrain, cmap=Elevation, how=\"linear\", alpha=180)\nmulti_shaded = dynspread(\n shade(path_multi, cmap=[\"cyan\", \"cyan\"], how=\"linear\", min_alpha=255),\n threshold=1, max_px=2,\n)\nstack(terrain_shaded, multi_shaded)",
268
+
"metadata": {},
269
+
"execution_count": null,
270
+
"outputs": []
271
+
},
272
+
{
273
+
"cell_type": "markdown",
274
+
"source": "#### Optimize waypoint order\n\nWith four or more waypoints, the given order may not be the cheapest.\n`optimize_order=True` solves a TSP over the pairwise A\\* costs, keeping the\nfirst and last waypoints fixed.",
275
+
"metadata": {}
276
+
},
277
+
{
278
+
"cell_type": "code",
279
+
"source": "# Four waypoints in a deliberately suboptimal order (zigzag)\nwp_a = (400.0, 50.0) # start (fixed)\nwp_b = (100.0, 450.0) # far corner\nwp_c = (350.0, 200.0) # back near start\nwp_d = (100.0, 350.0) # end (fixed)\n\nwaypoints = [wp_a, wp_b, wp_c, wp_d]\n\n# Without optimization\npath_naive = multi_stop_search(terrain, waypoints, friction=friction)\n\n# With optimization\npath_opt = multi_stop_search(\n terrain, waypoints, friction=friction, optimize_order=True\n)\n\nprint(f\"Naive order: {path_naive.attrs['waypoint_order']}\")\nprint(f\"Naive cost: {path_naive.attrs['total_cost']:.2f}\")\nprint(f\"Optimized order: {path_opt.attrs['waypoint_order']}\")\nprint(f\"Optimized cost: {path_opt.attrs['total_cost']:.2f}\")\nprint(f\"Savings: {path_naive.attrs['total_cost'] - path_opt.attrs['total_cost']:.2f}\")",
0 commit comments