From 74c86ee761d5da3734fbf7571aacc3529bda8f58 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Fri, 6 Mar 2026 07:47:22 -0800 Subject: [PATCH 1/7] update routing tests --- .../cuopt/routing/vehicle_routing_wrapper.pyx | 5 +- .../cuopt/cuopt/tests/routing/API_COVERAGE.md | 124 +++ .../cuopt/tests/routing/save_restore_test.py | 62 -- .../cuopt/cuopt/tests/routing/solver_test.py | 54 -- .../tests/routing/test_cuopt_exception.py | 13 - .../cuopt/cuopt/tests/routing/test_cvrptw.py | 70 -- .../cuopt/tests/routing/test_data_model.py | 42 +- .../cuopt/tests/routing/test_error_logging.py | 99 --- .../tests/routing/test_initial_solutions.py | 16 +- .../tests/routing/test_pickup_delivery.py | 131 --- .../tests/routing/test_prize_collection.py | 256 ------ .../cuopt/cuopt/tests/routing/test_solver.py | 253 ++++++ .../tests/routing/test_solver_settings.py | 71 +- python/cuopt/cuopt/tests/routing/test_tsp.py | 58 -- .../cuopt/tests/routing/test_validation.py | 93 --- .../tests/routing/test_vehicle_breaks.py | 218 ----- .../test_vehicle_dependent_service_times.py | 99 --- .../tests/routing/test_vehicle_fixed_costs.py | 52 -- .../tests/routing/test_vehicle_max_cost.py | 50 -- .../tests/routing/test_vehicle_max_time.py | 81 -- .../tests/routing/test_vehicle_order_match.py | 109 --- .../tests/routing/test_vehicle_properties.py | 766 ++++++++++++++++++ .../tests/routing/test_vehicle_routing.py | 105 --- .../cuopt/tests/routing/test_vehicle_types.py | 84 -- .../cuopt/tests/routing/test_warnings.py | 67 -- .../tests/routing/test_warnings_exceptions.py | 129 +++ 26 files changed, 1367 insertions(+), 1740 deletions(-) create mode 100644 python/cuopt/cuopt/tests/routing/API_COVERAGE.md delete mode 100644 python/cuopt/cuopt/tests/routing/save_restore_test.py delete mode 100644 python/cuopt/cuopt/tests/routing/solver_test.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_cuopt_exception.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_cvrptw.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_error_logging.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_pickup_delivery.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_prize_collection.py create mode 100644 python/cuopt/cuopt/tests/routing/test_solver.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_tsp.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_validation.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_vehicle_breaks.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_vehicle_dependent_service_times.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_vehicle_fixed_costs.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_vehicle_max_cost.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_vehicle_max_time.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_vehicle_order_match.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_vehicle_routing.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_vehicle_types.py delete mode 100644 python/cuopt/cuopt/tests/routing/test_warnings.py create mode 100644 python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py diff --git a/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx b/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx index c1d4bd01a2..1981c3ddd9 100644 --- a/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx +++ b/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx @@ -655,7 +655,10 @@ cdef class DataModel: } def get_order_service_times(self, vehicle_id): - return self.order_service_times[vehicle_id] + if vehicle_id in self.order_service_times: + return self.order_service_times[vehicle_id] + else: + return cudf.Series([]) def get_order_time_windows(self): return ( diff --git a/python/cuopt/cuopt/tests/routing/API_COVERAGE.md b/python/cuopt/cuopt/tests/routing/API_COVERAGE.md new file mode 100644 index 0000000000..e7260a78e5 --- /dev/null +++ b/python/cuopt/cuopt/tests/routing/API_COVERAGE.md @@ -0,0 +1,124 @@ +# Routing Python Tests – API Coverage + +Summary of which APIs from `assignment.py` and `vehicle_routing.py` are exercised by tests under `cuopt/tests/routing/`. + +--- + +## assignment.py + +| API | Covered | Where | +|-----|---------|--------| +| **SolutionStatus** (enum) | Indirect | Tests assert `get_status() == 0` etc.; enum not referenced by name | +| **Assignment** (class) | Yes | Returned by `routing.Solve()` and used across tests | +| `get_vehicle_count()` | Yes | test_vehicle_properties, test_solver, test_solver_settings, test_batch_solve, etc. | +| `get_total_objective()` | Yes | test_vehicle_properties, test_solver, test_initial_solutions, etc. | +| `get_objective_values()` | Yes | test_vehicle_properties, test_solver | +| `get_route()` | Yes | Most routing tests | +| `get_status()` | Yes | All solve tests | +| `get_message()` | Yes | test_solver | +| `get_error_status()` | Yes | test_vehicle_properties (test_vehicle_max_times_fail) | +| `get_error_message()` | Yes | test_vehicle_properties (test_vehicle_max_times_fail) | +| `get_infeasible_orders()` | Yes | test_solver (test_pdptw) | +| `get_accepted_solutions()` | Yes | test_solver (test_pdptw) | +| `display_routes()` | Yes | test_vehicle_properties (test_vehicle_fixed_costs) | + +--- + +## vehicle_routing.py – DataModel + +### Setters + +| API | Covered | Where | +|-----|---------|--------| +| `add_cost_matrix()` | Yes | test_data_model, test_vehicle_properties, test_solver, test_batch_solve, etc. | +| `add_transit_time_matrix()` | Yes | test_vehicle_properties, test_solver, test_initial_solutions, test_re_routing, etc. | +| `set_break_locations()` | Yes | test_vehicle_properties (breaks tests) | +| `add_break_dimension()` | Yes | test_vehicle_properties | +| `add_vehicle_break()` | Yes | test_vehicle_properties (test_heterogenous_breaks) | +| `set_objective_function()` | Yes | test_data_model, test_initial_solutions | +| `add_initial_solutions()` | Yes | test_initial_solutions | +| `set_order_locations()` | Yes | test_vehicle_properties, test_solver, test_initial_solutions, test_warnings, etc. | +| `set_vehicle_types()` | Yes | test_vehicle_properties, test_initial_solutions, test_warnings | +| `set_pickup_delivery_pairs()` | Yes | test_vehicle_properties, test_solver, test_warnings, test_re_routing | +| `set_vehicle_time_windows()` | Yes | test_vehicle_properties, test_initial_solutions, test_re_routing | +| `set_vehicle_locations()` | Yes | test_vehicle_properties, test_initial_solutions | +| `set_order_time_windows()` | Yes | test_data_model, test_vehicle_properties, test_solver, etc. | +| `set_order_prizes()` | Yes | test_solver, test_initial_solutions | +| `set_drop_return_trips()` | Yes | test_re_routing, test_initial_solutions | +| `set_skip_first_trips()` | Yes | test_initial_solutions | +| `add_vehicle_order_match()` | Yes | test_vehicle_properties, test_initial_solutions, test_re_routing | +| `add_order_vehicle_match()` | Yes | test_vehicle_properties, test_initial_solutions, test_solver | +| `set_order_service_times()` | Yes | test_vehicle_properties, test_data_model, test_solver, etc. | +| `add_capacity_dimension()` | Yes | test_data_model, test_vehicle_properties, test_solver, etc. | +| `set_vehicle_max_costs()` | Yes | test_vehicle_properties, test_solver_settings | +| `set_vehicle_max_times()` | Yes | test_vehicle_properties | +| `set_vehicle_fixed_costs()` | Yes | test_vehicle_properties | +| `set_min_vehicles()` | Yes | test_vehicle_properties, test_solver_settings, test_initial_solutions, test_solver | + +### Getters + +| API | Covered | Where | +|-----|---------|--------| +| `get_num_locations()` | Yes | test_solver (getter check), test_re_routing | +| `get_fleet_size()` | Yes | test_data_model, test_vehicle_properties, test_solver_settings | +| `get_num_orders()` | Yes | test_data_model | +| `get_cost_matrix()` | Yes | test_data_model | +| `get_transit_time_matrix()` | Yes | test_solver (PDP getter check) | +| `get_transit_time_matrices()` | **No** | — | +| `get_initial_solutions()` | Yes | test_initial_solutions | +| `get_order_locations()` | Yes | test_vehicle_properties (test_single_vehicle_with_match, test_empty_routes_with_breaks) | +| `get_vehicle_types()` | Yes | test_vehicle_properties (test_vehicle_types) | +| `get_pickup_delivery_pairs()` | Yes | test_solver (test_pdptw) | +| `get_vehicle_time_windows()` | Yes | test_vehicle_properties (test_time_windows) | +| `get_vehicle_locations()` | Yes | test_vehicle_properties (test_vehicle_locations) | +| `get_drop_return_trips()` | Yes | test_initial_solutions (SKIP_DEPOTS) | +| `get_skip_first_trips()` | Yes | test_initial_solutions (SKIP_DEPOTS) | +| `get_capacity_dimensions()` | Yes | test_data_model | +| `get_order_time_windows()` | Yes | test_vehicle_properties (test_time_windows uses order TW from model) | +| `get_order_prizes()` | Yes | test_solver | +| `get_break_locations()` | Yes | test_vehicle_properties (test_uniform_breaks, test_non_uniform_breaks) | +| `get_break_dimensions()` | Yes | test_vehicle_properties (same tests) | +| `get_non_uniform_breaks()` | Yes | test_vehicle_properties (test_heterogenous_breaks) | +| `get_objective_function()` | Yes | test_data_model | +| `get_vehicle_max_costs()` | Yes | test_vehicle_properties (test_vehicle_max_costs) | +| `get_vehicle_max_times()` | Yes | test_vehicle_properties (test_vehicle_max_times) | +| `get_vehicle_fixed_costs()` | Yes | test_vehicle_properties (test_vehicle_fixed_costs) | +| `get_vehicle_order_match()` | Yes | test_vehicle_properties (test_vehicle_to_order_match) | +| `get_order_vehicle_match()` | Yes | test_vehicle_properties (test_order_to_vehicle_match) | +| `get_order_service_times()` | Yes | test_data_model | +| `get_min_vehicles()` | Yes | test_solver_settings | + +--- + +## vehicle_routing.py – SolverSettings + +| API | Covered | Where | +|-----|---------|--------| +| `set_time_limit()` | Yes | All solve tests | +| `set_verbose_mode()` | **No** | — | +| `set_error_logging_mode()` | Yes | test_error_logging | +| `dump_best_results()` | Yes | test_solver_settings | +| `dump_config_file()` | Yes | save_restore_test | +| `get_time_limit()` | Yes | test_solver_settings (test_min_vehicles, test_max_distance, test_solver_settings_getters) | +| `get_best_results_file_path()` | Yes | test_solver_settings | +| `get_config_file_name()` | Yes | test_solver_settings (test_solver_settings_getters) | +| `get_best_results_interval()` | Yes | test_solver_settings | + +--- + +## vehicle_routing.py – Solve / BatchSolve + +| API | Covered | Where | +|-----|---------|--------| +| `Solve(data_model, solver_settings)` | Yes | All routing tests that run the solver | +| `BatchSolve(data_model_list, solver_settings)` | Yes | test_batch_solve | + +--- + +## Summary + +- **assignment.py:** All listed APIs are covered. +- **DataModel getters not covered:** `get_transit_time_matrices()`. +- **SolverSettings not covered:** `set_verbose_mode()`. + +All **setters** on DataModel and SolverSettings that are present in `vehicle_routing.py` are covered by at least one test. Getters and a few Assignment/SolverSettings methods remain untested. diff --git a/python/cuopt/cuopt/tests/routing/save_restore_test.py b/python/cuopt/cuopt/tests/routing/save_restore_test.py deleted file mode 100644 index 3ca7bf8d65..0000000000 --- a/python/cuopt/cuopt/tests/routing/save_restore_test.py +++ /dev/null @@ -1,62 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import numpy as np - -import cudf - -from cuopt import routing -from cuopt.routing import utils - - -def test_solomon(): - utils.convert_solomon_inp_file_to_yaml( - utils.RAPIDS_DATASET_ROOT_DIR + "/solomon/In/r107.txt" - ) - service_list, vehicle_capacity, vehicle_num = utils.create_from_yaml_file( - utils.RAPIDS_DATASET_ROOT_DIR + "/solomon/In/r107.yaml" - ) - - distances = utils.build_matrix(service_list) - distances = distances.astype(np.float32) - - nodes = service_list["demand"].shape[0] - d = routing.DataModel(nodes, vehicle_num) - d.add_cost_matrix(distances) - - demand = service_list["demand"].astype(np.int32) - capacity_list = vehicle_capacity - capacity_series = cudf.Series(capacity_list) - d.add_capacity_dimension("demand", demand, capacity_series) - - earliest = service_list["earliest_time"].astype(np.int32) - latest = service_list["latest_time"].astype(np.int32) - service = service_list["service_time"].astype(np.int32) - d.set_order_time_windows(earliest, latest) - d.set_order_service_times(service) - - s = routing.SolverSettings() - s.set_time_limit(nodes / 5) - s.dump_config_file("cfg_file.yaml") - - routing_solution = routing.Solve(d, s) - - # vehicle_size = routing_solution.get_vehicle_count() - # final_cost = routing_solution.get_total_objective() - cu_status = routing_solution.get_status() - - # ref_cost = final_cost - # ref_veh_cost = vehicle_size - assert cu_status == 0 - - d, s = routing.utils.create_data_model_from_yaml("cfg_file.yaml") - routing_solution = routing.Solve(d, s) - - # vehicle_size = routing_solution.get_vehicle_count() - # final_cost = routing_solution.get_total_objective() - cu_status = routing_solution.get_status() - - assert cu_status == 0 - # FIXME: Deterministic PR - # assert vehicle_size == ref_veh_cost - # assert math.fabs((final_cost - ref_cost) / ref_cost) < 0.01 diff --git a/python/cuopt/cuopt/tests/routing/solver_test.py b/python/cuopt/cuopt/tests/routing/solver_test.py deleted file mode 100644 index 8faf4177dd..0000000000 --- a/python/cuopt/cuopt/tests/routing/solver_test.py +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import math - -import numpy as np - -import cudf - -from cuopt import routing -from cuopt.routing import utils - - -def test_solomon(): - utils.convert_solomon_inp_file_to_yaml( - utils.RAPIDS_DATASET_ROOT_DIR + "/solomon/In/r107.txt" - ) - service_list, vehicle_capacity, vehicle_num = utils.create_from_yaml_file( - utils.RAPIDS_DATASET_ROOT_DIR + "/solomon/In/r107.yaml" - ) - - distances = utils.build_matrix(service_list) - distances = distances.astype(np.float32) - - nodes = service_list["demand"].shape[0] - d = routing.DataModel(nodes, vehicle_num) - d.add_cost_matrix(distances) - - demand = service_list["demand"].astype(np.int32) - capacity_list = vehicle_capacity - capacity_series = cudf.Series(capacity_list) - d.add_capacity_dimension("demand", demand, capacity_series) - - earliest = service_list["earliest_time"].astype(np.int32) - latest = service_list["latest_time"].astype(np.int32) - service = service_list["service_time"].astype(np.int32) - d.set_order_time_windows(earliest, latest) - d.set_order_service_times(service) - - s = routing.SolverSettings() - # set it back to nodes/3 once issue with ARM is resolved - s.set_time_limit(nodes) - - routing_solution = routing.Solve(d, s) - - vehicle_size = routing_solution.get_vehicle_count() - final_cost = routing_solution.get_total_objective() - cu_status = routing_solution.get_status() - - ref_cost = 1087.15 - assert cu_status == 0 - assert vehicle_size <= 12 - if vehicle_size == 11: - assert math.fabs((final_cost - ref_cost) / ref_cost) < 0.1 diff --git a/python/cuopt/cuopt/tests/routing/test_cuopt_exception.py b/python/cuopt/cuopt/tests/routing/test_cuopt_exception.py deleted file mode 100644 index cc7206b682..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_cuopt_exception.py +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import pytest - -from cuopt import routing -from cuopt.utilities import InputValidationError - - -def test_solve_infeasible_with_challenging_breaks(): - with pytest.raises(InputValidationError) as err: - routing.DataModel(0, 0, 0) - assert str(err.value) == "The data model needs at least one location" diff --git a/python/cuopt/cuopt/tests/routing/test_cvrptw.py b/python/cuopt/cuopt/tests/routing/test_cvrptw.py deleted file mode 100644 index 2428a96e93..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_cvrptw.py +++ /dev/null @@ -1,70 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import math - -import numpy as np -import pytest - -from cuopt import routing -from cuopt.routing import utils - - -@pytest.fixture(params=utils.DATASETS_SOLOMON) -def data_(request): - df, vehicle_capacity, n_vehicles = utils.create_from_file(request.param) - file_name = request.param - return df, vehicle_capacity, n_vehicles, file_name - - -use_time_matrix = [False] -solomon_nodes = [100] - - -@pytest.fixture(params=solomon_nodes) -def nodes_(request): - return request.param - - -@pytest.fixture(params=use_time_matrix) -def use_time_matrix_(request): - return request.param - - -def test_cvrptw_dist_mat(data_, nodes_): - df, vehicle_capacity, n_vehicles, file_name = data_ - # read reference, if it doesn't exists skip the test - try: - ref_cost, ref_vehicle = utils.read_ref(file_name, "solomon", nodes_) - except ValueError: - pytest.skip("Reference could not be found!") - - print(f"Running file {file_name}...") - df = df.head(nodes_ + 1) # get only the relative number of nodes - distances = utils.build_matrix(df) - distances = distances.astype(np.float32) - nodes = df["demand"].shape[0] - d = routing.DataModel(nodes, n_vehicles) - d.add_cost_matrix(distances) - utils.fill_demand(df, d, vehicle_capacity, n_vehicles) - utils.fill_tw(d, df) - - s = routing.SolverSettings() - utils.set_limits(s, nodes) - - routing_solution = routing.Solve(d, s) - final_cost = routing_solution.get_total_objective() - vehicle_count = routing_solution.get_vehicle_count() - cu_route = routing_solution.get_route() - cu_status = routing_solution.get_status() - - # assert cu_status == SolutionStatus.SUCCESS - # status returns integer instead of enum - assert cu_status == 0 - # FIXME find better error rates - # assert (final_cost - ref_cost) / ref_cost < 0.2 - assert vehicle_count - ref_vehicle <= 2 - if vehicle_count == ref_vehicle: - assert final_cost <= math.ceil(ref_cost * 2.0) - assert cu_route["route"].unique().count() == nodes_ + 1 - assert cu_route["truck_id"].unique().count() == vehicle_count diff --git a/python/cuopt/cuopt/tests/routing/test_data_model.py b/python/cuopt/cuopt/tests/routing/test_data_model.py index 7bbb2e35de..b8525d8ee8 100644 --- a/python/cuopt/cuopt/tests/routing/test_data_model.py +++ b/python/cuopt/cuopt/tests/routing/test_data_model.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import numpy as np +import pytest import cudf @@ -15,10 +16,12 @@ def test_order_constraints(): distances = utils.build_matrix(service_list) distances = distances.astype(np.float32) + transit_times = distances.copy(deep=True) nodes = service_list["demand"].shape[0] d = routing.DataModel(nodes, vehicle_num) d.add_cost_matrix(distances) + d.add_transit_time_matrix(transit_times) demand = service_list["demand"].astype(np.int32) capacity_list = [vehicle_capacity] * vehicle_num @@ -31,11 +34,8 @@ def test_order_constraints(): d.set_order_time_windows(earliest, latest) d.set_order_service_times(service) - s = routing.SolverSettings() - - routing_solution = routing.Solve(d, s) - ret_distances = d.get_cost_matrix() + ret_transit_times = d.get_transit_time_matrix(0) ret_vehicle_num = d.get_fleet_size() ret_num_orders = d.get_num_orders() ret_capacity_dimensions = d.get_capacity_dimensions() @@ -43,6 +43,7 @@ def test_order_constraints(): ret_service_time = d.get_order_service_times() assert cudf.DataFrame(ret_distances).equals(distances) + assert cudf.DataFrame(ret_transit_times).equals(transit_times) assert ret_vehicle_num == vehicle_num assert ret_num_orders == nodes assert (ret_capacity_dimensions["demand"]["demand"] == demand).all() @@ -53,12 +54,6 @@ def test_order_constraints(): assert (ret_time_windows[1] == latest).all() assert (ret_service_time == service).all() - cu_status = routing_solution.get_status() - vehicle_size = routing_solution.get_vehicle_count() - - assert cu_status == 0 - assert vehicle_size <= 11 - def test_objective_function(): d = utils.create_data_model(filename, run_nodes=10) @@ -71,3 +66,30 @@ def test_objective_function(): assert (objectives == ret_objectives).all() assert (objective_weights == ret_objective_weights).all() + + +def test_multi_cost_and_transit_matrices_getters(): + """Assert getters return correct multi vehicle-type cost and transit matrices.""" + cost_1 = cudf.DataFrame( + [[0, 4, 4], [4, 0, 4], [4, 4, 0]], dtype=np.float32 + ) + time_1 = cudf.DataFrame( + [[0, 50, 50], [50, 0, 50], [50, 50, 0]], dtype=np.float32 + ) + cost_2 = cudf.DataFrame( + [[0, 1, 1], [1, 0, 1], [1, 1, 0]], dtype=np.float32 + ) + time_2 = cudf.DataFrame( + [[0, 10, 10], [10, 0, 10], [10, 10, 0]], dtype=np.float32 + ) + dm = routing.DataModel(3, 2) + dm.add_cost_matrix(cost_1, 1) + dm.add_transit_time_matrix(time_1, 1) + dm.add_cost_matrix(cost_2, 2) + dm.add_transit_time_matrix(time_2, 2) + dm.set_vehicle_types(cudf.Series([1, 2])) + + assert cudf.DataFrame(dm.get_cost_matrix(1)).equals(cost_1) + assert cudf.DataFrame(dm.get_cost_matrix(2)).equals(cost_2) + assert cudf.DataFrame(dm.get_transit_time_matrix(1)).equals(time_1) + assert cudf.DataFrame(dm.get_transit_time_matrix(2)).equals(time_2) diff --git a/python/cuopt/cuopt/tests/routing/test_error_logging.py b/python/cuopt/cuopt/tests/routing/test_error_logging.py deleted file mode 100644 index 73877b1ae3..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_error_logging.py +++ /dev/null @@ -1,99 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import numpy as np -import pytest - -import cudf - -from cuopt import routing - -cost_matrix = cudf.DataFrame( - [ - [0, 1, 1, 1, 1], - [1, 0, 1, 1, 1], - [1, 1, 0, 1, 1], - [1, 1, 1, 0, 1], - [1, 1, 1, 1, 0], - ] -) -time_window = cudf.DataFrame( - { - "earliest": [0, 2, 1, 0, 8], - "latest": [30, 10, 10, 10, 10], - "service": [2, 2, 2, 2, 2], - } -) -demand = cudf.Series([0, 1, 2, 1, 2]) -capacity = cudf.Series([5, 5]) - - -@pytest.mark.skip(reason="No error logging in new solver") -def test_time_window_constraints(): - data_model = routing.DataModel(cost_matrix.shape[0], 2) - data_model.add_cost_matrix(cost_matrix) - - infeasible_tw = cudf.DataFrame( - { - "earliest": [0, 2, 1, 0, 7], - "latest": [30, 10, 10, 10, 10], - "service": [10, 15, 15, 15, 15], - } - ) - data_model.set_order_time_windows( - infeasible_tw["earliest"], infeasible_tw["latest"] - ) - data_model.set_order_service_times(infeasible_tw["service"]) - - data_model.add_capacity_dimension("n_orders", demand, capacity) - - solver_settings = routing.SolverSettings() - solver_settings.set_error_logging_mode(True) - solution = routing.Solve(data_model, solver_settings) - assert solution.get_status() != 0 - assert ( - solution.get_message() - == "Infeasible Solve - Try relaxing Time Window constraints" - ) - - -@pytest.mark.skip(reason="No error logging in new solver") -def test_break_constraints(): - vehicle_num = len(capacity) - data_model = routing.DataModel(cost_matrix.shape[0], vehicle_num) - data_model.add_cost_matrix(cost_matrix) - data_model.set_order_time_windows( - time_window["earliest"], time_window["latest"] - ) - data_model.set_order_service_times(time_window["service"]) - - data_model.add_capacity_dimension("n_orders", demand, capacity) - - break_times = [[10, 45]] - - num_breaks = len(break_times) - vehicle_breaks_earliest = np.zeros([vehicle_num, num_breaks]) - vehicle_breaks_latest = np.zeros([vehicle_num, num_breaks]) - vehicle_breaks_duration = np.zeros([vehicle_num, num_breaks]) - for b in range(num_breaks): - break_begin = break_times[b][0] - break_end = break_times[b][1] - break_duration = break_end - break_begin - vehicle_breaks_earliest[:, b] = [break_begin] * vehicle_num - vehicle_breaks_latest[:, b] = [break_begin] * vehicle_num - vehicle_breaks_duration[:, b] = [break_duration] * vehicle_num - - for b in range(num_breaks): - data_model.add_break_dimension( - cudf.Series(vehicle_breaks_earliest[:, b]), - cudf.Series(vehicle_breaks_latest[:, b]), - cudf.Series(vehicle_breaks_duration[:, b]), - ) - solver_settings = routing.SolverSettings() - solver_settings.set_error_logging_mode(True) - solution = routing.Solve(data_model, solver_settings) - assert solution.get_status() != 0 - assert ( - solution.get_message() - == "Infeasible Solve - Try relaxing Break constraints" - ) diff --git a/python/cuopt/cuopt/tests/routing/test_initial_solutions.py b/python/cuopt/cuopt/tests/routing/test_initial_solutions.py index c8b7ba1e7a..a7b521b0c6 100644 --- a/python/cuopt/cuopt/tests/routing/test_initial_solutions.py +++ b/python/cuopt/cuopt/tests/routing/test_initial_solutions.py @@ -77,8 +77,12 @@ def test_initial_solutions(flag): d.add_capacity_dimension("demand", demand, capacities) d.set_order_locations(order_loc) if flag == TestOption.SKIP_DEPOTS: - d.set_skip_first_trips(cudf.Series([1, 1, 1, 1, 1])) - d.set_drop_return_trips(cudf.Series([1, 1, 1, 1, 1])) + skip_first = cudf.Series([1, 1, 1, 1, 1]) + drop_return = cudf.Series([1, 1, 1, 1, 1]) + d.set_skip_first_trips(skip_first) + d.set_drop_return_trips(drop_return) + assert (d.get_skip_first_trips() == skip_first).all() + assert (d.get_drop_return_trips() == drop_return).all() if flag == TestOption.BREAKS: d.add_break_dimension( cudf.Series([0] * vehicle_num), @@ -121,6 +125,14 @@ def test_initial_solutions(flag): sol_offsets = cudf.Series([0, 4]) d.add_initial_solutions(vehicle_ids, routes, types, sol_offsets) + ret_initial = d.get_initial_solutions() + assert len(ret_initial) == 4 + ret_sizes = sorted(len(x) for x in ret_initial) + expected_sizes = sorted([ + len(vehicle_ids), len(routes), len(types), len(sol_offsets) + ]) + assert ret_sizes == expected_sizes + s.set_time_limit(1) routing_solution = routing.Solve(d, s) diff --git a/python/cuopt/cuopt/tests/routing/test_pickup_delivery.py b/python/cuopt/cuopt/tests/routing/test_pickup_delivery.py deleted file mode 100644 index 22bce40252..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_pickup_delivery.py +++ /dev/null @@ -1,131 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import numpy as np -import pandas as pd - -import cudf - -from cuopt import routing - - -# Function to extract order constraints, distance matrix -# and pickup/delivery indices data -def data_prep(order_pdf, matrix_pdf, depot): - # Prepare order time window and demand constraints - order_df = cudf.DataFrame(order_pdf[["earliest", "latest"]]).reset_index( - drop=True - ) - temp_df = cudf.DataFrame() - temp_df["earliest"] = order_df["earliest"] - temp_df["latest"] = order_df["latest"] - order_df = cudf.concat([order_df, temp_df]) - order_df["service"] = [10] * len(order_df) - order_df["demand"] = [-1] * len(temp_df) + [1] * len(temp_df) - constraints = order_df.reset_index(drop=True) - - sources = order_pdf["From"].tolist() - sinks = order_pdf["To"].tolist() - - orders = sources + sinks - - unique_locations = {} - order_locations = [] - locations = [] - - locations.append(depot) - unique_locations[depot] = 0 - cnt = len(unique_locations) - for order in orders: - if unique_locations.get(order, -1) == -1: - unique_locations[order] = cnt - order_locations.append(cnt) - locations.append(order) - cnt = cnt + 1 - else: - order_locations.append(unique_locations.get(order, -1)) - - order_locations = cudf.Series(order_locations) - - num_locations = cnt - - # Prepare the distance matrix - matrix = np.empty(shape=(num_locations, num_locations)) - matrix.fill(0.0) - matrix_df_cols = matrix_pdf.columns.tolist() - matrix_pdf = matrix_pdf.values.tolist() - for i in range(num_locations): - for j in range(num_locations): - my_x = locations[i] - my_y = locations[j] - my_ix = matrix_df_cols.index(my_x) - my_iy = matrix_df_cols.index(my_y) - matrix[i][j] = matrix_pdf[my_ix][my_iy] - - pdf = pd.DataFrame(matrix) - matrix_df = cudf.from_pandas(pdf).astype("float32") - - # Prepare pickup and delivery indices - delivery_indices = cudf.Series(i for i in range(0, int(len(order_df) / 2))) - pickup_indices = cudf.Series( - i for i in range(int(len(order_df) / 2), len(order_df)) - ) - - return ( - matrix_df, - constraints, - order_locations, - pickup_indices, - delivery_indices, - ) - - -# Get time in seconds -def get_time(t): - hh, mm, ss = t.split(":") - mytime = int(hh) * 60 * 60 + int(mm) * 60 + int(ss) - return mytime - - -def run_cuopt( - matrix_df, - constraints, - order_locations, - num_vehicles, - pickup_indices, - delivery_indices, -): - num_orders = len(constraints) - num_locations = len(matrix_df) - # Pass the distance matrix - data_model = routing.DataModel(num_locations, num_vehicles, num_orders) - data_model.add_cost_matrix(matrix_df) - data_model.set_order_locations(order_locations) - - data_model.set_pickup_delivery_pairs(pickup_indices, delivery_indices) - capacity_series = cudf.Series([1] * num_vehicles) - data_model.add_capacity_dimension( - "demand", constraints["demand"], capacity_series - ) - data_model.set_order_time_windows( - constraints["earliest"], constraints["latest"] - ) - data_model.set_order_service_times(constraints["service"]) - - # Composable solver settings for a simple CVRPTW - solver_settings = routing.SolverSettings() - - routing_solution = routing.Solve(data_model, solver_settings) - - ( - ret_pickup_indices, - ret_delivery_indices, - ) = data_model.get_pickup_delivery_pairs() - ret_order_locations = data_model.get_order_locations() - ret_num_locations = data_model.get_num_locations() - - assert ret_num_locations == num_locations - assert (ret_order_locations == order_locations).all() - assert (ret_pickup_indices == pickup_indices).all() - assert (ret_delivery_indices == delivery_indices).all() - assert routing_solution.get_status() == 0 diff --git a/python/cuopt/cuopt/tests/routing/test_prize_collection.py b/python/cuopt/cuopt/tests/routing/test_prize_collection.py deleted file mode 100644 index 0464250231..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_prize_collection.py +++ /dev/null @@ -1,256 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import numpy as np - -import cudf - -from cuopt import routing - - -def test_prize_collection(): - """ - Test prize collection - """ - - costs = cudf.DataFrame( - { - 0: [0, 1, 1, 1], - 1: [1, 0, 1, 1], - 2: [ - 1, - 1, - 0, - 1, - ], - 3: [1, 1, 1, 0], - } - ).astype(np.float32) - - vehicle_num = 1 - order_loc = cudf.Series([1, 2, 3]) - order_prizes = cudf.Series([5.0, 10.0, 5.0]).astype(np.int32) - cap = cudf.Series([2]) - dem = cudf.Series([1, 2, 1]) - earliest = cudf.Series([0, 0, 0]) - latest = cudf.Series([1000, 1000, 1000]) - - d = routing.DataModel(costs.shape[0], vehicle_num, len(order_loc)) - d.add_cost_matrix(costs) - d.set_order_locations(order_loc) - d.set_order_prizes(order_prizes) - d.add_capacity_dimension("dem", dem, cap) - d.set_order_time_windows(earliest, latest) - - s = routing.SolverSettings() - s.set_time_limit(2) - - routing_solution = routing.Solve(d, s) - cu_status = routing_solution.get_status() - objectives = routing_solution.get_objective_values() - - assert cu_status == 0 - assert routing_solution.get_total_objective() == -8.0 - assert objectives[routing.Objective.PRIZE] == -10.0 - assert objectives[routing.Objective.COST] == 2.0 - - -def test_min_vehicles(): - """ - Test min vehicles when prize collection is enabled - """ - cost_1 = cudf.DataFrame( - [ - [0, 5, 4, 3, 5], - [5, 0, 6, 4, 3], - [4, 8, 0, 4, 2], - [1, 4, 3, 0, 4], - [3, 3, 5, 6, 0], - ] - ).astype(np.float32) - - time_1 = cudf.DataFrame( - [ - [0, 10, 8, 6, 10], - [10, 0, 12, 8, 6], - [8, 16, 0, 8, 4], - [2, 8, 6, 0, 8], - [6, 6, 10, 12, 0], - ] - ).astype(np.float32) - - cost_2 = cudf.DataFrame( - [ - [0, 3, 2, 2, 4], - [4, 0, 5, 3, 2], - [3, 7, 0, 1, 1], - [1, 2, 2, 0, 3], - [2, 2, 3, 4, 0], - ] - ).astype(np.float32) - - time_2 = cudf.DataFrame( - [ - [0, 6, 4, 4, 8], - [8, 0, 10, 6, 4], - [6, 14, 0, 2, 2], - [2, 4, 4, 0, 6], - [4, 4, 6, 8, 0], - ] - ).astype(np.float32) - - vehicle_start_loc = cudf.Series([0, 1, 0, 1, 0]) - vehicle_end_loc = cudf.Series([0, 1, 1, 0, 0]) - - vehicle_types = cudf.Series([1, 1, 2, 2, 2]) - vehicle_cap = cudf.Series([30, 30, 10, 10, 10]) - - vehicle_start = cudf.Series([0, 5, 0, 20, 20]) - vehicle_end = cudf.Series([80, 80, 100, 100, 100]) - - vehicle_break_start = cudf.Series([20, 20, 20, 20, 20]) - vehicle_break_end = cudf.Series([25, 25, 25, 25, 25]) - vehicle_break_duration = cudf.Series([1, 1, 1, 1, 1]) - - vehicle_max_costs = cudf.Series([100, 100, 100, 100, 100]).astype( - np.float32 - ) - vehicle_max_times = cudf.Series([120, 120, 120, 120, 120]).astype( - np.float32 - ) - - order_loc = cudf.Series([1, 2, 3, 4]) - demand = cudf.Series([3, 4, 30, 3]) - - task_start = cudf.Series([3, 5, 1, 4]) - task_end = cudf.Series([20, 30, 20, 40]) - serv = cudf.Series([3, 1, 8, 4]) - prizes = cudf.Series([4, 4, 15, 3]) - - dm = routing.DataModel(cost_1.shape[0], len(vehicle_types), len(order_loc)) - - # Cost and Time - dm.add_cost_matrix(cost_1, 1) - dm.add_cost_matrix(cost_2, 2) - dm.add_transit_time_matrix(time_1, 1) - dm.add_transit_time_matrix(time_2, 2) - dm.set_vehicle_types(vehicle_types) - dm.set_vehicle_locations(vehicle_start_loc, vehicle_end_loc) - dm.set_vehicle_time_windows(vehicle_start, vehicle_end) - dm.add_break_dimension( - vehicle_break_start, vehicle_break_end, vehicle_break_duration - ) - dm.set_vehicle_max_costs(vehicle_max_costs) - dm.set_vehicle_max_times(vehicle_max_times) - dm.add_vehicle_order_match(3, cudf.Series([0, 3])) - dm.set_min_vehicles(2) - dm.set_order_locations(order_loc) - dm.add_capacity_dimension("1", demand, vehicle_cap) - dm.set_order_time_windows(task_start, task_end) - dm.set_order_service_times(serv) - dm.add_order_vehicle_match(3, cudf.Series([3])) - dm.add_order_vehicle_match(0, cudf.Series([3])) - dm.set_order_prizes(prizes) - - sol_set = routing.SolverSettings() - - sol_set.set_time_limit(15) - - sol = routing.Solve(dm, sol_set) - - assert sol.get_status() == 0 - assert sol.get_vehicle_count() >= 2 - - -def test_zero_prize(): - """ - Test prize collection when prize objective is zero - """ - - costs = cudf.DataFrame( - { - 0: [0, 1, 1, 1], - 1: [1, 0, 1, 1], - 2: [ - 1, - 1, - 0, - 1, - ], - 3: [1, 1, 1, 0], - } - ).astype(np.float32) - - vehicle_num = 1 - order_loc = cudf.Series([1, 2, 3]) - order_prizes = cudf.Series([5.0, 10.0, 5.0]).astype(np.int32) - - # Set capacity such that there is no feasible solution - cap = cudf.Series([2]) - dem = cudf.Series([1, 1, 1]) - - d = routing.DataModel(costs.shape[0], vehicle_num, len(order_loc)) - d.add_cost_matrix(costs) - d.set_order_locations(order_loc) - d.set_order_prizes(order_prizes) - d.add_capacity_dimension("dem", dem, cap) - d.set_objective_function( - cudf.Series([routing.Objective.PRIZE]), cudf.Series([0]) - ) - - s = routing.SolverSettings() - s.set_time_limit(2) - - routing_solution = routing.Solve(d, s) - cu_status = routing_solution.get_status() - - # Solution should be infeasible - assert cu_status == 1 - - -def test_no_feasible_task(): - """ - This is a corner case test when none of the task is feasible - """ - - costs = cudf.DataFrame( - { - 0: [0, 10, 10, 10], - 1: [10, 0, 10, 10], - 2: [ - 10, - 10, - 0, - 10, - ], - 3: [10, 10, 10, 0], - } - ).astype(np.float32) - - vehicle_num = 4 - vehicle_start_times = cudf.Series([0, 0, 0, 0]).astype(np.int32) - vehicle_return_times = cudf.Series([25, 22, 26, 29]).astype(np.int32) - - order_loc = cudf.Series([1, 2]) - order_prizes = cudf.Series([5.0, 5.0]).astype(np.int32) - order_service_times = cudf.Series([10, 10]).astype(np.int32) - - d = routing.DataModel(costs.shape[0], vehicle_num, len(order_loc)) - d.add_cost_matrix(costs) - d.add_transit_time_matrix(costs) - - d.set_vehicle_time_windows(vehicle_start_times, vehicle_return_times) - - d.set_order_locations(order_loc) - d.set_order_service_times(order_service_times) - d.set_order_prizes(order_prizes) - - s = routing.SolverSettings() - s.set_time_limit(2) - - routing_solution = routing.Solve(d, s) - cu_status = routing_solution.get_status() - - assert cu_status == 0 - assert routing_solution.get_total_objective() == 0.0 - assert routing_solution.get_vehicle_count() == 0 diff --git a/python/cuopt/cuopt/tests/routing/test_solver.py b/python/cuopt/cuopt/tests/routing/test_solver.py new file mode 100644 index 0000000000..016e1c0124 --- /dev/null +++ b/python/cuopt/cuopt/tests/routing/test_solver.py @@ -0,0 +1,253 @@ +# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import math +import os +import numpy as np + +import cudf + +from cuopt import routing +from cuopt.routing import utils + +SOLOMON_DATASETS_PATH = os.path.join(utils.RAPIDS_DATASET_ROOT_DIR, "solomon/In/") + +"""def test_solomon(): + SOLOMON_DATASET = "r107.txt" + SOLOMON_YAML = "r107.yaml" + utils.convert_solomon_inp_file_to_yaml( + SOLOMON_DATASETS_PATH + SOLOMON_DATASET + ) + service_list, vehicle_capacity, vehicle_num = utils.create_from_yaml_file( + SOLOMON_DATASETS_PATH + SOLOMON_YAML + ) + + distances = utils.build_matrix(service_list) + distances = distances.astype(np.float32) + + nodes = service_list["demand"].shape[0] + d = routing.DataModel(nodes, vehicle_num) + d.add_cost_matrix(distances) + + demand = service_list["demand"].astype(np.int32) + capacity_list = vehicle_capacity + capacity_series = cudf.Series(capacity_list) + d.add_capacity_dimension("demand", demand, capacity_series) + + earliest = service_list["earliest_time"].astype(np.int32) + latest = service_list["latest_time"].astype(np.int32) + service = service_list["service_time"].astype(np.int32) + d.set_order_time_windows(earliest, latest) + d.set_order_service_times(service) + + s = routing.SolverSettings() + # set it back to nodes/3 once issue with ARM is resolved + s.set_time_limit(nodes) + + routing_solution = routing.Solve(d, s) + + vehicle_size = routing_solution.get_vehicle_count() + final_cost = routing_solution.get_total_objective() + cu_status = routing_solution.get_status() + + ref_cost = 1087.15 + assert cu_status == 0 + assert vehicle_size <= 12 + if vehicle_size == 11: + assert math.fabs((final_cost - ref_cost) / ref_cost) < 0.1 +""" + +def test_pdptw(): + """ + Solve a small PDPTW: 5 locations (depot 0, pickups 1–2, deliveries 3–4), + 2 vehicles, 2 requests. Pickup must be visited before its delivery. + """ + # Locations: 0 = depot, 1 = pickup A, 2 = pickup B, 3 = delivery A, 4 = delivery B + n_locations = 5 + n_vehicles = 2 + + # Cost/distance matrix (symmetric, integer) + costs = cudf.DataFrame( + { + 0: [0, 2, 3, 4, 5], + 1: [2, 0, 2, 3, 4], + 2: [3, 2, 0, 4, 3], + 3: [4, 3, 4, 0, 2], + 4: [5, 4, 3, 2, 0], + }, + dtype=np.float32, + ) + # Use same matrix for transit time + times = costs.astype(np.float32) + + # Pickup-delivery pairs: (order index) pickup 1 -> delivery 3, pickup 2 -> delivery 4 + pickup_indices = cudf.Series([1, 2], dtype=np.int32) + delivery_indices = cudf.Series([3, 4], dtype=np.int32) + + # Demand: depot 0, pickups +1, deliveries -1 + demand = cudf.Series([0, 1, 1, -1, -1], dtype=np.int32) + capacities = cudf.Series([2, 2], dtype=np.int32) # capacity 2 per vehicle + + # Time windows [earliest, latest] per location (wide enough to be feasible) + earliest = cudf.Series([0, 0, 0, 0, 0], dtype=np.int32) + latest = cudf.Series([100, 100, 100, 100, 100], dtype=np.int32) + service_times = cudf.Series([0, 1, 1, 1, 1], dtype=np.int32) + + dm = routing.DataModel(n_locations, n_vehicles) + dm.add_cost_matrix(costs) + dm.add_transit_time_matrix(times) + dm.set_pickup_delivery_pairs(pickup_indices, delivery_indices) + dm.add_capacity_dimension("demand", demand, capacities) + dm.set_order_time_windows(earliest, latest) + dm.set_order_service_times(service_times) + + # Getter checks: pickup/delivery pairs and transit time matrix + ret_pickup, ret_delivery = dm.get_pickup_delivery_pairs() + assert (ret_pickup == pickup_indices).all(), "get_pickup_delivery_pairs pickup mismatch" + assert (ret_delivery == delivery_indices).all(), "get_pickup_delivery_pairs delivery mismatch" + ret_transit = dm.get_transit_time_matrix(0) + assert cudf.DataFrame(ret_transit).equals(times), "get_transit_time_matrix mismatch" + + settings = routing.SolverSettings() + settings.set_time_limit(10) + + solution = routing.Solve(dm, settings) + status = solution.get_status() + + assert status == 0, f"Expected status 0, got {status}: {solution.get_message()}" + assert solution.get_vehicle_count() >= 1 + # Exercise Assignment getters (return type / no raise) + assert isinstance(solution.get_accepted_solutions(), cudf.Series) + assert isinstance(solution.get_infeasible_orders(), cudf.Series) + assert solution.get_vehicle_count() <= n_vehicles + + # Check that each route respects pickup-before-delivery (order indices 1 before 3, 2 before 4) + route_df = solution.get_route() + for truck_id in route_df["truck_id"].unique().to_arrow().to_pylist(): + vehicle_route = route_df[route_df["truck_id"] == truck_id] + route_locs = vehicle_route["route"].to_arrow().to_pylist() + idx_1 = route_locs.index(1) if 1 in route_locs else -1 + idx_2 = route_locs.index(2) if 2 in route_locs else -1 + idx_3 = route_locs.index(3) if 3 in route_locs else -1 + idx_4 = route_locs.index(4) if 4 in route_locs else -1 + if idx_1 >= 0 and idx_3 >= 0: + assert idx_1 < idx_3, "Pickup 1 must be before delivery 3" + if idx_2 >= 0 and idx_4 >= 0: + assert idx_2 < idx_4, "Pickup 2 must be before delivery 4" + + # Optional: basic objective check + total_cost = solution.get_total_objective() + assert total_cost == 13.0 + + +def test_prize_collection(): + """ + Test min vehicles when prize collection is enabled + """ + cost_1 = cudf.DataFrame( + [ + [0, 5, 4, 3, 5], + [5, 0, 6, 4, 3], + [4, 8, 0, 4, 2], + [1, 4, 3, 0, 4], + [3, 3, 5, 6, 0], + ] + ).astype(np.float32) + + time_1 = cudf.DataFrame( + [ + [0, 10, 8, 6, 10], + [10, 0, 12, 8, 6], + [8, 16, 0, 8, 4], + [2, 8, 6, 0, 8], + [6, 6, 10, 12, 0], + ] + ).astype(np.float32) + + cost_2 = cudf.DataFrame( + [ + [0, 3, 2, 2, 4], + [4, 0, 5, 3, 2], + [3, 7, 0, 1, 1], + [1, 2, 2, 0, 3], + [2, 2, 3, 4, 0], + ] + ).astype(np.float32) + + time_2 = cudf.DataFrame( + [ + [0, 6, 4, 4, 8], + [8, 0, 10, 6, 4], + [6, 14, 0, 2, 2], + [2, 4, 4, 0, 6], + [4, 4, 6, 8, 0], + ] + ).astype(np.float32) + + vehicle_start_loc = cudf.Series([0, 1, 0, 1, 0]) + vehicle_end_loc = cudf.Series([0, 1, 1, 0, 0]) + + vehicle_types = cudf.Series([1, 1, 2, 2, 2]) + vehicle_cap = cudf.Series([30, 30, 10, 10, 10]) + + vehicle_start = cudf.Series([0, 5, 0, 20, 20]) + vehicle_end = cudf.Series([80, 80, 100, 100, 100]) + + vehicle_break_start = cudf.Series([20, 20, 20, 20, 20]) + vehicle_break_end = cudf.Series([25, 25, 25, 25, 25]) + vehicle_break_duration = cudf.Series([1, 1, 1, 1, 1]) + + vehicle_max_costs = cudf.Series([100, 100, 100, 100, 100]).astype( + np.float32 + ) + vehicle_max_times = cudf.Series([120, 120, 120, 120, 120]).astype( + np.float32 + ) + + order_loc = cudf.Series([1, 2, 3, 4]) + demand = cudf.Series([3, 4, 30, 3]) + + task_start = cudf.Series([3, 5, 1, 4]) + task_end = cudf.Series([20, 30, 20, 40]) + serv = cudf.Series([3, 1, 8, 4]) + prizes = cudf.Series([4, 4, 15, 3]) + + dm = routing.DataModel(cost_1.shape[0], len(vehicle_types), len(order_loc)) + + # Cost and Time + dm.add_cost_matrix(cost_1, 1) + dm.add_cost_matrix(cost_2, 2) + dm.add_transit_time_matrix(time_1, 1) + dm.add_transit_time_matrix(time_2, 2) + dm.set_vehicle_types(vehicle_types) + dm.set_vehicle_locations(vehicle_start_loc, vehicle_end_loc) + dm.set_vehicle_time_windows(vehicle_start, vehicle_end) + dm.add_break_dimension( + vehicle_break_start, vehicle_break_end, vehicle_break_duration + ) + dm.set_vehicle_max_costs(vehicle_max_costs) + dm.set_vehicle_max_times(vehicle_max_times) + dm.add_vehicle_order_match(3, cudf.Series([0, 3])) + dm.set_min_vehicles(2) + dm.set_order_locations(order_loc) + dm.add_capacity_dimension("1", demand, vehicle_cap) + dm.set_order_time_windows(task_start, task_end) + dm.set_order_service_times(serv) + dm.add_order_vehicle_match(3, cudf.Series([3])) + dm.add_order_vehicle_match(0, cudf.Series([3])) + dm.set_order_prizes(prizes) + assert (dm.get_order_prizes() == prizes).all() + + sol_set = routing.SolverSettings() + + sol_set.set_time_limit(15) + + sol = routing.Solve(dm, sol_set) + + objectives = sol.get_objective_values() + assert sol.get_total_objective() == -13.0 + assert objectives[routing.Objective.PRIZE] == -26.0 + assert objectives[routing.Objective.COST] == 13.0 + assert sol.get_status() == 0 + assert sol.get_vehicle_count() >= 2 + diff --git a/python/cuopt/cuopt/tests/routing/test_solver_settings.py b/python/cuopt/cuopt/tests/routing/test_solver_settings.py index 3616beb322..cfcb3f6e27 100644 --- a/python/cuopt/cuopt/tests/routing/test_solver_settings.py +++ b/python/cuopt/cuopt/tests/routing/test_solver_settings.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +import numpy as np + import cudf from cuopt import routing @@ -9,33 +11,19 @@ filename = utils.RAPIDS_DATASET_ROOT_DIR + "/solomon/In/r107.txt" -def test_min_vehicles(): - min_vehicles = 10 - d = utils.create_data_model(filename, run_nodes=10) - d.set_min_vehicles(min_vehicles) - - s = routing.SolverSettings() - s.set_time_limit(4) - routing_solution = routing.Solve(d, s) - ret_vehicle_num = d.get_min_vehicles() - - assert ret_vehicle_num == min_vehicles - assert routing_solution.get_vehicle_count() >= min_vehicles - assert routing_solution.get_status() == 0 - - -def test_max_distance(): - d = utils.create_data_model(filename, run_nodes=10) - max_distance = cudf.Series([250.0] * d.get_fleet_size()) - d.set_vehicle_max_costs(max_distance) +def test_verbose_mode(): + """Solve with verbose mode on; assert solution status.""" + cost = cudf.DataFrame( + [[0.0, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]], + dtype=np.float32, + ) + dm = routing.DataModel(3, 1) + dm.add_cost_matrix(cost) s = routing.SolverSettings() - s.set_time_limit(4) - routing_solution = routing.Solve(d, s) - routes = routing_solution.get_route() - trucks = routes["truck_id"].unique() - for i in range(0, len(trucks)): - truck_route = routes[routes["truck_id"] == trucks.iloc[i]] - assert truck_route["arrival_stamp"].iloc[-1] < max_distance[0] + s.set_verbose_mode(True) + s.set_time_limit(2) + solution = routing.Solve(dm, s) + assert solution.get_status() == 0 def test_dump_results(): @@ -51,3 +39,34 @@ def test_dump_results(): ret_interval = s.get_best_results_interval() assert file_path == ret_file_path assert interval == ret_interval + + +def test_solver_settings_getters(): + s = routing.SolverSettings() + time_limit = 10.5 + s.set_time_limit(time_limit) + assert s.get_time_limit() == time_limit + + +def test_dump_config(): + """Test SolverSettings solve with config file""" + s = routing.SolverSettings() + config_file = "solver_cfg.yaml" + s.dump_config_file(config_file) + assert s.get_config_file_name() == config_file + + # Small example data model: 3 locations, 1 vehicle + cost = cudf.DataFrame( + [[0.0, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]], + dtype=np.float32, + ) + dm = routing.DataModel(3, 1) + dm.add_cost_matrix(cost) + s.set_time_limit(2) + routing_solution = routing.Solve(dm, s) + assert routing_solution.get_status() == 0 + + # Load from written solver_cfg.yaml and solve again + dm_from_yaml, s_from_yaml = utils.create_data_model_from_yaml(config_file) + solution_from_yaml = routing.Solve(dm_from_yaml, s_from_yaml) + assert solution_from_yaml.get_status() == 0 diff --git a/python/cuopt/cuopt/tests/routing/test_tsp.py b/python/cuopt/cuopt/tests/routing/test_tsp.py deleted file mode 100644 index e70c0eb0b4..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_tsp.py +++ /dev/null @@ -1,58 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import os - -import numpy as np -import pytest - -from cuopt import routing -from cuopt.routing import utils - -TSP_PATH = os.path.join(utils.RAPIDS_DATASET_ROOT_DIR, "tsp") -DATASETS_TSP = [ - os.path.join(TSP_PATH, "a280.tsp"), - os.path.join(TSP_PATH, "tsp225.tsp"), - os.path.join(TSP_PATH, "ch150.tsp"), -] - - -@pytest.fixture(params=DATASETS_TSP) -def data_(request): - df = utils.create_from_file_tsp(request.param) - file_name = request.param - return df, file_name - - -# TO-DO: Remove this skip once the TSP Link is fixed and issue #609 is closed -@pytest.mark.skip(reason="Skipping TSP tests") -def test_tsp(data_): - df, file_name = data_ - # read reference, if it doesn't exists skip the test - try: - ref_cost, ref_vehicle = utils.read_ref_tsp(file_name, "l1_tsp") - except ValueError: - pytest.skip("Reference could not be found!") - - print(f"Running file {file_name}...") - distances = utils.build_matrix(df) - distances = distances.astype(np.float32) - nodes = df["vertex"].shape[0] - d = routing.DataModel(nodes, 1) - d.add_cost_matrix(distances) - - s = routing.SolverSettings() - utils.set_limits_for_quality(s, nodes) - - routing_solution = routing.Solve(d, s) - final_cost = routing_solution.get_total_objective() - vehicle_count = routing_solution.get_vehicle_count() - cu_route = routing_solution.get_route() - cu_status = routing_solution.get_status() - - # status returns integer instead of enum - assert cu_status == 0 - # FIXME find better error rates - assert (final_cost - ref_cost) / ref_cost < 0.2 - assert cu_route["route"].unique().count() == nodes - assert cu_route["truck_id"].unique().count() == vehicle_count diff --git a/python/cuopt/cuopt/tests/routing/test_validation.py b/python/cuopt/cuopt/tests/routing/test_validation.py deleted file mode 100644 index 869d6506a7..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_validation.py +++ /dev/null @@ -1,93 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import pytest - -import cudf - -from cuopt import routing - - -# Validates cost matrix exceptions -def test_dist_mat(): - cost_matrix = cudf.DataFrame( - [ - [0, 5.0, 5.0, 5.0], - [5.0, 0, 5.0, 5.0], - [5.0, 5.0, 0, 5.0], - [5.0, -5.0, 5.0, 0], - ] - ) - with pytest.raises(Exception) as exc: - dm = routing.DataModel(3, 3) - dm.add_cost_matrix(cost_matrix) - assert ( - str(exc.value) - == "Number of locations doesn't match number of locations in matrix" - ) - with pytest.raises(Exception) as exc: - dm = routing.DataModel(cost_matrix.shape[0], 3) - dm.add_cost_matrix(cost_matrix[:3]) - assert str(exc.value) == "cost matrix is expected to be a square matrix" - with pytest.raises(Exception) as exc: - dm = routing.DataModel(cost_matrix.shape[0], 3) - dm.add_cost_matrix(cost_matrix) - assert ( - str(exc.value) - == "All values in cost matrix must be greater than or equal to zero" - ) - - -# Validates non_negative, size and earliest= break_times_1[break_dim][0] - assert arrival_time <= break_times_1[break_dim][1] - else: - assert arrival_time >= break_times_2[break_dim][0] - assert arrival_time <= break_times_2[break_dim][1] - counters[truck_id] = counters[truck_id] + 1 - - # Make sure the achieved number of breaks is same as the specified - for truck_id, num_breaks in counters.items(): - if truck_id < num_v_type_1: - assert num_breaks == num_breaks_1 - else: - assert num_breaks == num_breaks_2 diff --git a/python/cuopt/cuopt/tests/routing/test_vehicle_dependent_service_times.py b/python/cuopt/cuopt/tests/routing/test_vehicle_dependent_service_times.py deleted file mode 100644 index a8f8d3cbd7..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_vehicle_dependent_service_times.py +++ /dev/null @@ -1,99 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import numpy as np - -import cudf - -from cuopt import routing - - -def check_cuopt_solution( - routing_solution, - distance_matrix, - time_matrix, - earliest_time, - latest_time, - v_service_times, -): - th = 0.001 - df_distance_matrix = distance_matrix.to_pandas().values - df_time_matrix = time_matrix.to_pandas().values - df_earliest_time = earliest_time.to_pandas().values - df_latest_time = latest_time.to_pandas().values - routes = routing_solution.get_route() - computed_cost = 0 - - for truck_id, assign in enumerate( - routes["truck_id"].unique().to_arrow().to_pylist() - ): - solution_vehicle_x = routes[routes["truck_id"] == assign] - vehicle_x_total_time = float(solution_vehicle_x["arrival_stamp"].max()) - arrival_time = 0 - curr_route = solution_vehicle_x["route"].to_arrow().to_pylist() - for i in range(len(curr_route) - 1): - travel_time = df_time_matrix[curr_route[i]][curr_route[i + 1]] - arrival_time += ( - travel_time + v_service_times[assign][curr_route[i]] - ) - arrival_time = max( - arrival_time, df_earliest_time[curr_route[i + 1]] - ) - computed_cost += df_distance_matrix[curr_route[i]][ - curr_route[i + 1] - ] - assert arrival_time <= df_latest_time[curr_route[i + 1]] - assert abs(vehicle_x_total_time - arrival_time) < th - assert abs(routing_solution.get_total_objective() - computed_cost) < th - - -def test_vehicle_dependent_service_times(): - """ - Test mixed fleet service times - """ - - costs = cudf.DataFrame( - { - 0: [0, 3, 4, 5, 2], - 1: [1, 0, 3, 2, 7], - 2: [10, 5, 0, 2, 9], - 3: [3, 11, 1, 0, 6], - 4: [5, 3, 8, 6, 0], - }, - dtype=np.float32, - ) - vehicle_num = 2 - earliest_time = cudf.Series([0, 0, 0, 0, 0], dtype=np.int32) - latest_time = cudf.Series( - [60000, 60000, 60000, 60000, 60000], dtype=np.int32 - ) - service_times = { - 0: [0, 5, 55, 3, 1], - 1: [0, 2, 100, 46, 96], - } - - pickup_orders = cudf.Series([1, 2]) - delivery_orders = cudf.Series([3, 4]) - - d = routing.DataModel(costs.shape[0], vehicle_num) - d.add_cost_matrix(costs) - d.set_pickup_delivery_pairs(pickup_orders, delivery_orders) - d.set_order_time_windows(earliest_time, latest_time) - for vehicle_id, v_service_times in service_times.items(): - d.set_order_service_times(cudf.Series(v_service_times), vehicle_id) - d.set_min_vehicles(2) - - settings = routing.SolverSettings() - settings.set_time_limit(2) - - routing_solution = routing.Solve(d, settings) - cu_status = routing_solution.get_status() - assert cu_status == 0 - check_cuopt_solution( - routing_solution, - costs, - costs, - earliest_time, - latest_time, - service_times, - ) diff --git a/python/cuopt/cuopt/tests/routing/test_vehicle_fixed_costs.py b/python/cuopt/cuopt/tests/routing/test_vehicle_fixed_costs.py deleted file mode 100644 index 688ddb8862..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_vehicle_fixed_costs.py +++ /dev/null @@ -1,52 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import cudf - -from cuopt import routing - - -def test_vehicle_fixed_costs(): - """ - Test mixed fleet fixed cost per vehicle - """ - - costs = cudf.DataFrame( - { - 0: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1], - 1: [1, 0, 1, 1, 1, 1, 1, 1, 1, 1], - 2: [1, 1, 0, 1, 1, 1, 1, 1, 1, 1], - 3: [1, 1, 1, 0, 1, 1, 1, 1, 1, 1], - 4: [1, 1, 1, 1, 0, 1, 1, 1, 1, 1], - 5: [1, 1, 1, 1, 1, 0, 1, 1, 1, 1], - 6: [1, 1, 1, 1, 1, 1, 0, 1, 1, 1], - 7: [1, 1, 1, 1, 1, 1, 1, 0, 1, 1], - 8: [1, 1, 1, 1, 1, 1, 1, 1, 0, 1], - 9: [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - } - ) - - vehicle_num = 16 - vehicle_fixed_costs = cudf.Series( - [16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 1, 1, 1] - ) - demand = cudf.Series([0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) - capacities = cudf.Series([2] * vehicle_num) - - d = routing.DataModel(costs.shape[0], vehicle_num) - d.add_cost_matrix(costs) - d.add_capacity_dimension("demand", demand, capacities) - d.set_vehicle_fixed_costs(vehicle_fixed_costs) - - s = routing.SolverSettings() - s.set_time_limit(3) - - routing_solution = routing.Solve(d, s) - routing_solution.display_routes() - cu_status = routing_solution.get_status() - objectives = routing_solution.get_objective_values() - - assert cu_status == 0 - assert routing_solution.get_total_objective() == 49 - assert objectives[routing.Objective.VEHICLE_FIXED_COST] == 35 - assert objectives[routing.Objective.COST] == 14 diff --git a/python/cuopt/cuopt/tests/routing/test_vehicle_max_cost.py b/python/cuopt/cuopt/tests/routing/test_vehicle_max_cost.py deleted file mode 100644 index 5d3cb1861e..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_vehicle_max_cost.py +++ /dev/null @@ -1,50 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import cudf - -from cuopt import routing - - -def test_vehicle_max_costs(): - """ - Test mixed fleet max cost per vehicle - """ - - costs = cudf.DataFrame( - { - 0: [0, 3, 4, 5, 2], - 1: [1, 0, 3, 2, 7], - 2: [10, 5, 0, 2, 9], - 3: [3, 11, 1, 0, 6], - 4: [5, 3, 8, 6, 0], - } - ) - - vehicle_num = 4 - vehicle_max_costs = cudf.Series([11, 12, 11, 15]) - - d = routing.DataModel(costs.shape[0], vehicle_num) - d.add_cost_matrix(costs) - d.set_vehicle_max_costs(vehicle_max_costs) - - s = routing.SolverSettings() - s.set_time_limit(1) - - routing_solution = routing.Solve(d, s) - cu_status = routing_solution.get_status() - solution_cudf = routing_solution.get_route() - - assert cu_status == 0 - - for i, assign in enumerate( - solution_cudf["truck_id"].unique().to_arrow().to_pylist() - ): - curr_route_dist = 0 - solution_vehicle_x = solution_cudf[solution_cudf["truck_id"] == assign] - h_route = solution_vehicle_x["route"].to_arrow().to_pylist() - route_len = len(h_route) - for j in range(route_len - 1): - curr_route_dist += costs.iloc[h_route[j], h_route[j + 1]] - - assert curr_route_dist < vehicle_max_costs[assign] + 0.001 diff --git a/python/cuopt/cuopt/tests/routing/test_vehicle_max_time.py b/python/cuopt/cuopt/tests/routing/test_vehicle_max_time.py deleted file mode 100644 index 5e8e45c66c..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_vehicle_max_time.py +++ /dev/null @@ -1,81 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import numpy as np - -import cudf - -from cuopt import routing -from cuopt.routing.vehicle_routing_wrapper import ErrorStatus - - -def test_vehicle_max_times_fail(): - costs = cudf.DataFrame( - { - 0: [0, 3, 4, 5, 2], - 1: [1, 0, 3, 2, 7], - 2: [10, 5, 0, 2, 9], - 3: [3, 11, 1, 0, 6], - 4: [5, 3, 8, 6, 0], - }, - dtype=np.float32, - ) - vehicle_num = 4 - vehicle_max_times = cudf.Series([100, 30, 50, 70], dtype=np.float32) - - d = routing.DataModel(costs.shape[0], vehicle_num) - d.add_cost_matrix(costs) - d.set_vehicle_max_times(vehicle_max_times) - - s = routing.SolverSettings() - s.set_time_limit(1) - - routing_solution = routing.Solve(d, s) - assert routing_solution.get_error_status() == ErrorStatus.ValidationError - - -def test_vehicle_max_times(): - """ - Test mixed fleet max time per vehicle - """ - - costs = cudf.DataFrame( - { - 0: [0, 3, 4, 5, 2], - 1: [1, 0, 3, 2, 7], - 2: [10, 5, 0, 2, 9], - 3: [3, 11, 1, 0, 6], - 4: [5, 3, 8, 6, 0], - }, - dtype=np.float32, - ) - times = costs * 10 - - vehicle_num = 4 - vehicle_max_times = cudf.Series([100, 30, 50, 70], dtype=np.float32) - - d = routing.DataModel(costs.shape[0], vehicle_num) - d.add_cost_matrix(costs) - d.add_transit_time_matrix(times) - d.set_vehicle_max_times(vehicle_max_times) - - s = routing.SolverSettings() - s.set_time_limit(10) - - routing_solution = routing.Solve(d, s) - cu_status = routing_solution.get_status() - solution_cudf = routing_solution.get_route() - - assert cu_status == 0 - - for i, assign in enumerate( - solution_cudf["truck_id"].unique().to_arrow().to_pylist() - ): - curr_route_time = 0 - solution_vehicle_x = solution_cudf[solution_cudf["truck_id"] == assign] - h_route = solution_vehicle_x["route"].to_arrow().to_pylist() - route_len = len(h_route) - for j in range(route_len - 1): - curr_route_time += times.iloc[h_route[j], h_route[j + 1]] - - assert curr_route_time < vehicle_max_times[assign] + 0.001 diff --git a/python/cuopt/cuopt/tests/routing/test_vehicle_order_match.py b/python/cuopt/cuopt/tests/routing/test_vehicle_order_match.py deleted file mode 100644 index 444971e092..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_vehicle_order_match.py +++ /dev/null @@ -1,109 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import cudf - -from cuopt import routing - - -def test_order_to_vehicle_match(): - n_vehicles = 3 - n_locations = 4 - time_mat = [[0, 1, 5, 2], [2, 0, 7, 4], [1, 5, 0, 9], [5, 6, 2, 0]] - - order_vehicle_match = {1: [0], 3: [0], 2: [1]} - - d = routing.DataModel(n_locations, n_vehicles) - d.add_cost_matrix(cudf.DataFrame(time_mat)) - - for order, vehicles in order_vehicle_match.items(): - d.add_order_vehicle_match(order, cudf.Series(vehicles)) - - s = routing.SolverSettings() - s.set_time_limit(10) - - routing_solution = routing.Solve(d, s) - vehicle_count = routing_solution.get_vehicle_count() - cu_route = routing_solution.get_route() - cu_status = routing_solution.get_status() - - assert cu_status == 0 - assert vehicle_count == 2 - - route_ids = cu_route["route"].to_arrow().to_pylist() - truck_ids = cu_route["truck_id"].to_arrow().to_pylist() - - for i in range(len(route_ids)): - order = route_ids[i] - if order == 1 or order == 3: - assert truck_ids[i] == 0 - if order == 2: - assert truck_ids[i] == 1 - - -def test_vehicle_to_order_match(): - """ - A user might have the vehicle to order match instead of - order to vehicle match, in those cases, we can use - cudf.DataFrame.transpose to feed the data_model - """ - n_vehicles = 3 - n_locations = 4 - time_mat = [[0, 1, 5, 2], [2, 0, 7, 4], [1, 5, 0, 9], [5, 6, 2, 0]] - - # Force one vehicle to pick only one order - vehicle_order_match = {0: [1], 1: [2], 2: [3]} - - d = routing.DataModel(n_locations, n_vehicles) - d.add_cost_matrix(cudf.DataFrame(time_mat)) - - for vehicle, orders in vehicle_order_match.items(): - d.add_vehicle_order_match(vehicle, cudf.Series(orders)) - - s = routing.SolverSettings() - s.set_time_limit(10) - - routing_solution = routing.Solve(d, s) - vehicle_count = routing_solution.get_vehicle_count() - cu_route = routing_solution.get_route() - cu_status = routing_solution.get_status() - - assert cu_status == 0 - assert vehicle_count == 3 - - route_ids = cu_route["route"].to_arrow().to_pylist() - truck_ids = cu_route["truck_id"].to_arrow().to_pylist() - - for i in range(len(route_ids)): - order = route_ids[i] - if order > 0: - assert truck_ids[i] == order - 1 - - -def test_single_vehicle_with_match(): - """ - This is a corner case test when there is only one vehicle present - """ - n_vehicles = 1 - n_locations = 4 - n_orders = 3 - time_mat = [[0, 1, 5, 2], [2, 0, 7, 4], [1, 5, 0, 9], [5, 6, 2, 0]] - - order_vehicle_match = {0: [0], 1: [0], 2: [0]} - - d = routing.DataModel(n_locations, n_vehicles, n_orders) - d.add_cost_matrix(cudf.DataFrame(time_mat)) - - d.set_order_locations(cudf.Series([1, 2, 3])) - for order, vehicles in order_vehicle_match.items(): - d.add_order_vehicle_match(order, cudf.Series(vehicles)) - - s = routing.SolverSettings() - s.set_time_limit(5) - - routing_solution = routing.Solve(d, s) - vehicle_count = routing_solution.get_vehicle_count() - cu_status = routing_solution.get_status() - - assert cu_status == 0 - assert vehicle_count == 1 diff --git a/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py b/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py index 550af63fef..d1a33b0e2e 100644 --- a/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py +++ b/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py @@ -7,10 +7,363 @@ from cuopt import routing from cuopt.routing import utils +from cuopt.routing.vehicle_routing_wrapper import ErrorStatus filename = utils.RAPIDS_DATASET_ROOT_DIR + "/solomon/In/r107.txt" +# ----- Vehicle types ----- + + +def test_vehicle_types(): + bikes_type = 1 + car_type = 2 + + bikes_cost = cudf.DataFrame([[0, 4, 4], [4, 0, 4], [4, 4, 0]]) + bikes_time = cudf.DataFrame([[0, 50, 50], [50, 0, 50], [50, 50, 0]]) + car_cost = cudf.DataFrame([[0, 1, 1], [1, 0, 1], [1, 1, 0]]) + car_time = cudf.DataFrame([[0, 10, 10], [10, 0, 10], [10, 10, 0]]) + vehicle_types = cudf.Series([bikes_type, car_type]) + + dm = routing.DataModel(3, 2) + dm.add_cost_matrix(bikes_cost, bikes_type) + dm.add_transit_time_matrix(bikes_time, bikes_type) + dm.add_cost_matrix(car_cost, car_type) + dm.add_transit_time_matrix(car_time, car_type) + dm.set_vehicle_types(vehicle_types) + dm.set_min_vehicles(2) + assert (dm.get_vehicle_types() == vehicle_types).all() + + s = routing.SolverSettings() + s.set_time_limit(1) + + sol = routing.Solve(dm, s) + + cost = sol.get_total_objective() + cu_status = sol.get_status() + vehicle_count = sol.get_vehicle_count() + assert cu_status == 0 + assert vehicle_count == 2 + assert cost == 10 + solution_cudf = sol.get_route() + + for i, assign in enumerate( + solution_cudf["truck_id"].unique().to_arrow().to_pylist() + ): + solution_vehicle_x = solution_cudf[solution_cudf["truck_id"] == assign] + vehicle_x_start_time = round( + float(solution_vehicle_x["arrival_stamp"].min()), 2 + ) + vehicle_x_final_time = round( + float(solution_vehicle_x["arrival_stamp"].max()), 2 + ) + vehicle_x_total_time = round( + vehicle_x_final_time - vehicle_x_start_time, 2 + ) + + if vehicle_types[assign] == bikes_type: + assert abs(vehicle_x_total_time - 100) < 0.01 + + if vehicle_types[assign] == car_type: + assert abs(vehicle_x_total_time - 20) < 0.01 + + +# ----- Vehicle fixed costs ----- + + +def test_vehicle_fixed_costs(): + """ + Test mixed fleet fixed cost per vehicle + """ + + costs = cudf.DataFrame( + { + 0: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1], + 1: [1, 0, 1, 1, 1, 1, 1, 1, 1, 1], + 2: [1, 1, 0, 1, 1, 1, 1, 1, 1, 1], + 3: [1, 1, 1, 0, 1, 1, 1, 1, 1, 1], + 4: [1, 1, 1, 1, 0, 1, 1, 1, 1, 1], + 5: [1, 1, 1, 1, 1, 0, 1, 1, 1, 1], + 6: [1, 1, 1, 1, 1, 1, 0, 1, 1, 1], + 7: [1, 1, 1, 1, 1, 1, 1, 0, 1, 1], + 8: [1, 1, 1, 1, 1, 1, 1, 1, 0, 1], + 9: [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + } + ) + + vehicle_num = 16 + vehicle_fixed_costs = cudf.Series( + [16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 1, 1, 1] + ) + demand = cudf.Series([0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + capacities = cudf.Series([2] * vehicle_num) + + d = routing.DataModel(costs.shape[0], vehicle_num) + d.add_cost_matrix(costs) + d.add_capacity_dimension("demand", demand, capacities) + d.set_vehicle_fixed_costs(vehicle_fixed_costs) + assert (d.get_vehicle_fixed_costs() == vehicle_fixed_costs).all() + + s = routing.SolverSettings() + s.set_time_limit(3) + + routing_solution = routing.Solve(d, s) + cu_status = routing_solution.get_status() + objectives = routing_solution.get_objective_values() + + assert cu_status == 0 + assert routing_solution.get_total_objective() == 49 + assert objectives[routing.Objective.VEHICLE_FIXED_COST] == 35 + assert objectives[routing.Objective.COST] == 14 + + +# ----- Vehicle max cost ----- + + +def test_vehicle_max_costs(): + """ + Test mixed fleet max cost per vehicle + """ + + costs = cudf.DataFrame( + { + 0: [0, 3, 4, 5, 2], + 1: [1, 0, 3, 2, 7], + 2: [10, 5, 0, 2, 9], + 3: [3, 11, 1, 0, 6], + 4: [5, 3, 8, 6, 0], + } + ) + + vehicle_num = 4 + vehicle_max_costs = cudf.Series([11, 12, 11, 15]) + + d = routing.DataModel(costs.shape[0], vehicle_num) + d.add_cost_matrix(costs) + d.set_vehicle_max_costs(vehicle_max_costs) + assert (d.get_vehicle_max_costs() == vehicle_max_costs).all() + + s = routing.SolverSettings() + s.set_time_limit(1) + + routing_solution = routing.Solve(d, s) + cu_status = routing_solution.get_status() + solution_cudf = routing_solution.get_route() + + assert cu_status == 0 + + for i, assign in enumerate( + solution_cudf["truck_id"].unique().to_arrow().to_pylist() + ): + curr_route_dist = 0 + solution_vehicle_x = solution_cudf[solution_cudf["truck_id"] == assign] + h_route = solution_vehicle_x["route"].to_arrow().to_pylist() + route_len = len(h_route) + for j in range(route_len - 1): + curr_route_dist += costs.iloc[h_route[j], h_route[j + 1]] + + assert curr_route_dist < vehicle_max_costs[assign] + 0.001 + + +# ----- Vehicle max time ----- + + +def test_vehicle_max_times_fail(): + costs = cudf.DataFrame( + { + 0: [0, 3, 4, 5, 2], + 1: [1, 0, 3, 2, 7], + 2: [10, 5, 0, 2, 9], + 3: [3, 11, 1, 0, 6], + 4: [5, 3, 8, 6, 0], + }, + dtype=np.float32, + ) + vehicle_num = 4 + vehicle_max_times = cudf.Series([100, 30, 50, 70], dtype=np.float32) + + d = routing.DataModel(costs.shape[0], vehicle_num) + d.add_cost_matrix(costs) + d.set_vehicle_max_times(vehicle_max_times) + + s = routing.SolverSettings() + s.set_time_limit(1) + + routing_solution = routing.Solve(d, s) + assert routing_solution.get_error_status() == ErrorStatus.ValidationError + err_msg = routing_solution.get_error_message().decode() + assert "Time matrix should be set in order to use vehicle max time constraints" in err_msg + + +def test_vehicle_max_times(): + """ + Test mixed fleet max time per vehicle + """ + + costs = cudf.DataFrame( + { + 0: [0, 3, 4, 5, 2], + 1: [1, 0, 3, 2, 7], + 2: [10, 5, 0, 2, 9], + 3: [3, 11, 1, 0, 6], + 4: [5, 3, 8, 6, 0], + }, + dtype=np.float32, + ) + times = costs * 10 + + vehicle_num = 4 + vehicle_max_times = cudf.Series([100, 30, 50, 70], dtype=np.float32) + + d = routing.DataModel(costs.shape[0], vehicle_num) + d.add_cost_matrix(costs) + d.add_transit_time_matrix(times) + d.set_vehicle_max_times(vehicle_max_times) + assert (d.get_vehicle_max_times() == vehicle_max_times).all() + + s = routing.SolverSettings() + s.set_time_limit(10) + + routing_solution = routing.Solve(d, s) + cu_status = routing_solution.get_status() + solution_cudf = routing_solution.get_route() + + assert cu_status == 0 + + for i, assign in enumerate( + solution_cudf["truck_id"].unique().to_arrow().to_pylist() + ): + curr_route_time = 0 + solution_vehicle_x = solution_cudf[solution_cudf["truck_id"] == assign] + h_route = solution_vehicle_x["route"].to_arrow().to_pylist() + route_len = len(h_route) + for j in range(route_len - 1): + curr_route_time += times.iloc[h_route[j], h_route[j + 1]] + + assert curr_route_time < vehicle_max_times[assign] + 0.001 + + +# ----- Order / vehicle match ----- + + +def test_order_to_vehicle_match(): + n_vehicles = 3 + n_locations = 4 + time_mat = [[0, 1, 5, 2], [2, 0, 7, 4], [1, 5, 0, 9], [5, 6, 2, 0]] + + order_vehicle_match = {1: [0], 3: [0], 2: [1]} + + d = routing.DataModel(n_locations, n_vehicles) + d.add_cost_matrix(cudf.DataFrame(time_mat)) + + for order, vehicles in order_vehicle_match.items(): + d.add_order_vehicle_match(order, cudf.Series(vehicles)) + + ret_order_vehicle = d.get_order_vehicle_match() + assert set(ret_order_vehicle.keys()) == set(order_vehicle_match.keys()) + for order, vehicles in order_vehicle_match.items(): + assert (ret_order_vehicle[order].to_arrow().to_pylist() == vehicles) + + s = routing.SolverSettings() + s.set_time_limit(10) + + routing_solution = routing.Solve(d, s) + vehicle_count = routing_solution.get_vehicle_count() + cu_route = routing_solution.get_route() + cu_status = routing_solution.get_status() + + assert cu_status == 0 + assert vehicle_count == 2 + + route_ids = cu_route["route"].to_arrow().to_pylist() + truck_ids = cu_route["truck_id"].to_arrow().to_pylist() + + for i in range(len(route_ids)): + order = route_ids[i] + if order == 1 or order == 3: + assert truck_ids[i] == 0 + if order == 2: + assert truck_ids[i] == 1 + + +def test_vehicle_to_order_match(): + """ + A user might have the vehicle to order match instead of + order to vehicle match, in those cases, we can use + cudf.DataFrame.transpose to feed the data_model + """ + n_vehicles = 3 + n_locations = 4 + time_mat = [[0, 1, 5, 2], [2, 0, 7, 4], [1, 5, 0, 9], [5, 6, 2, 0]] + + # Force one vehicle to pick only one order + vehicle_order_match = {0: [1], 1: [2], 2: [3]} + + d = routing.DataModel(n_locations, n_vehicles) + d.add_cost_matrix(cudf.DataFrame(time_mat)) + + for vehicle, orders in vehicle_order_match.items(): + d.add_vehicle_order_match(vehicle, cudf.Series(orders)) + + ret_vehicle_order = d.get_vehicle_order_match() + assert set(ret_vehicle_order.keys()) == set(vehicle_order_match.keys()) + for vehicle, orders in vehicle_order_match.items(): + assert (ret_vehicle_order[vehicle].to_arrow().to_pylist() == orders) + + s = routing.SolverSettings() + s.set_time_limit(10) + + routing_solution = routing.Solve(d, s) + vehicle_count = routing_solution.get_vehicle_count() + cu_route = routing_solution.get_route() + cu_status = routing_solution.get_status() + + assert cu_status == 0 + assert vehicle_count == 3 + + route_ids = cu_route["route"].to_arrow().to_pylist() + truck_ids = cu_route["truck_id"].to_arrow().to_pylist() + + for i in range(len(route_ids)): + order = route_ids[i] + if order > 0: + assert truck_ids[i] == order - 1 + + +def test_single_vehicle_with_match(): + """ + This is a corner case test when there is only one vehicle present + """ + n_vehicles = 1 + n_locations = 4 + n_orders = 3 + time_mat = [[0, 1, 5, 2], [2, 0, 7, 4], [1, 5, 0, 9], [5, 6, 2, 0]] + + order_vehicle_match = {0: [0], 1: [0], 2: [0]} + + d = routing.DataModel(n_locations, n_vehicles, n_orders) + d.add_cost_matrix(cudf.DataFrame(time_mat)) + + order_loc = cudf.Series([1, 2, 3]) + d.set_order_locations(order_loc) + for order, vehicles in order_vehicle_match.items(): + d.add_order_vehicle_match(order, cudf.Series(vehicles)) + assert (d.get_order_locations() == order_loc).all() + + s = routing.SolverSettings() + s.set_time_limit(5) + + routing_solution = routing.Solve(d, s) + vehicle_count = routing_solution.get_vehicle_count() + cu_status = routing_solution.get_status() + + assert cu_status == 0 + assert vehicle_count == 1 + + +# ----- Vehicle time windows and locations ----- + + def test_time_windows(): vehicle_num = 5 d = utils.create_data_model( @@ -80,3 +433,416 @@ def test_vehicle_locations(): vehicle_route = routes[routes["truck_id"] == truck_id] assert vehicle_route["location"].iloc[0] == 4 assert vehicle_route["location"].iloc[-1] == 10 + + +# ----- Vehicle breaks ----- + + +def test_uniform_breaks(): + vehicle_num = 25 + run_nodes = 100 + nodes = run_nodes + 1 + d = utils.create_data_model( + filename, run_nodes=run_nodes, num_vehicles=vehicle_num + ) + + break_times = [[40, 50], [170, 180]] + + num_breaks = len(break_times) + vehicle_breaks_earliest = np.zeros([vehicle_num, num_breaks]) + vehicle_breaks_latest = np.zeros([vehicle_num, num_breaks]) + vehicle_breaks_duration = np.zeros([vehicle_num, num_breaks]) + for b in range(num_breaks): + break_begin = break_times[b][0] + break_end = break_times[b][1] + break_duration = break_end - break_begin + vehicle_breaks_earliest[:, b] = [break_begin] * vehicle_num + vehicle_breaks_latest[:, b] = [break_begin] * vehicle_num + vehicle_breaks_duration[:, b] = [break_duration] * vehicle_num + + # Add all nodes as the vehicle break location + break_locations = cudf.Series([i for i in range(nodes)]) + + d.set_break_locations(break_locations) + for b in range(num_breaks): + d.add_break_dimension( + cudf.Series(vehicle_breaks_earliest[:, b]).astype(np.int32), + cudf.Series(vehicle_breaks_latest[:, b]).astype(np.int32), + cudf.Series(vehicle_breaks_duration[:, b]).astype(np.int32), + ) + + s = routing.SolverSettings() + s.set_time_limit(30) + routing_solution = routing.Solve(d, s) + ret_break_locations = d.get_break_locations() + ret_break_dimensions = d.get_break_dimensions() + + assert (ret_break_locations == break_locations).all() + + for b, break_dimension in enumerate(ret_break_dimensions.keys()): + vehicle_break = ret_break_dimensions[break_dimension] + assert ( + vehicle_break["earliest"] + == cudf.Series(vehicle_breaks_earliest[:, b]) + ).all() + assert ( + vehicle_break["latest"] == cudf.Series(vehicle_breaks_latest[:, b]) + ).all() + assert ( + vehicle_break["duration"] + == cudf.Series(vehicle_breaks_duration[:, b]) + ).all() + + # TO DO: Check if breaks are adhered to + assert routing_solution.get_status() == 0 + + +def test_non_uniform_breaks(): + vehicle_num = 30 + run_nodes = 100 + nodes = run_nodes + 1 + d = utils.create_data_model( + filename, run_nodes=run_nodes, num_vehicles=vehicle_num + ) + + num_v_type_1 = int(vehicle_num / 2) + break_times_1 = [[40, 50], [100, 120], [170, 180]] + break_durations_1 = [5, 20, 10] + + num_v_type_2 = vehicle_num - num_v_type_1 + break_times_2 = [[60, 90], [110, 120], [200, 210]] + break_durations_2 = [20, 10, 5] + + num_breaks = 3 + vehicle_breaks_earliest = np.zeros([vehicle_num, num_breaks]) + vehicle_breaks_latest = np.zeros([vehicle_num, num_breaks]) + vehicle_breaks_duration = np.zeros([vehicle_num, num_breaks]) + for b in range(num_breaks): + vehicle_breaks_earliest[:, b] = [ + break_times_1[b][0] + ] * num_v_type_1 + [break_times_2[b][0]] * num_v_type_2 + vehicle_breaks_latest[:, b] = [break_times_1[b][1]] * num_v_type_1 + [ + break_times_2[b][1] + ] * num_v_type_2 + vehicle_breaks_duration[:, b] = [ + break_durations_1[b] + ] * num_v_type_1 + [break_durations_2[b]] * num_v_type_2 + + # Depot should not be a break node + break_locations = cudf.Series([i + 1 for i in range(nodes - 1)]) + + d.set_break_locations(break_locations) + for b in range(num_breaks): + d.add_break_dimension( + cudf.Series(vehicle_breaks_earliest[:, b]).astype(np.int32), + cudf.Series(vehicle_breaks_latest[:, b]).astype(np.int32), + cudf.Series(vehicle_breaks_duration[:, b]).astype(np.int32), + ) + + s = routing.SolverSettings() + s.set_time_limit(30) + routing_solution = routing.Solve(d, s) + ret_break_locations = d.get_break_locations() + ret_break_dimensions = d.get_break_dimensions() + + assert (ret_break_locations == break_locations).all() + + for b, break_dimension in enumerate(ret_break_dimensions.keys()): + vehicle_break = ret_break_dimensions[break_dimension] + assert ( + vehicle_break["earliest"] + == cudf.Series(vehicle_breaks_earliest[:, b]) + ).all() + assert ( + vehicle_break["latest"] == cudf.Series(vehicle_breaks_latest[:, b]) + ).all() + assert ( + vehicle_break["duration"] + == cudf.Series(vehicle_breaks_duration[:, b]) + ).all() + + # TO DO: Check if breaks are adhered to + assert routing_solution.get_status() == 0 + + +def test_heterogenous_breaks(): + vehicle_num = 20 + run_nodes = 100 + d = utils.create_data_model( + filename, run_nodes=run_nodes, num_vehicles=vehicle_num + ) + + """ + Half of vehicles have three breaks and the remaining half have two breaks. + Break locations are also different. First set of vehicles have specified + subset of locations while the second set of vehicles have default, i.e. any + location can be a break + """ + num_breaks_1 = 2 + num_v_type_1 = int(vehicle_num / 2) + break_times_1 = [[90, 100], [150, 170]] + break_durations_1 = [15, 15] + break_locations_1 = cudf.Series([5 * i for i in range(1, 18)]) + + num_breaks_2 = 3 + num_v_type_2 = vehicle_num - num_v_type_1 + break_times_2 = [[40, 50], [110, 120], [160, 170]] + break_durations_2 = [10, 10, 10] + + for i in range(num_v_type_1): + for b in range(num_breaks_1): + d.add_vehicle_break( + i, + break_times_1[b][0], + break_times_1[b][1], + break_durations_1[b], + break_locations_1, + ) + + for i in range(num_v_type_2): + for b in range(num_breaks_2): + d.add_vehicle_break( + i + num_v_type_1, + break_times_2[b][0], + break_times_2[b][1], + break_durations_2[b], + ) + + ret_non_uniform = d.get_non_uniform_breaks() + assert len(ret_non_uniform) == vehicle_num + for i in range(num_v_type_1): + assert len(ret_non_uniform[i]) == num_breaks_1 + for i in range(num_v_type_2): + assert len(ret_non_uniform[num_v_type_1 + i]) == num_breaks_2 + + s = routing.SolverSettings() + s.set_time_limit(30) + routing_solution = routing.Solve(d, s) + + # TO DO: Check if breaks are adhered to + assert routing_solution.get_status() == 0 + counters = {} + routes = routing_solution.get_route().to_pandas() + break_locations_1_list = break_locations_1.to_arrow().to_pylist() + # make sure the break locations are the right ones and + # the arrival stamps satisfy the break time constraints + for i in range(routes.shape[0]): + truck_id = routes["truck_id"][i] + if truck_id not in counters: + counters[truck_id] = 0 + if routes["type"][i] == "Break": + break_dim = routes["route"][i] + location = routes["location"][i] + arrival_time = routes["arrival_stamp"][i] + if truck_id < num_v_type_1: + assert location in break_locations_1_list + assert arrival_time >= break_times_1[break_dim][0] + assert arrival_time <= break_times_1[break_dim][1] + else: + assert arrival_time >= break_times_2[break_dim][0] + assert arrival_time <= break_times_2[break_dim][1] + counters[truck_id] = counters[truck_id] + 1 + + # Make sure the achieved number of breaks is same as the specified + for truck_id, num_breaks in counters.items(): + if truck_id < num_v_type_1: + assert num_breaks == num_breaks_1 + else: + assert num_breaks == num_breaks_2 + + +# ----- Vehicle dependent service times ----- + + +def _check_cuopt_solution( + routing_solution, + distance_matrix, + time_matrix, + earliest_time, + latest_time, + v_service_times, +): + th = 0.001 + df_distance_matrix = distance_matrix.to_pandas().values + df_time_matrix = time_matrix.to_pandas().values + df_earliest_time = earliest_time.to_pandas().values + df_latest_time = latest_time.to_pandas().values + routes = routing_solution.get_route() + computed_cost = 0 + + for truck_id, assign in enumerate( + routes["truck_id"].unique().to_arrow().to_pylist() + ): + solution_vehicle_x = routes[routes["truck_id"] == assign] + vehicle_x_total_time = float(solution_vehicle_x["arrival_stamp"].max()) + arrival_time = 0 + curr_route = solution_vehicle_x["route"].to_arrow().to_pylist() + for i in range(len(curr_route) - 1): + travel_time = df_time_matrix[curr_route[i]][curr_route[i + 1]] + arrival_time += ( + travel_time + v_service_times[assign][curr_route[i]] + ) + arrival_time = max( + arrival_time, df_earliest_time[curr_route[i + 1]] + ) + computed_cost += df_distance_matrix[curr_route[i]][ + curr_route[i + 1] + ] + assert arrival_time <= df_latest_time[curr_route[i + 1]] + assert abs(vehicle_x_total_time - arrival_time) < th + assert abs(routing_solution.get_total_objective() - computed_cost) < th + + +def test_vehicle_dependent_service_times(): + """ + Test mixed fleet service times + """ + + costs = cudf.DataFrame( + { + 0: [0, 3, 4, 5, 2], + 1: [1, 0, 3, 2, 7], + 2: [10, 5, 0, 2, 9], + 3: [3, 11, 1, 0, 6], + 4: [5, 3, 8, 6, 0], + }, + dtype=np.float32, + ) + vehicle_num = 2 + earliest_time = cudf.Series([0, 0, 0, 0, 0], dtype=np.int32) + latest_time = cudf.Series( + [60000, 60000, 60000, 60000, 60000], dtype=np.int32 + ) + service_times = { + 0: [0, 5, 55, 3, 1], + 1: [0, 2, 100, 46, 96], + } + + pickup_orders = cudf.Series([1, 2]) + delivery_orders = cudf.Series([3, 4]) + + d = routing.DataModel(costs.shape[0], vehicle_num) + d.add_cost_matrix(costs) + d.set_pickup_delivery_pairs(pickup_orders, delivery_orders) + d.set_order_time_windows(earliest_time, latest_time) + for vehicle_id, v_service_times in service_times.items(): + d.set_order_service_times(cudf.Series(v_service_times), vehicle_id) + d.set_min_vehicles(2) + + settings = routing.SolverSettings() + settings.set_time_limit(2) + + routing_solution = routing.Solve(d, settings) + cu_status = routing_solution.get_status() + assert cu_status == 0 + _check_cuopt_solution( + routing_solution, + costs, + costs, + earliest_time, + latest_time, + service_times, + ) + + +# ----- Vehicle routing breaks and order match ----- + + +def test_empty_routes_with_breaks(): + cost_matrix = cudf.DataFrame( + [ + [0.0, 1.0, 2.0, 2.0, 5.0, 9.0], + [1.0, 0.0, 3.0, 3.0, 6.0, 10.0], + [3.0, 4.0, 0.0, 3.0, 6.0, 10.0], + [3.0, 4.0, 3.0, 0.0, 3.0, 7.0], + [5.0, 6.0, 7.0, 7.0, 0.0, 4.0], + [8.0, 9.0, 10.0, 10.0, 3.0, 0.0], + ] + ) + + cost_matrix_1 = cudf.DataFrame( + [ + [0.0, 2.0, 4.0, 4.0, 9.0, 14.0], + [2.0, 0.0, 6.0, 6.0, 11.0, 16.0], + [6.0, 8.0, 0.0, 4.0, 9.0, 14.0], + [5.0, 7.0, 5.0, 0.0, 5.0, 10.0], + [8.0, 10.0, 12.0, 12.0, 0.0, 5.0], + [12.0, 14.0, 16.0, 16.0, 4.0, 0.0], + ] + ) + + transit_time_matrix = cost_matrix.copy(deep=True) + transit_time_matrix_1 = cost_matrix_1.copy(deep=True) + + vehcile_start = cudf.Series([0, 1, 0, 1, 0]) + + vehicle_cap = cudf.Series([10, 12, 15, 8, 10]) + + vehicle_eal = cudf.Series([0, 1, 3, 5, 20]) + + vehicle_lat = cudf.Series([80, 40, 30, 80, 100]) + + vehicle_break_eal = cudf.Series([20, 20, 20, 20, 20]) + + vehicle_break_lat = cudf.Series([25, 25, 25, 25, 25]) + + vehicle_duration = cudf.Series([1, 1, 1, 1, 1]) + + task_locations = cudf.Series([1, 2, 3, 4, 5]) + + demand = cudf.Series([3, 4, 4, 3, 2]) + + task_time_eal = cudf.Series([3, 5, 1, 4, 0]) + + task_time_latest = cudf.Series([20, 30, 20, 40, 30]) + + task_serv = cudf.Series([3, 1, 8, 4, 0]) + + veh_types = cudf.Series([1, 2, 1, 2, 1]) + + dm = routing.DataModel( + cost_matrix.shape[0], len(vehcile_start), len(task_locations) + ) + + dm.add_cost_matrix(cost_matrix, 1) + dm.add_cost_matrix(cost_matrix_1, 2) + + dm.add_transit_time_matrix(transit_time_matrix, 1) + dm.add_transit_time_matrix(transit_time_matrix_1, 2) + + dm.set_order_locations(task_locations) + assert (dm.get_order_locations() == task_locations).all() + + dm.set_vehicle_types(veh_types) + + dm.add_break_dimension( + vehicle_break_eal, vehicle_break_lat, vehicle_duration + ) + + dm.add_capacity_dimension("1", demand, vehicle_cap) + + dm.add_vehicle_order_match(0, cudf.Series([0, 4])) + + dm.add_order_vehicle_match(0, cudf.Series([0])) + dm.add_order_vehicle_match(4, cudf.Series([0])) + + dm.set_vehicle_time_windows(vehicle_eal, vehicle_lat) + + dm.set_order_time_windows(task_time_eal, task_time_latest) + + dm.set_order_service_times(task_serv) + + sol_set = routing.SolverSettings() + + sol = routing.Solve(dm, sol_set) + + assert sol.get_status() == 0 + + solution_cudf = sol.get_route() + for i, assign in enumerate( + solution_cudf["truck_id"].unique().to_arrow().to_pylist() + ): + solution_vehicle_x = solution_cudf[solution_cudf["truck_id"] == assign] + h_route = solution_vehicle_x["route"].to_arrow().to_pylist() + route_len = len(h_route) + assert route_len > 3 diff --git a/python/cuopt/cuopt/tests/routing/test_vehicle_routing.py b/python/cuopt/cuopt/tests/routing/test_vehicle_routing.py deleted file mode 100644 index 148fcc11ee..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_vehicle_routing.py +++ /dev/null @@ -1,105 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import cudf - -from cuopt import routing - - -def test_empty_routes_with_breaks(): - cost_matrix = cudf.DataFrame( - [ - [0.0, 1.0, 2.0, 2.0, 5.0, 9.0], - [1.0, 0.0, 3.0, 3.0, 6.0, 10.0], - [3.0, 4.0, 0.0, 3.0, 6.0, 10.0], - [3.0, 4.0, 3.0, 0.0, 3.0, 7.0], - [5.0, 6.0, 7.0, 7.0, 0.0, 4.0], - [8.0, 9.0, 10.0, 10.0, 3.0, 0.0], - ] - ) - - cost_matrix_1 = cudf.DataFrame( - [ - [0.0, 2.0, 4.0, 4.0, 9.0, 14.0], - [2.0, 0.0, 6.0, 6.0, 11.0, 16.0], - [6.0, 8.0, 0.0, 4.0, 9.0, 14.0], - [5.0, 7.0, 5.0, 0.0, 5.0, 10.0], - [8.0, 10.0, 12.0, 12.0, 0.0, 5.0], - [12.0, 14.0, 16.0, 16.0, 4.0, 0.0], - ] - ) - - transit_time_matrix = cost_matrix.copy(deep=True) - transit_time_matrix_1 = cost_matrix_1.copy(deep=True) - - vehcile_start = cudf.Series([0, 1, 0, 1, 0]) - - vehicle_cap = cudf.Series([10, 12, 15, 8, 10]) - - vehicle_eal = cudf.Series([0, 1, 3, 5, 20]) - - vehicle_lat = cudf.Series([80, 40, 30, 80, 100]) - - vehicle_break_eal = cudf.Series([20, 20, 20, 20, 20]) - - vehicle_break_lat = cudf.Series([25, 25, 25, 25, 25]) - - vehicle_duration = cudf.Series([1, 1, 1, 1, 1]) - - task_locations = cudf.Series([1, 2, 3, 4, 5]) - - demand = cudf.Series([3, 4, 4, 3, 2]) - - task_time_eal = cudf.Series([3, 5, 1, 4, 0]) - - task_time_latest = cudf.Series([20, 30, 20, 40, 30]) - - task_serv = cudf.Series([3, 1, 8, 4, 0]) - - veh_types = cudf.Series([1, 2, 1, 2, 1]) - - dm = routing.DataModel( - cost_matrix.shape[0], len(vehcile_start), len(task_locations) - ) - - dm.add_cost_matrix(cost_matrix, 1) - dm.add_cost_matrix(cost_matrix_1, 2) - - dm.add_transit_time_matrix(transit_time_matrix, 1) - dm.add_transit_time_matrix(transit_time_matrix_1, 2) - - dm.set_order_locations(task_locations) - - dm.set_vehicle_types(veh_types) - - dm.add_break_dimension( - vehicle_break_eal, vehicle_break_lat, vehicle_duration - ) - - dm.add_capacity_dimension("1", demand, vehicle_cap) - - dm.add_vehicle_order_match(0, cudf.Series([0, 4])) - - dm.add_order_vehicle_match(0, cudf.Series([0])) - dm.add_order_vehicle_match(4, cudf.Series([0])) - - dm.set_vehicle_time_windows(vehicle_eal, vehicle_lat) - - dm.set_order_time_windows(task_time_eal, task_time_latest) - - dm.set_order_service_times(task_serv) - - sol_set = routing.SolverSettings() - - sol = routing.Solve(dm, sol_set) - - assert sol.get_status() == 0 - - solution_cudf = sol.get_route() - for i, assign in enumerate( - solution_cudf["truck_id"].unique().to_arrow().to_pylist() - ): - solution_vehicle_x = solution_cudf[solution_cudf["truck_id"] == assign] - h_route = solution_vehicle_x["route"].to_arrow().to_pylist() - route_len = len(h_route) - assert route_len > 3 diff --git a/python/cuopt/cuopt/tests/routing/test_vehicle_types.py b/python/cuopt/cuopt/tests/routing/test_vehicle_types.py deleted file mode 100644 index 4c250c5107..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_vehicle_types.py +++ /dev/null @@ -1,84 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import pytest - -import cudf - -from cuopt import routing - - -def test_multiple_set_cost_matrix(): - with pytest.raises(ValueError): - bikes_type = 1 - bikes_cost = cudf.DataFrame([[0, 4, 4], [4, 0, 4], [4, 4, 0]]) - bikes_time = cudf.DataFrame([[0, 50, 50], [50, 0, 50], [50, 50, 0]]) - - dm = routing.DataModel(3, 2) - dm.add_cost_matrix(bikes_cost, bikes_type) - dm.add_transit_time_matrix(bikes_time, bikes_type) - dm.add_cost_matrix(bikes_cost, bikes_type) - - -def test_multiple_set_time_matrix(): - with pytest.raises(ValueError): - bikes_type = 1 - bikes_cost = cudf.DataFrame([[0, 4, 4], [4, 0, 4], [4, 4, 0]]) - bikes_time = cudf.DataFrame([[0, 50, 50], [50, 0, 50], [50, 50, 0]]) - - dm = routing.DataModel(3, 2) - dm.add_cost_matrix(bikes_cost, bikes_type) - dm.add_transit_time_matrix(bikes_time, bikes_type) - dm.add_transit_time_matrix(bikes_time, bikes_type) - - -def test_vehicle_types(): - bikes_type = 1 - car_type = 2 - - bikes_cost = cudf.DataFrame([[0, 4, 4], [4, 0, 4], [4, 4, 0]]) - bikes_time = cudf.DataFrame([[0, 50, 50], [50, 0, 50], [50, 50, 0]]) - car_cost = cudf.DataFrame([[0, 1, 1], [1, 0, 1], [1, 1, 0]]) - car_time = cudf.DataFrame([[0, 10, 10], [10, 0, 10], [10, 10, 0]]) - vehicle_types = cudf.Series([bikes_type, car_type]) - - dm = routing.DataModel(3, 2) - dm.add_cost_matrix(bikes_cost, bikes_type) - dm.add_transit_time_matrix(bikes_time, bikes_type) - dm.add_cost_matrix(car_cost, car_type) - dm.add_transit_time_matrix(car_time, car_type) - dm.set_vehicle_types(vehicle_types) - dm.set_min_vehicles(2) - - s = routing.SolverSettings() - s.set_time_limit(1) - - sol = routing.Solve(dm, s) - - cost = sol.get_total_objective() - cu_status = sol.get_status() - vehicle_count = sol.get_vehicle_count() - assert cu_status == 0 - assert vehicle_count == 2 - assert cost == 10 - solution_cudf = sol.get_route() - - for i, assign in enumerate( - solution_cudf["truck_id"].unique().to_arrow().to_pylist() - ): - solution_vehicle_x = solution_cudf[solution_cudf["truck_id"] == assign] - vehicle_x_start_time = round( - float(solution_vehicle_x["arrival_stamp"].min()), 2 - ) - vehicle_x_final_time = round( - float(solution_vehicle_x["arrival_stamp"].max()), 2 - ) - vehicle_x_total_time = round( - vehicle_x_final_time - vehicle_x_start_time, 2 - ) - - if vehicle_types[assign] == bikes_type: - assert abs(vehicle_x_total_time - 100) < 0.01 - - if vehicle_types[assign] == car_type: - assert abs(vehicle_x_total_time - 20) < 0.01 diff --git a/python/cuopt/cuopt/tests/routing/test_warnings.py b/python/cuopt/cuopt/tests/routing/test_warnings.py deleted file mode 100644 index 10cb1def8e..0000000000 --- a/python/cuopt/cuopt/tests/routing/test_warnings.py +++ /dev/null @@ -1,67 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import warnings - -import cudf - -from cuopt import routing - - -def test_type_casting_warnings(): - cost_matrix = cudf.DataFrame([[0, 4, 4], [4, 0, 4], [4, 4, 0]]) - constraints = cudf.DataFrame() - constraints["earliest"] = [0, 0, 0] - constraints["latest"] = [45, 45, 45] - constraints["service"] = [2.5, 2.5, 2.5] - - dm = routing.DataModel(3, 2) - with warnings.catch_warnings(record=True) as w: - dm.add_cost_matrix(cost_matrix) - assert "Casting cost_matrix from int64 to float32" in str(w[0].message) - - dm.set_order_time_windows( - constraints["earliest"], constraints["latest"] - ) - - dm.set_order_service_times(constraints["service"]) - assert "Casting service_times from float64 to int32" in str( - w[1].message - ) - - -def test_lex_smoke(): - cost_matrix = cudf.DataFrame( - [ - [0.0, 6.0, 4.0, 6.0], - [6.0, 0.0, 4.0, 6.0], - [4.0, 4.0, 0.0, 4.0], - [6.0, 6.0, 4.0, 0.0], - ] - ) - vehicle_start = cudf.Series([0, 0]) - vehicle_cap = cudf.Series([2, 2]) - vehicle_eal = cudf.Series([0, 0]) - vehicle_lat = cudf.Series([100, 100]) - task_locations = cudf.Series([1, 2, 3, 3, 2, 1, 2, 3, 0, 2, 1, 0]) - demand = cudf.Series([1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1]) - task_time_eal = cudf.Series([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - task_time_latest = cudf.Series( - [10, 20, 30, 10, 20, 30, 45, 45, 45, 45, 45, 45] - ) - task_serv = cudf.Series([2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]) - pick_ind = cudf.Series([0, 1, 2, 3, 4, 5]) - del_ind = cudf.Series([6, 7, 8, 9, 10, 11]) - dm = routing.DataModel( - cost_matrix.shape[0], len(vehicle_start), len(task_locations) - ) - dm.add_cost_matrix(cost_matrix) - dm.set_order_locations(task_locations) - dm.add_capacity_dimension("1", demand, vehicle_cap) - dm.set_vehicle_time_windows(vehicle_eal, vehicle_lat) - dm.set_order_time_windows(task_time_eal, task_time_latest) - dm.set_order_service_times(task_serv) - dm.set_pickup_delivery_pairs(pick_ind, del_ind) - sol_set = routing.SolverSettings() - sol = routing.Solve(dm, sol_set) - assert sol.get_status() == 0 diff --git a/python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py b/python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py new file mode 100644 index 0000000000..1b8e3662e7 --- /dev/null +++ b/python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py @@ -0,0 +1,129 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import warnings + +import pytest + +import cudf + +from cuopt import routing +from cuopt.utilities import InputValidationError + + +# ----- Warnings ----- + + +def test_type_casting_warnings(): + cost_matrix = cudf.DataFrame([[0, 4, 4], [4, 0, 4], [4, 4, 0]]) + constraints = cudf.DataFrame() + constraints["earliest"] = [0, 0, 0] + constraints["latest"] = [45, 45, 45] + constraints["service"] = [2.5, 2.5, 2.5] + + dm = routing.DataModel(3, 2) + with warnings.catch_warnings(record=True) as w: + dm.add_cost_matrix(cost_matrix) + assert "Casting cost_matrix from int64 to float32" in str(w[0].message) + + dm.set_order_time_windows( + constraints["earliest"], constraints["latest"] + ) + + dm.set_order_service_times(constraints["service"]) + assert "Casting service_times from float64 to int32" in str( + w[1].message + ) + + +# ----- Validation (matrix, time windows, range) ----- + + +def test_dist_mat(): + cost_matrix = cudf.DataFrame( + [ + [0, 5.0, 5.0, 5.0], + [5.0, 0, 5.0, 5.0], + [5.0, 5.0, 0, 5.0], + [5.0, -5.0, 5.0, 0], + ] + ) + with pytest.raises(Exception) as exc_info: + dm = routing.DataModel(3, 3) + dm.add_cost_matrix(cost_matrix) + assert ( + str(exc_info.value) + == "Number of locations doesn't match number of locations in matrix" + ) + with pytest.raises(Exception) as exc_info: + dm = routing.DataModel(cost_matrix.shape[0], 3) + dm.add_cost_matrix(cost_matrix[:3]) + assert str(exc_info.value) == "cost matrix is expected to be a square matrix" + with pytest.raises(Exception) as exc_info: + dm = routing.DataModel(cost_matrix.shape[0], 3) + dm.add_cost_matrix(cost_matrix) + assert ( + str(exc_info.value) + == "All values in cost matrix must be greater than or equal to zero" + ) + + +def test_time_windows(): + cost_matrix = cudf.DataFrame( + [ + [0, 5.0, 5.0, 5.0], + [5.0, 0, 5.0, 5.0], + [5.0, 5.0, 0, 5.0], + [5.0, 5.0, 5.0, 0], + ] + ) + dm = routing.DataModel(cost_matrix.shape[0], 3) + dm.add_cost_matrix(cost_matrix) + + vehicle_start = cudf.Series([1, 2, 3]) + vehicle_return = cudf.Series([1, 2, 3]) + vehicle_earliest_size = cudf.Series([60, 60]) + vehicle_earliest_neg = cudf.Series([-60, 60, 60]) + vehicle_earliest_greater = cudf.Series([60, 60, 120]) + vehicle_latest = cudf.Series([100] * 3) + + dm.set_vehicle_locations(vehicle_start, vehicle_return) + with pytest.raises(Exception) as exc_info: + dm.set_vehicle_time_windows(vehicle_earliest_size, vehicle_latest) + assert ( + str(exc_info.value) + == "earliest times size doesn't match number of vehicles" + ) + with pytest.raises(Exception) as exc_info: + dm.set_vehicle_time_windows(vehicle_earliest_neg, vehicle_latest) + assert ( + str(exc_info.value) + == "All values in earliest times must be greater than or equal to zero" + ) + with pytest.raises(Exception) as exc_info: + dm.set_vehicle_time_windows(vehicle_earliest_greater, vehicle_latest) + assert ( + str(exc_info.value) + == "All earliest times must be lesser than latest times" + ) + + +def test_range(): + cost_matrix = cudf.DataFrame( + [[0, 5.0, 5.0], [5.0, 0, 5.0], [5.0, 5.0, 0]] + ) + dm = routing.DataModel(cost_matrix.shape[0], 3, 5) + dm.add_cost_matrix(cost_matrix) + with pytest.raises(Exception) as exc_info: + order_locations = cudf.Series([0, 1, 2, 4, 1]) + dm.set_order_locations(order_locations) + assert ( + str(exc_info.value) + == "All values in order locations must be less than or equal to 3" + ) + + +def test_invalid_datamodel(): + with pytest.raises(InputValidationError) as err: + routing.DataModel(0, 0, 0) + assert str(err.value) == "The data model needs at least one location" From ac1f2c43af25e2eaad6fe127556ff1879a6eb486 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Mon, 9 Mar 2026 12:56:09 -0700 Subject: [PATCH 2/7] move solomon break testing to C++, add break validation in check_route --- cpp/tests/routing/unit_tests/breaks.cu | 199 ++++++++++++++++++ .../routing/unit_tests/heterogenous_breaks.cu | 86 ++++++++ .../routing/utilities/check_constraints.cu | 79 +++++++ .../routing/utilities/test_utilities.hpp | 10 + .../cuopt/cuopt/tests/routing/API_COVERAGE.md | 36 ++-- .../tests/routing/test_vehicle_properties.py | 144 ++----------- 6 files changed, 406 insertions(+), 148 deletions(-) diff --git a/cpp/tests/routing/unit_tests/breaks.cu b/cpp/tests/routing/unit_tests/breaks.cu index eb335dbdc4..3d6e48a25d 100644 --- a/cpp/tests/routing/unit_tests/breaks.cu +++ b/cpp/tests/routing/unit_tests/breaks.cu @@ -9,9 +9,11 @@ #include #include +#include #include #include +#include #include namespace cuopt { @@ -36,6 +38,7 @@ TEST(vehicle_breaks, default_case) auto v_break_locations = cuopt::device_copy(break_locations, stream); cuopt::routing::data_model_view_t data_model(&handle, 3, 2); data_model.add_cost_matrix(v_cost_matrix.data()); + data_model.add_transit_time_matrix(v_cost_matrix.data()); data_model.add_break_dimension( v_break_earliest.data(), v_break_latest.data(), v_break_service.data()); data_model.set_break_locations(v_break_locations.data(), v_break_locations.size()); @@ -63,6 +66,7 @@ TEST(vehicle_breaks, non_default_case) cuopt::routing::data_model_view_t data_model(&handle, 3, 2); data_model.add_cost_matrix(v_cost_matrix.data()); + data_model.add_transit_time_matrix(v_cost_matrix.data()); data_model.add_break_dimension( v_break_earliest.data(), v_break_latest.data(), v_break_service.data()); data_model.set_break_locations(v_break_locations.data(), v_break_locations.size()); @@ -232,6 +236,201 @@ TEST(vehicle_breaks, vehicle_time_windows) } } +// Test uniform breaks (Solomon 100 nodes) +TEST(vehicle_breaks, uniform_breaks) +{ + raft::handle_t handle; + auto stream = handle.get_stream(); + + std::string path = cuopt::test::get_rapids_dataset_root_dir() + "/solomon/In/r107.txt"; + Route route; + load_solomon(path, route, 101); + + int nodes = route.n_locations; + int n_orders = nodes - 1; + int vehicle_num = 25; + + std::vector cost_matrix(nodes * nodes), time_matrix(nodes * nodes); + build_dense_matrix(cost_matrix.data(), route.x_h, route.y_h); + build_dense_matrix(time_matrix.data(), route.x_h, route.y_h); + + std::vector order_locations(n_orders), order_earliest(n_orders), order_latest(n_orders), + order_service(n_orders); + for (int i = 0; i < n_orders; ++i) { + order_locations[i] = i + 1; + order_earliest[i] = route.earliest_time_h[i + 1]; + order_latest[i] = route.latest_time_h[i + 1]; + order_service[i] = route.service_time_h[i + 1]; + } + int num_breaks = 2; + std::vector break_earliest(vehicle_num * num_breaks); + std::vector break_latest(vehicle_num * num_breaks); + std::vector break_duration(vehicle_num * num_breaks); + for (int v = 0; v < vehicle_num; ++v) { + break_earliest[v * num_breaks + 0] = 40; + break_latest[v * num_breaks + 0] = 50; + break_duration[v * num_breaks + 0] = 10; + break_earliest[v * num_breaks + 1] = 170; + break_latest[v * num_breaks + 1] = 180; + break_duration[v * num_breaks + 1] = 10; + } + + std::vector break_locations(nodes); + for (int i = 0; i < nodes; ++i) { break_locations[i] = i; } + + cuopt::routing::data_model_view_t data_model(&handle, nodes, vehicle_num, n_orders); + + auto v_cost_matrix = cuopt::device_copy(cost_matrix, stream); + auto v_time_matrix = cuopt::device_copy(time_matrix, stream); + auto v_order_locations = cuopt::device_copy(order_locations, stream); + auto v_order_earliest = cuopt::device_copy(order_earliest, stream); + auto v_order_latest = cuopt::device_copy(order_latest, stream); + auto v_order_service = cuopt::device_copy(order_service, stream); + auto v_break_locations = cuopt::device_copy(break_locations, stream); + + data_model.add_cost_matrix(v_cost_matrix.data()); + data_model.add_transit_time_matrix(v_time_matrix.data()); + data_model.set_order_locations(v_order_locations.data()); + data_model.set_order_time_windows(v_order_earliest.data(), v_order_latest.data()); + data_model.set_order_service_times(v_order_service.data()); + data_model.set_break_locations(v_break_locations.data(), v_break_locations.size()); + + std::vector dim0_earliest(vehicle_num), dim0_latest(vehicle_num), dim0_duration(vehicle_num); + std::vector dim1_earliest(vehicle_num), dim1_latest(vehicle_num), dim1_duration(vehicle_num); + for (int v = 0; v < vehicle_num; ++v) { + dim0_earliest[v] = break_earliest[v * num_breaks + 0]; + dim0_latest[v] = break_latest[v * num_breaks + 0]; + dim0_duration[v] = break_duration[v * num_breaks + 0]; + dim1_earliest[v] = break_earliest[v * num_breaks + 1]; + dim1_latest[v] = break_latest[v * num_breaks + 1]; + dim1_duration[v] = break_duration[v * num_breaks + 1]; + } + auto v_break_earliest_0 = cuopt::device_copy(dim0_earliest, stream); + auto v_break_latest_0 = cuopt::device_copy(dim0_latest, stream); + auto v_break_duration_0 = cuopt::device_copy(dim0_duration, stream); + data_model.add_break_dimension( + v_break_earliest_0.data(), v_break_latest_0.data(), v_break_duration_0.data()); + + auto v_break_earliest_1 = cuopt::device_copy(dim1_earliest, stream); + auto v_break_latest_1 = cuopt::device_copy(dim1_latest, stream); + auto v_break_duration_1 = cuopt::device_copy(dim1_duration, stream); + data_model.add_break_dimension( + v_break_earliest_1.data(), v_break_latest_1.data(), v_break_duration_1.data()); + + cuopt::routing::solver_settings_t settings; + settings.set_time_limit(30); + + auto routing_solution = cuopt::routing::solve(data_model, settings); + handle.sync_stream(); + + ASSERT_EQ(routing_solution.get_status(), cuopt::routing::solution_status_t::SUCCESS); + host_assignment_t h_routing_solution(routing_solution); + check_route(data_model, h_routing_solution); +} + +// Test non-uniform breaks (Solomon 100 nodes) +TEST(vehicle_breaks, non_uniform_breaks) +{ + raft::handle_t handle; + auto stream = handle.get_stream(); + + std::string path = cuopt::test::get_rapids_dataset_root_dir() + "/solomon/In/r107.txt"; + Route route; + load_solomon(path, route, 101); + + int nodes = route.n_locations; + int n_orders = nodes - 1; + int vehicle_num = 30; + + std::vector cost_matrix(nodes * nodes), time_matrix(nodes * nodes); + build_dense_matrix(cost_matrix.data(), route.x_h, route.y_h); + build_dense_matrix(time_matrix.data(), route.x_h, route.y_h); + + std::vector order_locations(n_orders), order_earliest(n_orders), order_latest(n_orders), + order_service(n_orders); + for (int i = 0; i < n_orders; ++i) { + order_locations[i] = i + 1; + order_earliest[i] = route.earliest_time_h[i + 1]; + order_latest[i] = route.latest_time_h[i + 1]; + order_service[i] = route.service_time_h[i + 1]; + } + int num_v_type_1 = vehicle_num / 2; + int num_v_type_2 = vehicle_num - num_v_type_1; + int num_breaks = 3; + + // Type 1: [40,50]/5, [100,120]/20, [170,180]/10 + // Type 2: [60,90]/20, [110,120]/10, [200,210]/5 + std::vector break_earliest(vehicle_num * num_breaks); + std::vector break_latest(vehicle_num * num_breaks); + std::vector break_duration(vehicle_num * num_breaks); + for (int v = 0; v < num_v_type_1; ++v) { + break_earliest[v * num_breaks + 0] = 40; + break_latest[v * num_breaks + 0] = 50; + break_duration[v * num_breaks + 0] = 5; + break_earliest[v * num_breaks + 1] = 100; + break_latest[v * num_breaks + 1] = 120; + break_duration[v * num_breaks + 1] = 20; + break_earliest[v * num_breaks + 2] = 170; + break_latest[v * num_breaks + 2] = 180; + break_duration[v * num_breaks + 2] = 10; + } + for (int v = num_v_type_1; v < vehicle_num; ++v) { + break_earliest[v * num_breaks + 0] = 60; + break_latest[v * num_breaks + 0] = 90; + break_duration[v * num_breaks + 0] = 20; + break_earliest[v * num_breaks + 1] = 110; + break_latest[v * num_breaks + 1] = 120; + break_duration[v * num_breaks + 1] = 10; + break_earliest[v * num_breaks + 2] = 200; + break_latest[v * num_breaks + 2] = 210; + break_duration[v * num_breaks + 2] = 5; + } + + // Depot (0) excluded from break locations + std::vector break_locations(nodes - 1); + for (int i = 0; i < nodes - 1; ++i) { break_locations[i] = i + 1; } + + cuopt::routing::data_model_view_t data_model(&handle, nodes, vehicle_num, n_orders); + + auto v_cost_matrix = cuopt::device_copy(cost_matrix, stream); + auto v_time_matrix = cuopt::device_copy(time_matrix, stream); + auto v_order_locations = cuopt::device_copy(order_locations, stream); + auto v_order_earliest = cuopt::device_copy(order_earliest, stream); + auto v_order_latest = cuopt::device_copy(order_latest, stream); + auto v_order_service = cuopt::device_copy(order_service, stream); + auto v_break_locations = cuopt::device_copy(break_locations, stream); + + data_model.add_cost_matrix(v_cost_matrix.data()); + data_model.add_transit_time_matrix(v_time_matrix.data()); + data_model.set_order_locations(v_order_locations.data()); + data_model.set_order_time_windows(v_order_earliest.data(), v_order_latest.data()); + data_model.set_order_service_times(v_order_service.data()); + data_model.set_break_locations(v_break_locations.data(), v_break_locations.size()); + + for (int b = 0; b < num_breaks; ++b) { + std::vector e(vehicle_num), l(vehicle_num), d(vehicle_num); + for (int v = 0; v < vehicle_num; ++v) { + e[v] = break_earliest[v * num_breaks + b]; + l[v] = break_latest[v * num_breaks + b]; + d[v] = break_duration[v * num_breaks + b]; + } + auto v_e = cuopt::device_copy(e, stream); + auto v_l = cuopt::device_copy(l, stream); + auto v_d = cuopt::device_copy(d, stream); + data_model.add_break_dimension(v_e.data(), v_l.data(), v_d.data()); + } + + cuopt::routing::solver_settings_t settings; + settings.set_time_limit(30); + + auto routing_solution = cuopt::routing::solve(data_model, settings); + handle.sync_stream(); + + ASSERT_EQ(routing_solution.get_status(), cuopt::routing::solution_status_t::SUCCESS); + host_assignment_t h_routing_solution(routing_solution); + check_route(data_model, h_routing_solution); +} + } // namespace test } // namespace routing } // namespace cuopt diff --git a/cpp/tests/routing/unit_tests/heterogenous_breaks.cu b/cpp/tests/routing/unit_tests/heterogenous_breaks.cu index 3f24c34f58..a522a98372 100644 --- a/cpp/tests/routing/unit_tests/heterogenous_breaks.cu +++ b/cpp/tests/routing/unit_tests/heterogenous_breaks.cu @@ -9,10 +9,12 @@ #include #include +#include #include #include #include +#include #include namespace cuopt { @@ -111,6 +113,90 @@ TEST(heterogenous_breaks, simple_non_uniform) check_route(data_model, h_routing_solution); } +// Test heterogenous breaks (Solomon 100 nodes): +// Half of vehicles have 2 breaks with custom locations; remaining half have 3 breaks with default (any) location. +TEST(heterogenous_breaks, test_heterogeneous_breaks) +{ + raft::handle_t handle; + auto stream = handle.get_stream(); + + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + std::string routing_file = rapidsDatasetRootDir + "/solomon/In/r107.txt"; + Route route; + load_solomon(routing_file, route, 101); + + int nodes = route.n_locations; + int n_orders = nodes - 1; + int vehicle_num = 20; + int num_v_type_1 = vehicle_num / 2; + + std::vector cost_matrix(nodes * nodes), time_matrix(nodes * nodes); + build_dense_matrix(cost_matrix.data(), route.x_h, route.y_h); + build_dense_matrix(time_matrix.data(), route.x_h, route.y_h); + + std::vector order_locations(n_orders), order_earliest(n_orders), order_latest(n_orders), + order_service(n_orders); + for (int i = 0; i < n_orders; ++i) { + order_locations[i] = i + 1; + order_earliest[i] = route.earliest_time_h[i + 1]; + order_latest[i] = route.latest_time_h[i + 1]; + order_service[i] = route.service_time_h[i + 1]; + } + + // Type 1: 2 breaks [90,100]/15, [150,170]/15 at locations 5,10,15,...,85 (every 5th node, excluding depot) + std::vector break_locations_1; + for (int i = 1; i <= 17; ++i) { break_locations_1.push_back(5 * i); } + + cuopt::routing::data_model_view_t data_model(&handle, nodes, vehicle_num, n_orders); + + auto v_cost_matrix = cuopt::device_copy(cost_matrix, stream); + auto v_time_matrix = cuopt::device_copy(time_matrix, stream); + auto v_order_locations = cuopt::device_copy(order_locations, stream); + auto v_order_earliest = cuopt::device_copy(order_earliest, stream); + auto v_order_latest = cuopt::device_copy(order_latest, stream); + auto v_order_service = cuopt::device_copy(order_service, stream); + + data_model.add_cost_matrix(v_cost_matrix.data()); + data_model.add_transit_time_matrix(v_time_matrix.data()); + data_model.set_order_locations(v_order_locations.data()); + data_model.set_order_time_windows(v_order_earliest.data(), v_order_latest.data()); + data_model.set_order_service_times(v_order_service.data()); + + auto v_break_locations_1 = cuopt::device_copy(break_locations_1, stream); + + for (int i = 0; i < num_v_type_1; ++i) { + data_model.add_vehicle_break(i, + 90, + 100, + 15, + v_break_locations_1.data(), + static_cast(break_locations_1.size())); + data_model.add_vehicle_break(i, + 150, + 170, + 15, + v_break_locations_1.data(), + static_cast(break_locations_1.size())); + } + + // Type 2: 3 breaks [40,50]/10, [110,120]/10, [160,170]/10 with default (any) location + for (int i = num_v_type_1; i < vehicle_num; ++i) { + data_model.add_vehicle_break(i, 40, 50, 10, nullptr, 0); + data_model.add_vehicle_break(i, 110, 120, 10, nullptr, 0); + data_model.add_vehicle_break(i, 160, 170, 10, nullptr, 0); + } + + cuopt::routing::solver_settings_t settings; + settings.set_time_limit(30); + + auto routing_solution = cuopt::routing::solve(data_model, settings); + handle.sync_stream(); + + ASSERT_EQ(routing_solution.get_status(), cuopt::routing::solution_status_t::SUCCESS); + host_assignment_t h_routing_solution(routing_solution); + check_route(data_model, h_routing_solution); +} + } // namespace test } // namespace routing } // namespace cuopt diff --git a/cpp/tests/routing/utilities/check_constraints.cu b/cpp/tests/routing/utilities/check_constraints.cu index 7acb82ce2d..c9dc183096 100644 --- a/cpp/tests/routing/utilities/check_constraints.cu +++ b/cpp/tests/routing/utilities/check_constraints.cu @@ -16,6 +16,7 @@ #include #include +#include namespace cuopt { namespace routing { @@ -104,7 +105,28 @@ void check_route(data_model_view_t const& data_model, if (data_model.get_order_locations() == nullptr) { visited.insert(0); } + bool has_breaks = data_model.has_vehicle_breaks(); + std::vector> uniform_break_earliest_h, uniform_break_latest_h; + std::unordered_set uniform_break_locations_set; + if (has_breaks) { + auto const& uniform = data_model.get_uniform_breaks(); + for (auto const& dim : uniform) { + auto [e_ptr, l_ptr, d_ptr] = dim.get_breaks(); + uniform_break_earliest_h.push_back( + cuopt::host_copy(e_ptr, static_cast(fleet_size), stream)); + uniform_break_latest_h.push_back( + cuopt::host_copy(l_ptr, static_cast(fleet_size), stream)); + } + auto [break_loc_ptr, n_break_loc] = data_model.get_break_locations(); + if (n_break_loc > 0) { + auto break_locations_h = + cuopt::host_copy(break_loc_ptr, static_cast(n_break_loc), stream); + uniform_break_locations_set.insert(break_locations_h.begin(), break_locations_h.end()); + } + } + for (auto const& id : temp_truck_ids) { + size_t i_vehicle_start = i; std::vector path, path_locations; f_t route_dist = 0; f_t route_time = 0.f; @@ -136,6 +158,63 @@ void check_route(data_model_view_t const& data_model, } } + if (has_breaks) { + int break_dim = 0; + auto const& non_uniform = data_model.get_non_uniform_breaks(); + bool use_uniform = !uniform_break_earliest_h.empty(); + bool use_non_uniform = (non_uniform.count(id) > 0); + for (size_t k = i_vehicle_start; k < i; ++k) { + if (static_cast(node_types[k]) == node_type_t::BREAK) { + double arrival = h_routing_solution.stamp[k]; + i_t break_loc_id = locations[k]; + if (use_uniform && break_dim < static_cast(uniform_break_earliest_h.size())) { + //std::cout<<"VEHID: "<(uniform_break_earliest_h[break_dim][id]) - 1e-6) + << "Break " << break_dim << " vehicle " << id << " arrival " << arrival + << " before earliest " << uniform_break_earliest_h[break_dim][id]; + ASSERT_LE(arrival, + static_cast(uniform_break_latest_h[break_dim][id]) + 1e-6) + << "Break " << break_dim << " vehicle " << id << " arrival " << arrival + << " after latest " << uniform_break_latest_h[break_dim][id]; + if (!uniform_break_locations_set.empty()) { + ASSERT_EQ(uniform_break_locations_set.count(break_loc_id), 1u) + << "Break " << break_dim << " vehicle " << id << " at location " << break_loc_id + << " not in allowed break locations"; + } + } else if (use_non_uniform) { + auto const& breaks = non_uniform.at(id); + if (break_dim < static_cast(breaks.size())) { + auto const& b = breaks[break_dim]; + ASSERT_GE(arrival, static_cast(b.earliest_) - 1e-6) + << "Non-uniform break " << break_dim << " vehicle " << id; + ASSERT_LE(arrival, static_cast(b.latest_) + 1e-6) + << "Non-uniform break " << break_dim << " vehicle " << id; + if (b.locations_.size() > 0) { + auto allowed_locs = cuopt::host_copy(b.locations_, stream); + bool found = + std::find(allowed_locs.begin(), allowed_locs.end(), break_loc_id) != + allowed_locs.end(); + ASSERT_TRUE(found) + << "Non-uniform break " << break_dim << " vehicle " << id << " at location " + << break_loc_id << " not in allowed break locations"; + } + } + } + ++break_dim; + } + } + if (use_uniform) { + ASSERT_EQ(break_dim, static_cast(uniform_break_earliest_h.size())) + << "Vehicle " << id << " break count " << break_dim + << " expected " << uniform_break_earliest_h.size(); + } else if (use_non_uniform) { + ASSERT_EQ(break_dim, static_cast(non_uniform.at(id).size())) + << "Vehicle " << id << " non-uniform break count " << break_dim + << " expected " << non_uniform.at(id).size(); + } + } + // Check for a case when user indicates that this vehicle can not carry any orders if (possible_orders.empty() && vehicle_order_match_h.count(id) > 0) { EXPECT_EQ(path.size(), 0u); diff --git a/cpp/tests/routing/utilities/test_utilities.hpp b/cpp/tests/routing/utilities/test_utilities.hpp index e64b81473d..8eb9e08842 100644 --- a/cpp/tests/routing/utilities/test_utilities.hpp +++ b/cpp/tests/routing/utilities/test_utilities.hpp @@ -248,6 +248,16 @@ void load_cvrptw(const std::string& fileName, Route& route, i_t limit) route.n_locations = route.x_h.size(); } +// Load Solomon CVRPTW file with 100 nodes (101 including depot) +constexpr int SOLOMON_100_NODES = 101; +template +void load_solomon(const std::string& solomon_path, + Route& route, + i_t limit = SOLOMON_100_NODES) +{ + load_cvrptw(solomon_path, route, limit); +} + template void load_pickup(const std::string& fileName, Route& route) { diff --git a/python/cuopt/cuopt/tests/routing/API_COVERAGE.md b/python/cuopt/cuopt/tests/routing/API_COVERAGE.md index e7260a78e5..061688fc66 100644 --- a/python/cuopt/cuopt/tests/routing/API_COVERAGE.md +++ b/python/cuopt/cuopt/tests/routing/API_COVERAGE.md @@ -32,8 +32,8 @@ Summary of which APIs from `assignment.py` and `vehicle_routing.py` are exercise |-----|---------|--------| | `add_cost_matrix()` | Yes | test_data_model, test_vehicle_properties, test_solver, test_batch_solve, etc. | | `add_transit_time_matrix()` | Yes | test_vehicle_properties, test_solver, test_initial_solutions, test_re_routing, etc. | -| `set_break_locations()` | Yes | test_vehicle_properties (breaks tests) | -| `add_break_dimension()` | Yes | test_vehicle_properties | +| `set_break_locations()` | Yes | test_vehicle_properties (test_empty_routes_with_breaks) | +| `add_break_dimension()` | Yes | test_vehicle_properties (test_empty_routes_with_breaks), test_solver, test_initial_solutions | | `add_vehicle_break()` | Yes | test_vehicle_properties (test_heterogenous_breaks) | | `set_objective_function()` | Yes | test_data_model, test_initial_solutions | | `add_initial_solutions()` | Yes | test_initial_solutions | @@ -62,8 +62,8 @@ Summary of which APIs from `assignment.py` and `vehicle_routing.py` are exercise | `get_num_locations()` | Yes | test_solver (getter check), test_re_routing | | `get_fleet_size()` | Yes | test_data_model, test_vehicle_properties, test_solver_settings | | `get_num_orders()` | Yes | test_data_model | -| `get_cost_matrix()` | Yes | test_data_model | -| `get_transit_time_matrix()` | Yes | test_solver (PDP getter check) | +| `get_cost_matrix()` | Yes | test_data_model (test_order_constraints, test_multi_cost_and_transit_matrices_getters) | +| `get_transit_time_matrix()` | Yes | test_data_model, test_solver (test_pdptw) | | `get_transit_time_matrices()` | **No** | — | | `get_initial_solutions()` | Yes | test_initial_solutions | | `get_order_locations()` | Yes | test_vehicle_properties (test_single_vehicle_with_match, test_empty_routes_with_breaks) | @@ -76,8 +76,8 @@ Summary of which APIs from `assignment.py` and `vehicle_routing.py` are exercise | `get_capacity_dimensions()` | Yes | test_data_model | | `get_order_time_windows()` | Yes | test_vehicle_properties (test_time_windows uses order TW from model) | | `get_order_prizes()` | Yes | test_solver | -| `get_break_locations()` | Yes | test_vehicle_properties (test_uniform_breaks, test_non_uniform_breaks) | -| `get_break_dimensions()` | Yes | test_vehicle_properties (same tests) | +| `get_break_locations()` | Yes | test_vehicle_properties (test_empty_routes_with_breaks) | +| `get_break_dimensions()` | Yes | test_vehicle_properties (test_empty_routes_with_breaks) | | `get_non_uniform_breaks()` | Yes | test_vehicle_properties (test_heterogenous_breaks) | | `get_objective_function()` | Yes | test_data_model | | `get_vehicle_max_costs()` | Yes | test_vehicle_properties (test_vehicle_max_costs) | @@ -86,7 +86,7 @@ Summary of which APIs from `assignment.py` and `vehicle_routing.py` are exercise | `get_vehicle_order_match()` | Yes | test_vehicle_properties (test_vehicle_to_order_match) | | `get_order_vehicle_match()` | Yes | test_vehicle_properties (test_order_to_vehicle_match) | | `get_order_service_times()` | Yes | test_data_model | -| `get_min_vehicles()` | Yes | test_solver_settings | +| `get_min_vehicles()` | Yes | test_vehicle_properties (test_vehicle_types, test_pickup_delivery_orders) | --- @@ -95,14 +95,14 @@ Summary of which APIs from `assignment.py` and `vehicle_routing.py` are exercise | API | Covered | Where | |-----|---------|--------| | `set_time_limit()` | Yes | All solve tests | -| `set_verbose_mode()` | **No** | — | -| `set_error_logging_mode()` | Yes | test_error_logging | -| `dump_best_results()` | Yes | test_solver_settings | -| `dump_config_file()` | Yes | save_restore_test | -| `get_time_limit()` | Yes | test_solver_settings (test_min_vehicles, test_max_distance, test_solver_settings_getters) | -| `get_best_results_file_path()` | Yes | test_solver_settings | -| `get_config_file_name()` | Yes | test_solver_settings (test_solver_settings_getters) | -| `get_best_results_interval()` | Yes | test_solver_settings | +| `set_verbose_mode()` | Yes | test_solver_settings (test_verbose_mode) | +| `set_error_logging_mode()` | **No** | — | +| `dump_best_results()` | Yes | test_solver_settings (test_dump_results) | +| `dump_config_file()` | Yes | test_solver_settings (test_dump_config) | +| `get_time_limit()` | Yes | test_solver_settings (test_solver_settings_getters) | +| `get_best_results_file_path()` | Yes | test_solver_settings (test_dump_results) | +| `get_config_file_name()` | Yes | test_solver_settings (test_dump_config) | +| `get_best_results_interval()` | Yes | test_solver_settings (test_dump_results) | --- @@ -118,7 +118,7 @@ Summary of which APIs from `assignment.py` and `vehicle_routing.py` are exercise ## Summary - **assignment.py:** All listed APIs are covered. -- **DataModel getters not covered:** `get_transit_time_matrices()`. -- **SolverSettings not covered:** `set_verbose_mode()`. +- **DataModel not covered:** getter `get_transit_time_matrices()` only. +- **SolverSettings not covered:** `set_error_logging_mode()`. -All **setters** on DataModel and SolverSettings that are present in `vehicle_routing.py` are covered by at least one test. Getters and a few Assignment/SolverSettings methods remain untested. +All other **setters** on DataModel (and all except `set_error_logging_mode` on SolverSettings) are covered by at least one test. diff --git a/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py b/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py index d1a33b0e2e..a5090f1907 100644 --- a/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py +++ b/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py @@ -32,6 +32,7 @@ def test_vehicle_types(): dm.add_transit_time_matrix(car_time, car_type) dm.set_vehicle_types(vehicle_types) dm.set_min_vehicles(2) + assert dm.get_min_vehicles() == 2 assert (dm.get_vehicle_types() == vehicle_types).all() s = routing.SolverSettings() @@ -438,136 +439,9 @@ def test_vehicle_locations(): # ----- Vehicle breaks ----- -def test_uniform_breaks(): - vehicle_num = 25 - run_nodes = 100 - nodes = run_nodes + 1 - d = utils.create_data_model( - filename, run_nodes=run_nodes, num_vehicles=vehicle_num - ) - - break_times = [[40, 50], [170, 180]] - - num_breaks = len(break_times) - vehicle_breaks_earliest = np.zeros([vehicle_num, num_breaks]) - vehicle_breaks_latest = np.zeros([vehicle_num, num_breaks]) - vehicle_breaks_duration = np.zeros([vehicle_num, num_breaks]) - for b in range(num_breaks): - break_begin = break_times[b][0] - break_end = break_times[b][1] - break_duration = break_end - break_begin - vehicle_breaks_earliest[:, b] = [break_begin] * vehicle_num - vehicle_breaks_latest[:, b] = [break_begin] * vehicle_num - vehicle_breaks_duration[:, b] = [break_duration] * vehicle_num - - # Add all nodes as the vehicle break location - break_locations = cudf.Series([i for i in range(nodes)]) - - d.set_break_locations(break_locations) - for b in range(num_breaks): - d.add_break_dimension( - cudf.Series(vehicle_breaks_earliest[:, b]).astype(np.int32), - cudf.Series(vehicle_breaks_latest[:, b]).astype(np.int32), - cudf.Series(vehicle_breaks_duration[:, b]).astype(np.int32), - ) - - s = routing.SolverSettings() - s.set_time_limit(30) - routing_solution = routing.Solve(d, s) - ret_break_locations = d.get_break_locations() - ret_break_dimensions = d.get_break_dimensions() - - assert (ret_break_locations == break_locations).all() - - for b, break_dimension in enumerate(ret_break_dimensions.keys()): - vehicle_break = ret_break_dimensions[break_dimension] - assert ( - vehicle_break["earliest"] - == cudf.Series(vehicle_breaks_earliest[:, b]) - ).all() - assert ( - vehicle_break["latest"] == cudf.Series(vehicle_breaks_latest[:, b]) - ).all() - assert ( - vehicle_break["duration"] - == cudf.Series(vehicle_breaks_duration[:, b]) - ).all() - - # TO DO: Check if breaks are adhered to - assert routing_solution.get_status() == 0 - - -def test_non_uniform_breaks(): - vehicle_num = 30 - run_nodes = 100 - nodes = run_nodes + 1 - d = utils.create_data_model( - filename, run_nodes=run_nodes, num_vehicles=vehicle_num - ) - - num_v_type_1 = int(vehicle_num / 2) - break_times_1 = [[40, 50], [100, 120], [170, 180]] - break_durations_1 = [5, 20, 10] - - num_v_type_2 = vehicle_num - num_v_type_1 - break_times_2 = [[60, 90], [110, 120], [200, 210]] - break_durations_2 = [20, 10, 5] - - num_breaks = 3 - vehicle_breaks_earliest = np.zeros([vehicle_num, num_breaks]) - vehicle_breaks_latest = np.zeros([vehicle_num, num_breaks]) - vehicle_breaks_duration = np.zeros([vehicle_num, num_breaks]) - for b in range(num_breaks): - vehicle_breaks_earliest[:, b] = [ - break_times_1[b][0] - ] * num_v_type_1 + [break_times_2[b][0]] * num_v_type_2 - vehicle_breaks_latest[:, b] = [break_times_1[b][1]] * num_v_type_1 + [ - break_times_2[b][1] - ] * num_v_type_2 - vehicle_breaks_duration[:, b] = [ - break_durations_1[b] - ] * num_v_type_1 + [break_durations_2[b]] * num_v_type_2 - - # Depot should not be a break node - break_locations = cudf.Series([i + 1 for i in range(nodes - 1)]) - - d.set_break_locations(break_locations) - for b in range(num_breaks): - d.add_break_dimension( - cudf.Series(vehicle_breaks_earliest[:, b]).astype(np.int32), - cudf.Series(vehicle_breaks_latest[:, b]).astype(np.int32), - cudf.Series(vehicle_breaks_duration[:, b]).astype(np.int32), - ) - - s = routing.SolverSettings() - s.set_time_limit(30) - routing_solution = routing.Solve(d, s) - ret_break_locations = d.get_break_locations() - ret_break_dimensions = d.get_break_dimensions() - - assert (ret_break_locations == break_locations).all() - - for b, break_dimension in enumerate(ret_break_dimensions.keys()): - vehicle_break = ret_break_dimensions[break_dimension] - assert ( - vehicle_break["earliest"] - == cudf.Series(vehicle_breaks_earliest[:, b]) - ).all() - assert ( - vehicle_break["latest"] == cudf.Series(vehicle_breaks_latest[:, b]) - ).all() - assert ( - vehicle_break["duration"] - == cudf.Series(vehicle_breaks_duration[:, b]) - ).all() - - # TO DO: Check if breaks are adhered to - assert routing_solution.get_status() == 0 - - def test_heterogenous_breaks(): - vehicle_num = 20 - run_nodes = 100 + vehicle_num = 5 + run_nodes = 20 d = utils.create_data_model( filename, run_nodes=run_nodes, num_vehicles=vehicle_num ) @@ -582,7 +456,7 @@ def test_heterogenous_breaks(): num_v_type_1 = int(vehicle_num / 2) break_times_1 = [[90, 100], [150, 170]] break_durations_1 = [15, 15] - break_locations_1 = cudf.Series([5 * i for i in range(1, 18)]) + break_locations_1 = cudf.Series([4 * i for i in range(1, 5)]) num_breaks_2 = 3 num_v_type_2 = vehicle_num - num_v_type_1 @@ -728,6 +602,7 @@ def test_vehicle_dependent_service_times(): for vehicle_id, v_service_times in service_times.items(): d.set_order_service_times(cudf.Series(v_service_times), vehicle_id) d.set_min_vehicles(2) + assert d.get_min_vehicles() == 2 settings = routing.SolverSettings() settings.set_time_limit(2) @@ -815,9 +690,18 @@ def test_empty_routes_with_breaks(): dm.set_vehicle_types(veh_types) + break_locations = cudf.Series([1, 2, 3, 4, 5]) + dm.set_break_locations(break_locations) dm.add_break_dimension( vehicle_break_eal, vehicle_break_lat, vehicle_duration ) + assert (dm.get_break_locations() == break_locations).all() + ret_break_dims = dm.get_break_dimensions() + assert len(ret_break_dims) == 1 + dim0 = list(ret_break_dims.values())[0] + assert (dim0["earliest"] == vehicle_break_eal).all() + assert (dim0["latest"] == vehicle_break_lat).all() + assert (dim0["duration"] == vehicle_duration).all() dm.add_capacity_dimension("1", demand, vehicle_cap) From 1fb0a10261c6e8e8aed6ba9eb49d1a896c700904 Mon Sep 17 00:00:00 2001 From: Ishika Roy <41401566+Iroy30@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:26:58 -0500 Subject: [PATCH 3/7] Update test_solver.py --- python/cuopt/cuopt/tests/routing/test_solver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/cuopt/cuopt/tests/routing/test_solver.py b/python/cuopt/cuopt/tests/routing/test_solver.py index 016e1c0124..1533062c6f 100644 --- a/python/cuopt/cuopt/tests/routing/test_solver.py +++ b/python/cuopt/cuopt/tests/routing/test_solver.py @@ -12,7 +12,7 @@ SOLOMON_DATASETS_PATH = os.path.join(utils.RAPIDS_DATASET_ROOT_DIR, "solomon/In/") -"""def test_solomon(): +def test_solomon(): SOLOMON_DATASET = "r107.txt" SOLOMON_YAML = "r107.yaml" utils.convert_solomon_inp_file_to_yaml( @@ -55,7 +55,7 @@ assert vehicle_size <= 12 if vehicle_size == 11: assert math.fabs((final_cost - ref_cost) / ref_cost) < 0.1 -""" + def test_pdptw(): """ From 0826f7d964fd9f59c78adbf48f3c93f66699a757 Mon Sep 17 00:00:00 2001 From: Ishika Roy <41401566+Iroy30@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:33:40 -0500 Subject: [PATCH 4/7] Update breaks.cu --- cpp/tests/routing/unit_tests/breaks.cu | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cpp/tests/routing/unit_tests/breaks.cu b/cpp/tests/routing/unit_tests/breaks.cu index 3d6e48a25d..bd02b87d7d 100644 --- a/cpp/tests/routing/unit_tests/breaks.cu +++ b/cpp/tests/routing/unit_tests/breaks.cu @@ -250,9 +250,8 @@ TEST(vehicle_breaks, uniform_breaks) int n_orders = nodes - 1; int vehicle_num = 25; - std::vector cost_matrix(nodes * nodes), time_matrix(nodes * nodes); + std::vector cost_matrix(nodes * nodes); build_dense_matrix(cost_matrix.data(), route.x_h, route.y_h); - build_dense_matrix(time_matrix.data(), route.x_h, route.y_h); std::vector order_locations(n_orders), order_earliest(n_orders), order_latest(n_orders), order_service(n_orders); @@ -281,7 +280,6 @@ TEST(vehicle_breaks, uniform_breaks) cuopt::routing::data_model_view_t data_model(&handle, nodes, vehicle_num, n_orders); auto v_cost_matrix = cuopt::device_copy(cost_matrix, stream); - auto v_time_matrix = cuopt::device_copy(time_matrix, stream); auto v_order_locations = cuopt::device_copy(order_locations, stream); auto v_order_earliest = cuopt::device_copy(order_earliest, stream); auto v_order_latest = cuopt::device_copy(order_latest, stream); @@ -289,7 +287,7 @@ TEST(vehicle_breaks, uniform_breaks) auto v_break_locations = cuopt::device_copy(break_locations, stream); data_model.add_cost_matrix(v_cost_matrix.data()); - data_model.add_transit_time_matrix(v_time_matrix.data()); + data_model.add_transit_time_matrix(v_cost_matrix.data()); data_model.set_order_locations(v_order_locations.data()); data_model.set_order_time_windows(v_order_earliest.data(), v_order_latest.data()); data_model.set_order_service_times(v_order_service.data()); @@ -342,9 +340,8 @@ TEST(vehicle_breaks, non_uniform_breaks) int n_orders = nodes - 1; int vehicle_num = 30; - std::vector cost_matrix(nodes * nodes), time_matrix(nodes * nodes); + std::vector cost_matrix(nodes * nodes); build_dense_matrix(cost_matrix.data(), route.x_h, route.y_h); - build_dense_matrix(time_matrix.data(), route.x_h, route.y_h); std::vector order_locations(n_orders), order_earliest(n_orders), order_latest(n_orders), order_service(n_orders); @@ -393,7 +390,6 @@ TEST(vehicle_breaks, non_uniform_breaks) cuopt::routing::data_model_view_t data_model(&handle, nodes, vehicle_num, n_orders); auto v_cost_matrix = cuopt::device_copy(cost_matrix, stream); - auto v_time_matrix = cuopt::device_copy(time_matrix, stream); auto v_order_locations = cuopt::device_copy(order_locations, stream); auto v_order_earliest = cuopt::device_copy(order_earliest, stream); auto v_order_latest = cuopt::device_copy(order_latest, stream); @@ -401,7 +397,7 @@ TEST(vehicle_breaks, non_uniform_breaks) auto v_break_locations = cuopt::device_copy(break_locations, stream); data_model.add_cost_matrix(v_cost_matrix.data()); - data_model.add_transit_time_matrix(v_time_matrix.data()); + data_model.add_transit_time_matrix(v_cost_matrix.data()); data_model.set_order_locations(v_order_locations.data()); data_model.set_order_time_windows(v_order_earliest.data(), v_order_latest.data()); data_model.set_order_service_times(v_order_service.data()); From 8612bbea33bf2134414e98c73384d0af1006bd17 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 10 Mar 2026 18:42:58 -0700 Subject: [PATCH 5/7] formatting --- cpp/tests/routing/unit_tests/breaks.cu | 22 +++++++----- .../routing/unit_tests/heterogenous_breaks.cu | 36 +++++++++---------- .../routing/utilities/check_constraints.cu | 27 +++++++------- .../cuopt/routing/vehicle_routing_wrapper.pyx | 2 +- .../cuopt/tests/routing/test_data_model.py | 3 +- .../tests/routing/test_initial_solutions.py | 8 ++--- .../cuopt/cuopt/tests/routing/test_solver.py | 25 ++++++++----- .../tests/routing/test_vehicle_properties.py | 11 +++--- .../tests/routing/test_warnings_exceptions.py | 10 +++--- 9 files changed, 77 insertions(+), 67 deletions(-) diff --git a/cpp/tests/routing/unit_tests/breaks.cu b/cpp/tests/routing/unit_tests/breaks.cu index 3d6e48a25d..d433f9fd97 100644 --- a/cpp/tests/routing/unit_tests/breaks.cu +++ b/cpp/tests/routing/unit_tests/breaks.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -262,7 +262,7 @@ TEST(vehicle_breaks, uniform_breaks) order_latest[i] = route.latest_time_h[i + 1]; order_service[i] = route.service_time_h[i + 1]; } - int num_breaks = 2; + int num_breaks = 2; std::vector break_earliest(vehicle_num * num_breaks); std::vector break_latest(vehicle_num * num_breaks); std::vector break_duration(vehicle_num * num_breaks); @@ -276,7 +276,9 @@ TEST(vehicle_breaks, uniform_breaks) } std::vector break_locations(nodes); - for (int i = 0; i < nodes; ++i) { break_locations[i] = i; } + for (int i = 0; i < nodes; ++i) { + break_locations[i] = i; + } cuopt::routing::data_model_view_t data_model(&handle, nodes, vehicle_num, n_orders); @@ -306,13 +308,13 @@ TEST(vehicle_breaks, uniform_breaks) dim1_duration[v] = break_duration[v * num_breaks + 1]; } auto v_break_earliest_0 = cuopt::device_copy(dim0_earliest, stream); - auto v_break_latest_0 = cuopt::device_copy(dim0_latest, stream); + auto v_break_latest_0 = cuopt::device_copy(dim0_latest, stream); auto v_break_duration_0 = cuopt::device_copy(dim0_duration, stream); data_model.add_break_dimension( v_break_earliest_0.data(), v_break_latest_0.data(), v_break_duration_0.data()); auto v_break_earliest_1 = cuopt::device_copy(dim1_earliest, stream); - auto v_break_latest_1 = cuopt::device_copy(dim1_latest, stream); + auto v_break_latest_1 = cuopt::device_copy(dim1_latest, stream); auto v_break_duration_1 = cuopt::device_copy(dim1_duration, stream); data_model.add_break_dimension( v_break_earliest_1.data(), v_break_latest_1.data(), v_break_duration_1.data()); @@ -354,9 +356,9 @@ TEST(vehicle_breaks, non_uniform_breaks) order_latest[i] = route.latest_time_h[i + 1]; order_service[i] = route.service_time_h[i + 1]; } - int num_v_type_1 = vehicle_num / 2; - int num_v_type_2 = vehicle_num - num_v_type_1; - int num_breaks = 3; + int num_v_type_1 = vehicle_num / 2; + int num_v_type_2 = vehicle_num - num_v_type_1; + int num_breaks = 3; // Type 1: [40,50]/5, [100,120]/20, [170,180]/10 // Type 2: [60,90]/20, [110,120]/10, [200,210]/5 @@ -388,7 +390,9 @@ TEST(vehicle_breaks, non_uniform_breaks) // Depot (0) excluded from break locations std::vector break_locations(nodes - 1); - for (int i = 0; i < nodes - 1; ++i) { break_locations[i] = i + 1; } + for (int i = 0; i < nodes - 1; ++i) { + break_locations[i] = i + 1; + } cuopt::routing::data_model_view_t data_model(&handle, nodes, vehicle_num, n_orders); diff --git a/cpp/tests/routing/unit_tests/heterogenous_breaks.cu b/cpp/tests/routing/unit_tests/heterogenous_breaks.cu index a522a98372..40d1591590 100644 --- a/cpp/tests/routing/unit_tests/heterogenous_breaks.cu +++ b/cpp/tests/routing/unit_tests/heterogenous_breaks.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -114,20 +114,21 @@ TEST(heterogenous_breaks, simple_non_uniform) } // Test heterogenous breaks (Solomon 100 nodes): -// Half of vehicles have 2 breaks with custom locations; remaining half have 3 breaks with default (any) location. +// Half of vehicles have 2 breaks with custom locations; remaining half have 3 breaks with default +// (any) location. TEST(heterogenous_breaks, test_heterogeneous_breaks) { raft::handle_t handle; auto stream = handle.get_stream(); const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); - std::string routing_file = rapidsDatasetRootDir + "/solomon/In/r107.txt"; + std::string routing_file = rapidsDatasetRootDir + "/solomon/In/r107.txt"; Route route; load_solomon(routing_file, route, 101); - int nodes = route.n_locations; - int n_orders = nodes - 1; - int vehicle_num = 20; + int nodes = route.n_locations; + int n_orders = nodes - 1; + int vehicle_num = 20; int num_v_type_1 = vehicle_num / 2; std::vector cost_matrix(nodes * nodes), time_matrix(nodes * nodes); @@ -143,9 +144,12 @@ TEST(heterogenous_breaks, test_heterogeneous_breaks) order_service[i] = route.service_time_h[i + 1]; } - // Type 1: 2 breaks [90,100]/15, [150,170]/15 at locations 5,10,15,...,85 (every 5th node, excluding depot) + // Type 1: 2 breaks [90,100]/15, [150,170]/15 at locations 5,10,15,...,85 (every 5th node, + // excluding depot) std::vector break_locations_1; - for (int i = 1; i <= 17; ++i) { break_locations_1.push_back(5 * i); } + for (int i = 1; i <= 17; ++i) { + break_locations_1.push_back(5 * i); + } cuopt::routing::data_model_view_t data_model(&handle, nodes, vehicle_num, n_orders); @@ -165,18 +169,10 @@ TEST(heterogenous_breaks, test_heterogeneous_breaks) auto v_break_locations_1 = cuopt::device_copy(break_locations_1, stream); for (int i = 0; i < num_v_type_1; ++i) { - data_model.add_vehicle_break(i, - 90, - 100, - 15, - v_break_locations_1.data(), - static_cast(break_locations_1.size())); - data_model.add_vehicle_break(i, - 150, - 170, - 15, - v_break_locations_1.data(), - static_cast(break_locations_1.size())); + data_model.add_vehicle_break( + i, 90, 100, 15, v_break_locations_1.data(), static_cast(break_locations_1.size())); + data_model.add_vehicle_break( + i, 150, 170, 15, v_break_locations_1.data(), static_cast(break_locations_1.size())); } // Type 2: 3 breaks [40,50]/10, [110,120]/10, [160,170]/10 with default (any) location diff --git a/cpp/tests/routing/utilities/check_constraints.cu b/cpp/tests/routing/utilities/check_constraints.cu index c9dc183096..e5debd4440 100644 --- a/cpp/tests/routing/utilities/check_constraints.cu +++ b/cpp/tests/routing/utilities/check_constraints.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -159,7 +159,7 @@ void check_route(data_model_view_t const& data_model, } if (has_breaks) { - int break_dim = 0; + int break_dim = 0; auto const& non_uniform = data_model.get_non_uniform_breaks(); bool use_uniform = !uniform_break_earliest_h.empty(); bool use_non_uniform = (non_uniform.count(id) > 0); @@ -168,13 +168,13 @@ void check_route(data_model_view_t const& data_model, double arrival = h_routing_solution.stamp[k]; i_t break_loc_id = locations[k]; if (use_uniform && break_dim < static_cast(uniform_break_earliest_h.size())) { - //std::cout<<"VEHID: "<(uniform_break_earliest_h[break_dim][id]) - 1e-6) + // std::cout<<"VEHID: "<(uniform_break_earliest_h[break_dim][id]) - 1e-6) << "Break " << break_dim << " vehicle " << id << " arrival " << arrival << " before earliest " << uniform_break_earliest_h[break_dim][id]; - ASSERT_LE(arrival, - static_cast(uniform_break_latest_h[break_dim][id]) + 1e-6) + ASSERT_LE(arrival, static_cast(uniform_break_latest_h[break_dim][id]) + 1e-6) << "Break " << break_dim << " vehicle " << id << " arrival " << arrival << " after latest " << uniform_break_latest_h[break_dim][id]; if (!uniform_break_locations_set.empty()) { @@ -192,9 +192,8 @@ void check_route(data_model_view_t const& data_model, << "Non-uniform break " << break_dim << " vehicle " << id; if (b.locations_.size() > 0) { auto allowed_locs = cuopt::host_copy(b.locations_, stream); - bool found = - std::find(allowed_locs.begin(), allowed_locs.end(), break_loc_id) != - allowed_locs.end(); + bool found = std::find(allowed_locs.begin(), allowed_locs.end(), break_loc_id) != + allowed_locs.end(); ASSERT_TRUE(found) << "Non-uniform break " << break_dim << " vehicle " << id << " at location " << break_loc_id << " not in allowed break locations"; @@ -206,12 +205,12 @@ void check_route(data_model_view_t const& data_model, } if (use_uniform) { ASSERT_EQ(break_dim, static_cast(uniform_break_earliest_h.size())) - << "Vehicle " << id << " break count " << break_dim - << " expected " << uniform_break_earliest_h.size(); + << "Vehicle " << id << " break count " << break_dim << " expected " + << uniform_break_earliest_h.size(); } else if (use_non_uniform) { ASSERT_EQ(break_dim, static_cast(non_uniform.at(id).size())) - << "Vehicle " << id << " non-uniform break count " << break_dim - << " expected " << non_uniform.at(id).size(); + << "Vehicle " << id << " non-uniform break count " << break_dim << " expected " + << non_uniform.at(id).size(); } } diff --git a/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx b/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx index 1981c3ddd9..217d5aeae3 100644 --- a/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx +++ b/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx @@ -655,7 +655,7 @@ cdef class DataModel: } def get_order_service_times(self, vehicle_id): - if vehicle_id in self.order_service_times: + if vehicle_id in self.order_service_times: return self.order_service_times[vehicle_id] else: return cudf.Series([]) diff --git a/python/cuopt/cuopt/tests/routing/test_data_model.py b/python/cuopt/cuopt/tests/routing/test_data_model.py index b8525d8ee8..ad9f0b53a2 100644 --- a/python/cuopt/cuopt/tests/routing/test_data_model.py +++ b/python/cuopt/cuopt/tests/routing/test_data_model.py @@ -1,8 +1,7 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import numpy as np -import pytest import cudf diff --git a/python/cuopt/cuopt/tests/routing/test_initial_solutions.py b/python/cuopt/cuopt/tests/routing/test_initial_solutions.py index a7b521b0c6..5ce7dc5601 100644 --- a/python/cuopt/cuopt/tests/routing/test_initial_solutions.py +++ b/python/cuopt/cuopt/tests/routing/test_initial_solutions.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from enum import Enum @@ -128,9 +128,9 @@ def test_initial_solutions(flag): ret_initial = d.get_initial_solutions() assert len(ret_initial) == 4 ret_sizes = sorted(len(x) for x in ret_initial) - expected_sizes = sorted([ - len(vehicle_ids), len(routes), len(types), len(sol_offsets) - ]) + expected_sizes = sorted( + [len(vehicle_ids), len(routes), len(types), len(sol_offsets)] + ) assert ret_sizes == expected_sizes s.set_time_limit(1) diff --git a/python/cuopt/cuopt/tests/routing/test_solver.py b/python/cuopt/cuopt/tests/routing/test_solver.py index 016e1c0124..409c0ffc04 100644 --- a/python/cuopt/cuopt/tests/routing/test_solver.py +++ b/python/cuopt/cuopt/tests/routing/test_solver.py @@ -1,7 +1,6 @@ -# 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 math import os import numpy as np @@ -10,7 +9,9 @@ from cuopt import routing from cuopt.routing import utils -SOLOMON_DATASETS_PATH = os.path.join(utils.RAPIDS_DATASET_ROOT_DIR, "solomon/In/") +SOLOMON_DATASETS_PATH = os.path.join( + utils.RAPIDS_DATASET_ROOT_DIR, "solomon/In/" +) """def test_solomon(): SOLOMON_DATASET = "r107.txt" @@ -57,6 +58,7 @@ assert math.fabs((final_cost - ref_cost) / ref_cost) < 0.1 """ + def test_pdptw(): """ Solve a small PDPTW: 5 locations (depot 0, pickups 1–2, deliveries 3–4), @@ -103,10 +105,16 @@ def test_pdptw(): # Getter checks: pickup/delivery pairs and transit time matrix ret_pickup, ret_delivery = dm.get_pickup_delivery_pairs() - assert (ret_pickup == pickup_indices).all(), "get_pickup_delivery_pairs pickup mismatch" - assert (ret_delivery == delivery_indices).all(), "get_pickup_delivery_pairs delivery mismatch" + assert (ret_pickup == pickup_indices).all(), ( + "get_pickup_delivery_pairs pickup mismatch" + ) + assert (ret_delivery == delivery_indices).all(), ( + "get_pickup_delivery_pairs delivery mismatch" + ) ret_transit = dm.get_transit_time_matrix(0) - assert cudf.DataFrame(ret_transit).equals(times), "get_transit_time_matrix mismatch" + assert cudf.DataFrame(ret_transit).equals(times), ( + "get_transit_time_matrix mismatch" + ) settings = routing.SolverSettings() settings.set_time_limit(10) @@ -114,7 +122,9 @@ def test_pdptw(): solution = routing.Solve(dm, settings) status = solution.get_status() - assert status == 0, f"Expected status 0, got {status}: {solution.get_message()}" + assert status == 0, ( + f"Expected status 0, got {status}: {solution.get_message()}" + ) assert solution.get_vehicle_count() >= 1 # Exercise Assignment getters (return type / no raise) assert isinstance(solution.get_accepted_solutions(), cudf.Series) @@ -250,4 +260,3 @@ def test_prize_collection(): assert objectives[routing.Objective.COST] == 13.0 assert sol.get_status() == 0 assert sol.get_vehicle_count() >= 2 - diff --git a/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py b/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py index a5090f1907..cdf25291f2 100644 --- a/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py +++ b/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import numpy as np @@ -193,7 +193,10 @@ def test_vehicle_max_times_fail(): routing_solution = routing.Solve(d, s) assert routing_solution.get_error_status() == ErrorStatus.ValidationError err_msg = routing_solution.get_error_message().decode() - assert "Time matrix should be set in order to use vehicle max time constraints" in err_msg + assert ( + "Time matrix should be set in order to use vehicle max time constraints" + in err_msg + ) def test_vehicle_max_times(): @@ -263,7 +266,7 @@ def test_order_to_vehicle_match(): ret_order_vehicle = d.get_order_vehicle_match() assert set(ret_order_vehicle.keys()) == set(order_vehicle_match.keys()) for order, vehicles in order_vehicle_match.items(): - assert (ret_order_vehicle[order].to_arrow().to_pylist() == vehicles) + assert ret_order_vehicle[order].to_arrow().to_pylist() == vehicles s = routing.SolverSettings() s.set_time_limit(10) @@ -309,7 +312,7 @@ def test_vehicle_to_order_match(): ret_vehicle_order = d.get_vehicle_order_match() assert set(ret_vehicle_order.keys()) == set(vehicle_order_match.keys()) for vehicle, orders in vehicle_order_match.items(): - assert (ret_vehicle_order[vehicle].to_arrow().to_pylist() == orders) + assert ret_vehicle_order[vehicle].to_arrow().to_pylist() == orders s = routing.SolverSettings() s.set_time_limit(10) diff --git a/python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py b/python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py index 1b8e3662e7..aafcb45a6a 100644 --- a/python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py +++ b/python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import warnings @@ -58,7 +58,9 @@ def test_dist_mat(): with pytest.raises(Exception) as exc_info: dm = routing.DataModel(cost_matrix.shape[0], 3) dm.add_cost_matrix(cost_matrix[:3]) - assert str(exc_info.value) == "cost matrix is expected to be a square matrix" + assert ( + str(exc_info.value) == "cost matrix is expected to be a square matrix" + ) with pytest.raises(Exception) as exc_info: dm = routing.DataModel(cost_matrix.shape[0], 3) dm.add_cost_matrix(cost_matrix) @@ -109,9 +111,7 @@ def test_time_windows(): def test_range(): - cost_matrix = cudf.DataFrame( - [[0, 5.0, 5.0], [5.0, 0, 5.0], [5.0, 5.0, 0]] - ) + cost_matrix = cudf.DataFrame([[0, 5.0, 5.0], [5.0, 0, 5.0], [5.0, 5.0, 0]]) dm = routing.DataModel(cost_matrix.shape[0], 3, 5) dm.add_cost_matrix(cost_matrix) with pytest.raises(Exception) as exc_info: From 4f6b141d45a25210014b6e873189d95561391b3d Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 10 Mar 2026 18:54:51 -0700 Subject: [PATCH 6/7] formatting --- cpp/tests/routing/utilities/test_utilities.hpp | 2 +- python/cuopt/cuopt/tests/routing/test_solver.py | 2 +- python/cuopt/cuopt/tests/routing/test_solver_settings.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/tests/routing/utilities/test_utilities.hpp b/cpp/tests/routing/utilities/test_utilities.hpp index 8eb9e08842..1543f91426 100644 --- a/cpp/tests/routing/utilities/test_utilities.hpp +++ b/cpp/tests/routing/utilities/test_utilities.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * 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 */ /* clang-format on */ diff --git a/python/cuopt/cuopt/tests/routing/test_solver.py b/python/cuopt/cuopt/tests/routing/test_solver.py index d85417beae..9c16c375c4 100644 --- a/python/cuopt/cuopt/tests/routing/test_solver.py +++ b/python/cuopt/cuopt/tests/routing/test_solver.py @@ -13,6 +13,7 @@ utils.RAPIDS_DATASET_ROOT_DIR, "solomon/In/" ) + def test_solomon(): SOLOMON_DATASET = "r107.txt" SOLOMON_YAML = "r107.yaml" @@ -58,7 +59,6 @@ def test_solomon(): assert math.fabs((final_cost - ref_cost) / ref_cost) < 0.1 - def test_pdptw(): """ Solve a small PDPTW: 5 locations (depot 0, pickups 1–2, deliveries 3–4), diff --git a/python/cuopt/cuopt/tests/routing/test_solver_settings.py b/python/cuopt/cuopt/tests/routing/test_solver_settings.py index cfcb3f6e27..0472b10a8b 100644 --- a/python/cuopt/cuopt/tests/routing/test_solver_settings.py +++ b/python/cuopt/cuopt/tests/routing/test_solver_settings.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import numpy as np From 5020a0068b8d2dfce9427f20527f662879a9f65c Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Wed, 11 Mar 2026 15:52:29 -0700 Subject: [PATCH 7/7] add missing import --- python/cuopt/cuopt/tests/routing/test_solver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/cuopt/cuopt/tests/routing/test_solver.py b/python/cuopt/cuopt/tests/routing/test_solver.py index 9c16c375c4..92622b1f16 100644 --- a/python/cuopt/cuopt/tests/routing/test_solver.py +++ b/python/cuopt/cuopt/tests/routing/test_solver.py @@ -9,6 +9,8 @@ from cuopt import routing from cuopt.routing import utils +import math + SOLOMON_DATASETS_PATH = os.path.join( utils.RAPIDS_DATASET_ROOT_DIR, "solomon/In/" )