API reference¶
The public API is everything importable from cuplan. Algorithm
modules are documented on their own pages; the shared vocabulary lives
here.
Grid and problem types¶
Occupancy-grid world shared by every solver in cuplan.
The array is the data structure: a boolean (height, width) occupancy
map, truthy where blocked, matching pymapf.core.grid.GridMap
semantics — 4-connected moves, unit edge costs, one move per timestep —
so a scenario ported between the two libraries means the same problem.
Grid ¶
An immutable 4-connected occupancy grid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obstacles
|
Iterable[Iterable]
|
2D array-like, truthy where a cell is blocked. Nested lists and NumPy arrays both work; the grid is copied and frozen. |
required |
Source code in cuplan/grid.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | |
empty
classmethod
¶
empty(height, width)
Return an obstacle-free grid of the given shape.
Source code in cuplan/grid.py
41 42 43 44 | |
neighbors ¶
neighbors(cell)
Return the free, in-bounds 4-connected neighbours of cell.
Source code in cuplan/grid.py
63 64 65 66 67 68 69 70 71 | |
to_linear ¶
to_linear(cells)
Convert (..., 2) row/col coordinates to linear indices.
Source code in cuplan/grid.py
73 74 75 76 | |
from_linear ¶
from_linear(index)
Convert linear indices back to (..., 2) row/col pairs.
Source code in cuplan/grid.py
78 79 80 81 | |
Problem and solution types, mirroring pymapf's vocabulary.
Agent/Problem/Solution carry the same semantics as
pymapf.core.solver: paths are lists of cells where index t is the
position at timestep t, an agent parks on its goal after arrival,
and validity means no vertex conflict (two agents on one cell) and no
edge conflict (two agents swapping cells between t and t+1).
Agent
dataclass
¶
A planning agent: a unique name with start and goal cells.
Source code in cuplan/problem.py
21 22 23 24 25 26 27 | |
Problem
dataclass
¶
A multi-agent path finding instance on a 4-connected grid.
Source code in cuplan/problem.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
Conflict
dataclass
¶
A vertex or edge conflict between two agents in a joint plan.
Source code in cuplan/problem.py
64 65 66 67 68 69 70 71 72 73 | |
Solution
dataclass
¶
Result of a solve: one path per agent plus cost metrics.
paths[name][t] is the agent's cell at timestep t; index 0 is
the start and the agent stays on its goal after the path ends.
Source code in cuplan/problem.py
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 | |
first_conflict ¶
first_conflict()
Return the earliest conflict, or None for a valid plan.
Source code in cuplan/problem.py
100 101 102 | |
is_valid ¶
is_valid()
True when the joint plan has no vertex or edge conflict.
Source code in cuplan/problem.py
104 105 106 | |
find_first_conflict ¶
find_first_conflict(paths)
Return the earliest vertex or edge conflict between any agent pair.
Paths are implicitly padded: an agent that has arrived occupies its goal at every later timestep, exactly as in pymapf.
Vectorized over agent pairs per timestep, so validating a 500-agent plan costs milliseconds rather than the O(n^2 T) Python loop it replaces.
Source code in cuplan/problem.py
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 | |
paths_from_array ¶
paths_from_array(steps, names, goals)
Convert a (T+1, n, 2) position array to per-agent paths.
The parked tail an agent spends on its goal is trimmed, matching pymapf's sum-of-costs convention (waiting on the goal at the end of a plan costs nothing).
Source code in cuplan/problem.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | |
Backend selection¶
Backend selection: NumPy reference or CUDA via CuPy.
pip install cuplan alone gives the CPU reference backend;
installing the [cuda12] or [cuda11] extra adds the CUDA one.
CuPy needs only the NVIDIA driver at runtime — kernels are CUDA C
compiled on first use through NVRTC, so no CUDA toolkit install is
required on the host.
CudaUnavailableError ¶
Bases: RuntimeError
Raised when backend="cuda" is requested but no device works.
Source code in cuplan/backend.py
21 22 | |
cuda_available
cached
¶
cuda_available()
Return True if CuPy is importable and a CUDA device executes.
The probe runs one tiny kernel rather than trusting the import: a
machine with CuPy installed but no usable driver fails at launch
time, and that is the failure this function must report.
Set CUPLAN_FORCE_CPU=1 to make it return False, which is how CI
tests the fallback path on GPU machines.
Source code in cuplan/backend.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
resolve_backend ¶
resolve_backend(backend='auto')
Map "auto" to the best available backend, validating the name.
"auto" prefers CUDA when :func:cuda_available holds, otherwise
falls back to the NumPy reference. "cuda" raises
:class:CudaUnavailableError instead of silently degrading — a
benchmark that quietly ran on the CPU is worse than one that failed.
Source code in cuplan/backend.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
get_cupy ¶
get_cupy()
Import and return CuPy, raising a helpful error if absent.
Source code in cuplan/backend.py
71 72 73 74 75 76 77 78 79 80 | |
Reservations¶
Space-time reservation table shared by the constrained searches.
The table is two dense arrays over (timestep, cell):
vertex[t, v]— cellvis occupied at timet(vertex constraint, Silver 2005).arrived_from[t, v]— linear index of the cell the occupying agent came from, or-1. Because vertex reservations guarantee at most one agent arrives atvper timestep, this single integer encodes every edge (swap) constraint: a moveu -> varriving attis illegal exactly whenarrived_from[t, u] == v.
Dense arrays instead of pymapf's constraint sets is the whole trick:
membership tests become array lookups the wavefront can do for every
cell at once, on either backend (the xp module is NumPy or CuPy).
ReservationTable ¶
Dense vertex + edge reservations over a bounded time horizon.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grid
|
Grid
|
the occupancy grid the reservations refer to. |
required |
horizon
|
int
|
last timestep (inclusive) the table covers. Searches against the table cannot return paths longer than this. |
required |
xp
|
module
|
array module — |
numpy
|
Source code in cuplan/reservations.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | |
reserve_path ¶
reserve_path(path)
Reserve a full agent path, parking it on its last cell forever.
path[t] is the agent's cell at time t. After the path
ends the agent is assumed to stay on its final cell, so that
cell is blocked through the end of the horizon — the same
convention as pymapf's prioritized planner.
Source code in cuplan/reservations.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
block_vertex ¶
block_vertex(cell, t)
Add a single vertex constraint: cell is occupied at t.
Source code in cuplan/reservations.py
75 76 77 78 | |
block_edge ¶
block_edge(u, v, t)
Forbid traversing u -> v arriving at time t.
Source code in cuplan/reservations.py
80 81 82 83 | |
last_vertex_time ¶
last_vertex_time(cell)
Latest t with a vertex reservation on cell (-1 if none).
A search must not settle on its goal before this time — the reservation could push it off again.
Source code in cuplan/reservations.py
85 86 87 88 89 90 91 92 93 94 | |
Kernel loading¶
CUDA C kernel sources and their NVRTC loader.
Kernels ship as .cu source files inside the wheel and are compiled
on first use through CuPy's :class:cupy.RawModule (NVRTC). CuPy caches
compiled cubins on disk, so the cost is paid once per machine, not once
per process.
kernel_source ¶
kernel_source(name)
Return the CUDA C source of kernels/<name>.cu.
Source code in cuplan/kernels/__init__.py
19 20 21 | |
kernel_names ¶
kernel_names()
List the kernel source files bundled with the package.
Source code in cuplan/kernels/__init__.py
24 25 26 27 28 29 30 | |
load_module
cached
¶
load_module(name)
Compile kernels/<name>.cu and return the cupy.RawModule.
Source code in cuplan/kernels/__init__.py
33 34 35 36 37 | |
get_kernel ¶
get_kernel(module_name, kernel)
Return a launchable cupy.RawKernel from a bundled module.
Source code in cuplan/kernels/__init__.py
40 41 42 | |
Roadmap stubs¶
Planned solvers: documented stubs, not implementations.
Each class below names the algorithm, the paper, and the intended
parallelization strategy, and raises :class:NotImplementedError from
its constructor so nothing can mistake a stub for a solver. pymapf has
working CPU implementations of all of them.
CBS ¶
Bases: _Planned
Conflict-Based Search (Sharon et al. 2015, AIJ 219:40-66).
Optimal two-level search. GPU plan: the high-level constraint tree is sequential, but sibling nodes' low-level searches are independent — batch them as parallel space-time wavefronts, one stream each.
Source code in cuplan/roadmap.py
25 26 27 28 29 30 31 | |
LaCAM ¶
Bases: _Planned
LaCAM (Okumura 2023, AAAI): complete search wrapping PIBT.
GPU plan: reuse cuplan's PIBT step (batched candidate evaluation); the lazy high-level DFS stays on the host.
Source code in cuplan/roadmap.py
34 35 36 37 38 39 | |
LNS ¶
Bases: _Planned
MAPF-LNS (Li et al. 2021, IJCAI): large neighbourhood search.
GPU plan: destroy/repair proposals are independent — evaluate many neighbourhoods concurrently and keep the best repair.
Source code in cuplan/roadmap.py
42 43 44 45 46 47 | |
SIPP ¶
Bases: _Planned
Safe Interval Path Planning (Phillips and Likhachev 2011, ICRA).
GPU plan: safe-interval construction from a reservation table is a per-cell scan (one thread per cell); the interval graph search itself is small enough to stay on the host.
Source code in cuplan/roadmap.py
50 51 52 53 54 55 56 | |
NMPC ¶
Bases: _Planned
Decentralized nonlinear MPC (mirroring pymapf's NMPC agent).
GPU plan: sampling-based MPC (MPPI) — thousands of rollouts per agent per step, each one thread.
Source code in cuplan/roadmap.py
59 60 61 62 63 64 | |
Benchmark harness¶
Reproducible random MAPF scenarios shared across libraries.
A scenario is a seeded random obstacle grid plus distinct start and
goal cells, all mutually reachable (verified with a flood fill from the
first start). The same object converts to a cuplan
:class:~cuplan.problem.Problem and a pymapf MAPFProblem, which is
what makes the benchmark apples-to-apples.
Scenario
dataclass
¶
A reproducible MAPF instance description.
Source code in cuplan/benchmark/scenarios.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
to_cuplan ¶
to_cuplan()
Return the instance as a cuplan :class:~cuplan.problem.Problem.
Source code in cuplan/benchmark/scenarios.py
36 37 38 39 40 41 42 | |
to_pymapf ¶
to_pymapf()
Return the instance as a pymapf problem (imported lazily).
Source code in cuplan/benchmark/scenarios.py
44 45 46 47 48 49 50 51 52 53 54 55 | |
random_scenario ¶
random_scenario(size, n_agents, obstacle_density=0.15, seed=0)
Generate a connected random instance.
Obstacles are sampled i.i.d. at obstacle_density; starts and
goals are distinct free cells drawn from the largest connected
component, so every agent's goal is reachable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
size
|
int
|
grid is |
required |
n_agents
|
int
|
number of agents (must fit in the free space). |
required |
obstacle_density
|
float
|
fraction of blocked cells. |
0.15
|
seed
|
int
|
RNG seed; same seed, same instance. |
0
|
Source code in cuplan/benchmark/scenarios.py
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 | |
Benchmark runners: same scenarios, every solver, honest numbers.
Each record carries the machine-independent facts (solver, backend,
grid size, agent count, seed) and the measured ones (wall time, sum of
costs, success). Wall time covers the full solve call including
host/device transfers — the number a user would actually see.
BenchmarkResult
dataclass
¶
One (scenario, solver) measurement.
Source code in cuplan/benchmark/harness.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | |
machine_description ¶
machine_description()
One-line description of the benchmark machine.
Source code in cuplan/benchmark/harness.py
44 45 46 47 48 49 50 51 52 | |
run_mapf_benchmark ¶
run_mapf_benchmark(sizes, agent_counts, seeds, obstacle_density=0.15, include_pymapf=True, include_cuda=None, progress=None)
Run prioritized planning and PIBT across scenario axes.
Every (size, agents, seed) triple builds one scenario handed to all solvers. Agent counts that do not fit a grid size are skipped.
Returns the flat list of records; aggregation is the reporter's job.
Source code in cuplan/benchmark/harness.py
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 | |
run_bfs_benchmark ¶
run_bfs_benchmark(sizes, batch_sizes, seeds, obstacle_density=0.15, include_cuda=None, progress=None)
Benchmark the batched distance-map primitive: CPU vs CUDA.
This is the primitive every solver consumes (heuristic tables, the PIBT oracle), measured directly: one flood fill per source, batched. pymapf has no batched equivalent — its per-goal Dijkstra cost is included in the solver families' timings.
Source code in cuplan/benchmark/harness.py
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 | |
run_vo_benchmark ¶
run_vo_benchmark(agent_counts, seeds, n_steps=80, include_cuda=None, progress=None)
Benchmark velocity-obstacle steps: cuplan CPU vs CUDA.
Agents start on a circle with antipodal goals — the classic
all-cross stress case. pymapf's simulator is not timed here: its
sequential in-step update solves a different problem per agent (see
:mod:cuplan.velocity_obstacles), so wall-clock comparison would
be misleading; the MAPF families carry the cross-library numbers.
Source code in cuplan/benchmark/harness.py
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 | |
Turn benchmark records into CSV, Markdown, and Frontier-styled charts.
write_csv ¶
write_csv(results, path)
Write the flat records as CSV (one row per measurement).
Source code in cuplan/benchmark/report.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | |
read_csv ¶
read_csv(path)
Load records written by :func:write_csv, e.g. to re-render reports.
Source code in cuplan/benchmark/report.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | |
write_markdown ¶
write_markdown(results, path)
Write an aggregated Markdown table with the measurement conditions.
Source code in cuplan/benchmark/report.py
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 | |
write_charts ¶
write_charts(results, out_dir)
Render runtime-scaling charts per family. Returns written paths.
Source code in cuplan/benchmark/report.py
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 | |