Routing Examples#

This section contains examples for the cuOpt routing Python API.

Intra-factory Transport#

A capacitated pickup-and-delivery problem with time windows (PDPTW) for a fleet of autonomous mobile robots (AMRs) moving parts between processing stations on a factory floor. The example uses cuopt.distance_engine.WaypointMatrix to derive a cost matrix from a weighted waypoint graph, sets up pickup/delivery orders with demand and time windows, solves with cuopt.routing.Solve(), and expands the target-location route back to a waypoint-level route per robot.

Waypoint graph

Problem details:

  • 4 target locations: 1 start location for AMRs and 3 processing stations

  • 6 transport orders (pickup/delivery pairs) with individual time windows

  • 2 AMRs, each with a carrying capacity of 2 parts

  • Factory hours: 0 to 100 time units

intra_factory_example.py

  1# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
  2# SPDX-License-Identifier: Apache-2.0
  3#
  4# Intra-factory transport example.
  5#
  6# Scenario: a small factory floor where a fleet of autonomous mobile robots
  7# (AMRs) pick up parts at processing stations and deliver them to other
  8# stations (or off the floor). cuOpt plans the routes for each robot such
  9# that every order is served within its pickup/delivery time windows and
 10# no robot exceeds its carrying capacity.
 11#
 12# This is a Capacitated Pickup-and-Delivery Problem with Time Windows
 13# (PDPTW) solved on a weighted waypoint graph.
 14
 15import cudf
 16import numpy as np
 17
 18from cuopt import distance_engine, routing
 19
 20# --- Factory layout --------------------------------------------------------
 21# Waypoints in the factory, referenced by integer id:
 22#     0 = AMR depot (robots start/return here)
 23#     4 = Station A
 24#     5 = Station B
 25#     6 = Station C
 26# Other waypoints (1, 2, 3, 7, 8, 9) are intermediate nodes that robots
 27# travel through but never stop at.
 28DEPOT = 0
 29STATION_A = 4
 30STATION_B = 5
 31STATION_C = 6
 32TARGET_LOCATIONS = np.array([DEPOT, STATION_A, STATION_B, STATION_C])
 33
 34# Factory operating hours (in time units).
 35FACTORY_OPEN = 0
 36FACTORY_CLOSE = 100
 37
 38# Weighted waypoint graph of the factory floor in Compressed Sparse Row
 39# (CSR) form: for node i, GRAPH_OFFSETS[i]:GRAPH_OFFSETS[i+1] indexes into
 40# GRAPH_EDGES (destination nodes) and GRAPH_WEIGHTS (edge costs). See the
 41# `cuopt.distance_engine.WaypointMatrix` API reference in the cuOpt User
 42# Guide for input requirements.
 43GRAPH_OFFSETS = np.array([0, 1, 3, 7, 9, 11, 13, 15, 17, 20, 22])
 44GRAPH_EDGES = np.array(
 45    [2, 2, 4, 0, 1, 3, 5, 2, 6, 1, 7, 2, 8, 3, 9, 4, 8, 5, 7, 9, 6, 8]
 46)
 47GRAPH_WEIGHTS = np.array(
 48    [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 2, 1, 2]
 49)
 50
 51
 52def build_cost_matrix():
 53    """Compute an all-pairs cost matrix over the target locations."""
 54    graph = distance_engine.WaypointMatrix(
 55        GRAPH_OFFSETS, GRAPH_EDGES, GRAPH_WEIGHTS
 56    )
 57    cost_matrix = graph.compute_cost_matrix(TARGET_LOCATIONS)
 58    # waypoint id -> cost-matrix index (e.g. Station A [waypoint 4] -> index 1)
 59    wp_to_idx = {int(wp): i for i, wp in enumerate(TARGET_LOCATIONS)}
 60    return graph, cost_matrix, wp_to_idx
 61
 62
 63def build_orders():
 64    """Six transport orders. Each row is one pickup/delivery pair."""
 65    return cudf.DataFrame(
 66        {
 67            "pickup_location": [
 68                STATION_A,
 69                STATION_B,
 70                STATION_C,
 71                STATION_C,
 72                STATION_B,
 73                STATION_A,
 74            ],
 75            "delivery_location": [
 76                STATION_B,
 77                STATION_C,
 78                DEPOT,
 79                STATION_B,
 80                STATION_A,
 81                DEPOT,
 82            ],
 83            "demand": [1, 1, 1, 1, 1, 1],
 84            "earliest_pickup": [0, 0, 0, 0, 0, 0],
 85            "latest_pickup": [10, 20, 30, 10, 20, 30],
 86            "earliest_delivery": [0, 0, 0, 0, 0, 0],
 87            "latest_delivery": [45, 45, 45, 45, 45, 45],
 88            "pickup_service_time": [2, 2, 2, 2, 2, 2],
 89            "delivery_service_time": [2, 2, 2, 2, 2, 2],
 90        }
 91    )
 92
 93
 94def build_fleet():
 95    """Two AMRs, each able to carry two parts at once."""
 96    return cudf.DataFrame({"robot_id": [0, 1], "capacity": [2, 2]}).set_index(
 97        "robot_id"
 98    )
 99
