Pathsolvers
Generic A*
astar_search is the low-level A* implementation shared by GraphPlanner and grid_astar.
It works on any hashable node type — you can use it directly to build a custom planner.
See Using astar_search directly for a worked example.
map_data.pathsolver.astar.astar_search(start_node, goal_node, get_neighbors_func, heuristic_func)
Search a graph using the A* algorithm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start_node
|
object
|
The starting node. |
required |
goal_node
|
object
|
The goal node. |
required |
get_neighbors_func
|
callable
|
A function that takes a node and returns an iterable of (neighbor, cost) tuples. |
required |
heuristic_func
|
callable
|
A function that takes a node and returns the estimated cost to the goal. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
path |
list or None
|
The path from start to goal as a list of nodes, or None if no path exists. |
Source code in map_data/pathsolver/astar.py
Graph Planner
Graph-based path planner that routes along the OSM road and footway network.
Builds an undirected weighted graph from the ways stored in a
:class:~map_data.map_data.MapData instance. Manually annotated paths
(negative-ID ways) are spliced into the graph by projecting their
endpoints onto the nearest OSM edge, so annotations extend the
traversable network seamlessly.
Walkable areas (closed area=yes or multipolygon ways, e.g. pedestrian
squares) can be crossed rather than only walked around: A* may hop between
any two of an area's entries along the shortest path inside it (see
:mod:map_data.pathsolver.walkable_area), and a waypoint inside an area
stays where it is instead of snapping to its rim.
Planning is performed with A* (see :meth:plan). The planner operates
entirely in UTM coordinates.
map_data.pathsolver.graph_planner.GraphPlanner.__init__(map_data, highway_types=None, max_snap_distance=DEFAULT_MAX_SNAP_DISTANCE, exclude_highway=NON_ROUTABLE_HIGHWAY_VALUES, traversability=None, highway_costs=None, surface_costs=None, path_cost_cap=None)
Initialize the graph planner.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
map_data
|
MapData
|
Parsed map data containing the OSM ways and node cache. |
required |
highway_types
|
list of str
|
Way categories to include in the graph. Supported values are
|
None
|
max_snap_distance
|
float
|
Maximum distance (metres) a waypoint passed to :meth: |
DEFAULT_MAX_SNAP_DISTANCE
|
exclude_highway
|
iterable of str
|
|
NON_ROUTABLE_HIGHWAY_VALUES
|
traversability
|
TraversabilityRules or str or Path
|
Tag rules deciding which ways may be driven on at all and what
extra cost they carry ( |
None
|
highway_costs
|
mapping
|
Cost per |
None
|
surface_costs
|
mapping
|
Cost per |
None
|
path_cost_cap
|
float
|
Upper bound of the |
None
|
Source code in map_data/pathsolver/graph_planner.py
map_data.pathsolver.graph_planner.GraphPlanner.plan(path_utm, keep_start=False, keep_goal=False)
Plan a path through a sequence of UTM waypoints along the graph.
Each consecutive pair of waypoints is routed independently. The
waypoints are snapped to the nearest graph edge before planning,
so they do not need to lie exactly on the network; the returned
route consists of on-network points only, the requested waypoints
being represented by their projections rather than repeated
verbatim (see :func:_drop_stacked_points).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path_utm
|
ndarray
|
Array of shape |
required |
keep_start
|
bool
|
Prepend the first waypoint verbatim instead of starting the route at its projection. For planning from the robot's own pose, where the route has to begin where the robot actually is; the leading off-network leg is then the robot's way onto the network. Only the first waypoint is treated this way — doing it for a waypoint in the middle of the route would produce a spur out to it and back. |
False
|
keep_goal
|
bool
|
Append the last waypoint verbatim after its projection, so the
route ends at the requested coordinate rather than
:attr: |
False
|
Returns:
| Type | Description |
|---|---|
ndarray or None
|
Concatenated path as an |
Source code in map_data/pathsolver/graph_planner.py
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 | |
map_data.pathsolver.graph_planner.GraphPlanner._route_segment(id_s, id_g, positions, extra_adj, extra_via)
Run A from id_s to id_g* over the graph plus a local subgraph.
positions gives the world coordinate of the two temporary snapped
nodes (everything else resolves through :attr:nodes); extra_adj
gives their (and their edge endpoints') extra adjacency, and
extra_via the geometry of those extra hops that cross an area. Kept
as plain dicts rather than one mixing them under string/int keys, so
none needs a cast to satisfy the type checker. Besides the graph's own
edges, an entry of a walkable area neighbours the area's other entries.
Source code in map_data/pathsolver/graph_planner.py
Walkable areas
Open walkable areas (pedestrian squares) for the graph planner.
A pedestrian square is mapped as a closed area=yes way, i.e. by its outline
only. Routed as a chain of nodes like any other way, it makes a route walk the
rim of the square instead of crossing it. :class:WalkableArea keeps the
polygon and lets the planner cross it: between two of its entries (network
nodes on, inside or within :data:ENTRY_TOLERANCE of the area) it prices the
shortest path that stays inside the polygon, holes included.
Such a path bends only at reflex corners of the outline and at hole vertices, so a visibility graph over the entries and those corners is enough. It is built on first use and kept with the area; the planner's global adjacency gains no edges.
WalkableArea
Shortest in-area crossings between the entries of one walkable polygon.
Costs are the in-polygon path length times factor, the weight multiplier of the area's own edges, so a crossing is priced like walking its rim and the planner's straight-line A* heuristic stays admissible.
__init__(polygon, factor, entries)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
polygon
|
Polygon
|
The area in UTM coordinates, holes included. |
required |
factor
|
float
|
Weight multiplier of the area's edges (at least 1). |
required |
entries
|
dict
|
Node id -> |
required |
covers(point)
Return True if point lies in the area (boundary included, holes excluded).
crossings(node)
Crossings from entry node to every other entry it can reach inside the area.
from_point(point, goal=None)
Crossings from a point in or next to the area to its entries and, when given, to goal.
A point (or goal) just outside joins the area at its nearest boundary
point, like an entry does. The goal crossing is None when goal is
not given or cannot be reached from point inside the area.
Grid A*
map_data.pathsolver.grid_astar.grid_astar(grid, start_utm, goal_utm, low, cs, *, simplify_path=True, grid_cost_weight=GRID_COST_WEIGHT)
Optimized A* search on a 2D grid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grid
|
ndarray
|
2D grid of costs (Y, X). |
required |
start_utm
|
tuple or ndarray
|
Starting point in UTM coordinates. |
required |
goal_utm
|
tuple or ndarray
|
Goal point in UTM coordinates. |
required |
low
|
tuple
|
|
required |
cs
|
float
|
Cell size of the grid in metres. |
required |
simplify_path
|
bool
|
Whether to simplify the resulting path with Douglas-Peucker. |
True
|
Returns:
| Type | Description |
|---|---|
ndarray or None
|
Found path as UTM coordinate array, or |
Source code in map_data/pathsolver/grid_astar.py
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | |
RRT*
Rapidly-exploring Random Tree Star (RRT*) path planner.
Builds a collision-free tree by randomly sampling the free space and rewiring edges to minimise path cost. The planner is asymptotically optimal: given enough iterations it converges to the shortest feasible path.
Collision checking uses two sources simultaneously:
- A Shapely STRtree of barrier polygons (hard obstacles).
- A 2-D cost grid where cells at or above traversability_threshold are treated as blocked.
Traversable cells contribute a weighted cost to the edge cost, so the planner naturally prefers low-cost corridors (e.g. footways) over open terrain.
map_data.pathsolver.rrt_star.RRTStar.__init__(start, goal, obstacles, obstacles_tree, grid, low, grid_scale=1.0, max_iter=2000, step_size=2.0, neighbor_radius=5.0, traversability_threshold=10.0, grid_cost_weight=GRID_COST_WEIGHT, *, transfer_id=None, improve_after_goal=False, improve_iter=200, informed=True, adaptive_radius=True, rng=None)
Initialize the RRT* planner.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
ndarray
|
Starting position as a 2-element array |
required |
goal
|
ndarray
|
Goal position as a 2-element array |
required |
obstacles
|
list
|
Shapely geometries representing hard barriers. Not read directly (collision checks go through obstacles_tree); accepted so callers can pass the same pair they already have. |
required |
obstacles_tree
|
STRtree or None
|
Pre-built Shapely STRtree index over obstacles. Pass |
required |
grid
|
ndarray
|
2-D cost array with shape |
required |
low
|
tuple of float
|
|
required |
grid_scale
|
float
|
Metres per grid cell (default |
1.0
|
max_iter
|
int
|
Maximum number of RRT* iterations (default |
2000
|
step_size
|
float
|
Maximum distance the tree extends toward a sampled point per
iteration in metres (default |
2.0
|
neighbor_radius
|
float
|
Radius in metres within which nearby nodes are considered for
rewiring (default |
5.0
|
traversability_threshold
|
float
|
Grid cost at which a cell is considered an obstacle
(default |
10.0
|
transfer_id
|
str or None
|
Optional identifier used to check for external cancellation
signals during planning. Pass |
None
|
improve_after_goal
|
bool
|
If |
False
|
improve_iter
|
int
|
With improve_after_goal, the most extra iterations spent
improving once the goal is first reached (default |
200
|
informed
|
bool
|
If |
True
|
adaptive_radius
|
bool
|
If |
True
|
rng
|
Random or None
|
Random source for every sampling draw. Defaults to a fresh
:class: |
None
|
Source code in map_data/pathsolver/rrt_star.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
map_data.pathsolver.rrt_star.RRTStar.find_path()
Run the RRT* algorithm and return the planned path.
Iterates up to max_iter times, growing the tree from start
toward randomly sampled points and rewiring edges to reduce cost.
The goal is sampled directly 10 % of the time to encourage
convergence.
Returns:
| Type | Description |
|---|---|
ndarray or None
|
Path as an |
Source code in map_data/pathsolver/rrt_star.py
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 | |
map_data.pathsolver.rrt_star.RRTStar._sample_point()
Sample a random point.
When a solution exists and informed is enabled, samples uniformly from the informed ellipse. Otherwise biases toward traversable grid cells 90 % of the time.
Source code in map_data/pathsolver/rrt_star.py
map_data.pathsolver.rrt_star.RRTStar._nearest_node(point)
Return the index of the tree node closest to point.
Uses a lazily rebuilt KD-tree for the bulk of the tree, plus a linear scan over nodes added since the last rebuild.
Source code in map_data/pathsolver/rrt_star.py
map_data.pathsolver.rrt_star.RRTStar._steer(start, target)
Return a point at most step_size metres from start toward target.
Source code in map_data/pathsolver/rrt_star.py
map_data.pathsolver.rrt_star.RRTStar._get_near_nodes(new_point, radius)
Return indices of all tree nodes within radius of new_point.
Source code in map_data/pathsolver/rrt_star.py
map_data.pathsolver.rrt_star.RRTStar._segment_cost(start, end)
Compute the cost of the segment from start to end.
Returns:
| Type | Description |
|---|---|
tuple of (bool, float)
|
|
Source code in map_data/pathsolver/rrt_star.py
map_data.pathsolver.rrt_star.RRTStar._reconstruct_path(goal_idx)
Walk the parent chain from goal_idx back to the root and return the path.
Not simplified here: :meth:~map_data.pathsolver.replan.ReplanPath._post_process_path
already runs a collision-checked Douglas-Peucker pass over the whole
assembled route, so simplifying each RRT* segment first would just
redo the same work at a smaller (and less effective) scale.
Source code in map_data/pathsolver/rrt_star.py
ReplanPath
ReplanPath is the local replanning engine used by the viewer's Planner mode and the
replan CLI tool. It discretizes the area around the input waypoints into a cost grid,
assigns traversal costs based on OSM highway and surface types, then finds a collision-free
path using either Grid A or RRT.
Class attributes
These cost tables are loaded from config/planner_defaults.yaml at import time and can be
overridden per-instance:
| Attribute | Type | Description |
|---|---|---|
HIGHWAY_COSTS |
dict[str, float] |
Per OSM highway value cost (0.0 = free, 1.0 = obstacle) |
SURFACE_COSTS |
dict[str, float] |
Extra penalty per OSM surface value |
DEFAULT_OFF_PATH_COST |
float |
Cost for cells not near any known way (default 0.9) |
PATH_COST_CAP |
float |
Maximum cost a way cell can have (default 0.85, keeps ways preferred over off-path) |
Constructor
| Parameter | Type | Description |
|---|---|---|
args |
argparse.Namespace |
Planning parameters. Use parse_args([]) to get defaults. |
obstacles |
list[shapely.Geometry] |
Obstacle geometries (from ways_to_shapely(md.barriers_list)). |
transfer_id |
str \| None |
Optional UUID for cancellation via cancel_replan_backend(). |
args attributes used by ReplanPath:
| Attribute | Default | Description |
|---|---|---|
low |
— | (min_x, min_y) lower bound of the planning area in UTM metres |
high |
— | (max_x, max_y) upper bound of the planning area in UTM metres |
cell_size |
0.25 |
Grid resolution in metres |
inflate_obstacles |
0.25 |
Buffer added to obstacle geometries in metres |
simplify_path |
True |
Apply Douglas-Peucker simplification to the output |
smooth_path |
False |
Apply gradient-descent smoothing after planning |
Methods
fill_grid(map_data, highway_types=None, max_path_dist=2.0)
Populate the cost grid from map data. Must be called before replan().
| Parameter | Default | Description |
|---|---|---|
map_data |
— | A loaded MapData object |
highway_types |
["footway"] |
Which way categories to use. Pass ["footway", "road"] to include roads. |
max_path_dist |
2.0 |
Cells within this distance (m) of a known way receive an interpolated cost. Cells beyond receive DEFAULT_OFF_PATH_COST. |
Cost model per cell: way_cost + (DEFAULT_OFF_PATH_COST - way_cost) × (dist / max_path_dist)², where way_cost = min(PATH_COST_CAP, HIGHWAY_COSTS[highway] + SURFACE_COSTS[surface]). Obstacle cells are set to inf.
replan(path, algorithm="astar")
Plan a path through the cost grid.
| Parameter | Description |
|---|---|
path |
np.ndarray of shape (N, 2+) — input waypoints in UTM metres |
algorithm |
"astar" (Grid A) or "rrt" (RRT) |
Returns np.ndarray (replanned path) or None (no path found, or the run was cancelled).
Segments between consecutive waypoints are processed sequentially.
Cancellation
To cancel a running replan() call from another thread, pass a transfer_id UUID to the
constructor and call:
Minimal usage example
import numpy as np
from map_data.map_data import MapData
from map_data.pathsolver.replan import ReplanPath, parse_args
from map_data.utils.parsing import ways_to_shapely
md = MapData.load("coords.mapdata")
args = parse_args([])
args.low = (md.min_x, md.min_y)
args.high = (md.max_x, md.max_y)
replanner = ReplanPath(args, ways_to_shapely(md.barriers_list))
replanner.fill_grid(md, highway_types=["footway"], max_path_dist=2.0)
start = np.array([md.min_x + 10, md.min_y + 10])
goal = np.array([md.max_x - 10, md.max_y - 10])
new_path = replanner.replan(np.array([start, goal]), algorithm="astar")