Skip to content
Merged
23 changes: 22 additions & 1 deletion python/cuopt/cuopt/routing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,26 @@
update_routes_and_vehicles,
)
from cuopt.routing.utils_wrapper import DatasetDistribution
from cuopt.routing.vehicle_routing import BatchSolve, DataModel, Solve, SolverSettings
from cuopt.routing.vehicle_routing import (
BatchSolve,
DataModel,
Solve,
SolverSettings,
)
from cuopt.routing.vehicle_routing_wrapper import ErrorStatus, Objective

__all__ = [
"Assignment",
"BatchSolve",
"DataModel",
"DatasetDistribution",
"ErrorStatus",
"Objective",
"SolutionStatus",
"Solve",
"SolverSettings",
"add_vehicle_constraints",
"create_pickup_delivery_data",
"generate_dataset",
"update_routes_and_vehicles",
]
22 changes: 17 additions & 5 deletions python/cuopt/cuopt/routing/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import glob
Expand All @@ -17,10 +17,10 @@
def generate_dataset(
locations=100,
asymmetric=True,
min_demand=cudf.Series(),
max_demand=cudf.Series(),
min_capacities=cudf.Series(),
max_capacities=cudf.Series(),
min_demand=None,
max_demand=None,
min_capacities=None,
max_capacities=None,
min_service_time=0,
max_service_time=0,
tw_tightness=0.0,
Expand Down Expand Up @@ -114,6 +114,18 @@ def generate_dataset(
Time windows and multi dimension
capacity for each vehicle.
"""
# Default to empty device series here rather than in the signature:
# a cudf.Series() default is constructed at import time and needs a GPU,
# which would make importing this module fail on a GPU-less host.
if min_demand is None:
min_demand = cudf.Series()
if max_demand is None:
max_demand = cudf.Series()
if min_capacities is None:
min_capacities = cudf.Series()
if max_capacities is None:
max_capacities = cudf.Series()

if (
min_demand.shape[0] != max_demand.shape[0]
or max_demand.shape[0] != min_capacities.shape[0]
Expand Down
20 changes: 16 additions & 4 deletions python/cuopt/cuopt/routing/utils_wrapper.pyx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa
# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0


Expand Down Expand Up @@ -35,15 +35,27 @@ class DatasetDistribution(IntEnum):
RANDOM_CLUSTERED = dataset_distribution_t.RANDOM_CLUSTERED


def generate_dataset(locations=100, asymmetric=True, min_demand=cudf.Series(),
max_demand=cudf.Series(), min_capacities=cudf.Series(),
max_capacities=cudf.Series(), min_service_time=0,
def generate_dataset(locations=100, asymmetric=True, min_demand=None,
max_demand=None, min_capacities=None,
max_capacities=None, min_service_time=0,
max_service_time=0, tw_tightness=0.0,
drop_return_trips=0.0, shifts=1,
n_vehicle_types=1, n_matrix_types=1,
distribution=DatasetDistribution.CLUSTERED,
center_box=None, seed=0):

# Default to empty device series here rather than in the signature:
# a cudf.Series() default is constructed at import time and needs a GPU,
# which would make importing this module fail on a GPU-less host.
if min_demand is None:
min_demand = cudf.Series()
if max_demand is None:
max_demand = cudf.Series()
if min_capacities is None:
min_capacities = cudf.Series()
if max_capacities is None:
max_capacities = cudf.Series()

cdef unique_ptr[handle_t] handle_ptr
handle_ptr.reset(new handle_t())
handle_ = handle_ptr.get()
Expand Down
23 changes: 23 additions & 0 deletions python/cuopt/cuopt/tests/routing/test_gpu_free_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import os
import subprocess
import sys


def test_routing_imports_without_a_gpu():
"""cuopt.routing (and its dataset helpers) must import on a GPU-less host.

The dataset helpers build empty cudf.Series objects; doing so in a default
argument would run at import time and fail with cudaErrorNoDevice where no
GPU is visible. This guards that the construction stays inside the function
bodies. Run in a subprocess with no visible GPU for a faithful check.
"""
code = (
"import cuopt.routing as r\n"
"assert callable(r.generate_dataset)\n"
"assert r.DatasetDistribution.CLUSTERED is not None\n"
)
env = {**os.environ, "CUDA_VISIBLE_DEVICES": ""}
subprocess.run([sys.executable, "-c", code], check=True, env=env)
Loading