100
101def build_data_model(cost_matrix, orders, fleet, wp_to_idx):
102    """Assemble the cuOpt routing DataModel from the problem inputs."""
103    n_locations = len(cost_matrix)
104    n_vehicles = len(fleet)
105    # Each order contributes two stops: one pickup and one delivery.
106    n_orders = len(orders) * 2
107
108    data_model = routing.DataModel(n_locations, n_vehicles, n_orders)
109    data_model.add_cost_matrix(cost_matrix)
110
111    # Capacity: pickups add load, deliveries remove it.
112    demand = cudf.concat(
113        [orders["demand"], -orders["demand"]], ignore_index=True
114    )
115    data_model.add_capacity_dimension("parts", demand, fleet["capacity"])
116
117    # Order locations are expressed as cost-matrix indices, not waypoint ids.
118    pickup_idx = orders["pickup_location"].map(wp_to_idx)
119    delivery_idx = orders["delivery_location"].map(wp_to_idx)
120    data_model.set_order_locations(
121        cudf.concat([pickup_idx, delivery_idx], ignore_index=True)
122    )
123
124    # Pickup at row i must be served before its delivery at row i + n.
125    n = len(orders)
126    data_model.set_pickup_delivery_pairs(
127        cudf.Series(range(n)), cudf.Series(range(n, 2 * n))
128    )
129
130    # Time windows.
131    data_model.set_order_time_windows(
132        cudf.concat(
133            [orders["earliest_pickup"], orders["earliest_delivery"]],
134            ignore_index=True,
135        ),
136        cudf.concat(
137            [orders["latest_pickup"], orders["latest_delivery"]],
138            ignore_index=True,
139        ),
140    )
141    data_model.set_order_service_times(
142        cudf.concat(
143            [orders["pickup_service_time"], orders["delivery_service_time"]],
144            ignore_index=True,
145        )
146    )
147    data_model.set_vehicle_time_windows(
148        cudf.Series([FACTORY_OPEN] * n_vehicles),
149        cudf.Series([FACTORY_CLOSE] * n_vehicles),
150    )
151    return data_model
152
153
154def print_schedule(solution, graph, wp_to_idx):
155    """Print a per-robot, human-readable schedule of stops and waypoint paths."""
156    idx_to_wp = {i: wp for wp, i in wp_to_idx.items()}
157    route = solution.get_route()
158
159    print(f"\nTotal route cost: {solution.get_total_objective():g}")
160    print(f"Robots used:      {solution.get_vehicle_count()}\n")
161
162    for robot_id in sorted(route["truck_id"].unique().to_arrow().to_pylist()):
163        stops_gpu = route[route["truck_id"] == robot_id]
164        stops = stops_gpu.to_pandas()
165
166        print(f"Robot {robot_id}:")
167        for _, s in stops.iterrows():
168            print(
169                f"  t={s['arrival_stamp']:>5g}  "
170                f"waypoint {idx_to_wp[s['location']]:<2}  {s['type']}"
171            )
172
173        # compute_waypoint_sequence mutates its input, so hand it a fresh copy.
174        wp_path = graph.compute_waypoint_sequence(
175            TARGET_LOCATIONS, stops_gpu.copy()
176        )
177        path_str = " -> ".join(
178            str(w) for w in wp_path["waypoint_sequence"].to_arrow().to_pylist()
179        )
180        print(f"  path: {path_str}\n")
181
182
183def main():
184    graph, cost_matrix, wp_to_idx = build_cost_matrix()
185    orders = build_orders()
186    fleet = build_fleet()
187
188    print("Target locations (waypoint -> cost-matrix index):", wp_to_idx)
189    print("\nCost matrix between target locations:")
190    print(cost_matrix)
191    print(f"\n{len(orders)} transport orders, {len(fleet)} AMRs.")
192
193    data_model = build_data_model(cost_matrix, orders, fleet, wp_to_idx)
194
195    settings = routing.SolverSettings()
196    settings.set_time_limit(5)
197
198    solution = routing.Solve(data_model, settings)
199    if solution.get_status() != 0:
200        print(
201            f"cuOpt failed to find a solution (status={solution.get_status()})"
202        )
203        return
204
205    print_schedule(solution, graph, wp_to_idx)
206
207
208if __name__ == "__main__":
209    main()

TSP Batch Mode#

The routing Python API supports batch mode for solving many TSP (or routing) instances in a single call. Instead of calling cuopt.routing.Solve() repeatedly, you build a list of cuopt.routing.DataModel objects and call cuopt.routing.BatchSolve(). The solver runs the problems in parallel to improve throughput.

When to use batch mode:

  • You have many similar routing problems (e.g., dozens or hundreds of small TSPs).

  • You want to maximize throughput by utilizing the GPU across multiple problems at once.

  • Problem sizes and structure are compatible with the same cuopt.routing.SolverSettings (e.g., same time limit).

Returns: A list of cuopt.routing.Assignment objects, one per input data model, in the same order as data_model_list. Use cuopt.routing.Assignment.get_status() and other assignment methods to inspect each solution.

The following example builds several TSPs of different sizes, solves them in one batch, and prints a short summary per solution.

tsp_batch_example.py

 1# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 2# SPDX-License-Identifier: Apache-2.0
 3#
 4# TSP batch solve example: solve multiple TSP instances in one call for higher throughput.
 5# Use this when you have many similar routing problems (e.g., many small TSPs) to solve.
 6
 7import cudf
 8import numpy as np
 9
10from cuopt import routing
11
12
13def create_tsp_cost_matrix(n_locations):
14    """Create a simple symmetric cost matrix for a TSP of size n_locations."""
15    cost_matrix = np.zeros((n_locations, n_locations), dtype=np.float32)
16    for i in range(n_locations):
17        for j in range(n_locations):
18            cost_matrix[i, j] = abs(i - j)
19    return cudf.DataFrame(cost_matrix)
20
21
22def main():
23    # Define multiple TSP sizes to solve in one batch
24    tsp_sizes = [5, 8, 10, 6, 7, 9]
25
26    # Build one DataModel per TSP
27    data_models = []
28    for n_locations in tsp_sizes:
29        cost_matrix = create_tsp_cost_matrix(n_locations)
30        dm = routing.DataModel(n_locations, 1)  # n_locations, 1 vehicle (TSP)
31        dm.add_cost_matrix(cost_matrix)
32        data_models.append(dm)
33
34    # Shared solver settings for the batch
35    settings = routing.SolverSettings()
36    settings.set_time_limit(5.0)
37
38    # Solve all TSPs in batch (parallel execution)
39    solutions = routing.BatchSolve(data_models, settings)
40
41    # Inspect results
42    print(f"Solved {len(solutions)} TSPs in batch.")
43    for i, (size, solution) in enumerate(zip(tsp_sizes, solutions)):
44        status = solution.get_status()
45        status_str = (
46            "SUCCESS" if status == routing.SolutionStatus.SUCCESS else status
47        )
48        vehicle_count = solution.get_vehicle_count()
49        print(
50            f"  TSP {i} (size {size}): status={status_str}, vehicles={vehicle_count}"
51        )
52
53
54if __name__ == "__main__":
55    main()

Sample output:

Solved 6 TSPs in batch.
  TSP 0 (size 5): status=SUCCESS, vehicles=1
  TSP 1 (size 8): status=SUCCESS, vehicles=1
  TSP 2 (size 10): status=SUCCESS, vehicles=1
  ...

Notes:

  • All problems in the batch use the same cuopt.routing.SolverSettings (e.g., time limit, solver options).

  • Callbacks are not supported in batch mode.

  • For best practices when batching many instances, see the Add best practices for batch solving note in the release documentation.