diff --git a/python/cuopt/cuopt/tests/quadratic_programming/test_qp.py b/python/cuopt/cuopt/tests/quadratic_programming/test_qp.py deleted file mode 100644 index 94db751148..0000000000 --- a/python/cuopt/cuopt/tests/quadratic_programming/test_qp.py +++ /dev/null @@ -1,69 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa -# SPDX-License-Identifier: Apache-2.0 - - -import numpy as np -import pytest - -from cuopt.linear_programming import data_model, solver, solver_settings -from cuopt.linear_programming.solver.solver_parameters import ( - CUOPT_ITERATION_LIMIT, -) - - -def test_solver(): - data_model_obj = data_model.DataModel() - - # Minimize x1 ^2 + 4 x2 ^2 - 8 x1 - 16 x2 - # subject to x1 + x2 >= 5 - # x1 >= 3 - # x2 >= 0 - - # A - A_values = np.array([1.0, 1.0]) - A_indices = np.array([0, 1]) - A_offsets = np.array([0, 2]) - data_model_obj.set_csr_constraint_matrix(A_values, A_indices, A_offsets) - - # constr_lb - constr_lower_bounds = np.array([5.0]) - data_model_obj.set_constraint_lower_bounds(constr_lower_bounds) - - # constr_upper_bounds - constr_upper_bounds = np.array([np.inf]) - data_model_obj.set_constraint_upper_bounds(constr_upper_bounds) - - # variable bounds - lb = np.array([0.0, 0.0]) - # ub = np.array([np.inf, np.inf]) - ub = np.array([10.0, 10.0]) - data_model_obj.set_variable_lower_bounds(lb) - data_model_obj.set_variable_upper_bounds(ub) - - # c - c = np.array([-8.0, -16.0]) - data_model_obj.set_objective_coefficients(c) - - # Q - Q_values = np.array([1.0, 4.0]) - Q_indices = np.array([0, 1]) - Q_offsets = np.array([0, 1, 2]) - - data_model_obj.set_quadratic_objective_matrix( - Q_values, Q_indices, Q_offsets - ) - - test_q = data_model_obj.get_quadratic_objective_values() - print(f"test_q: {test_q}") - test_q_indices = data_model_obj.get_quadratic_objective_indices() - print(f"test_q_indices: {test_q_indices}") - test_q_offsets = data_model_obj.get_quadratic_objective_offsets() - print(f"test_q_offsets: {test_q_offsets}") - - settings = solver_settings.SolverSettings() - settings.set_parameter(CUOPT_ITERATION_LIMIT, 10) - solution = solver.Solve(data_model_obj, settings) - assert solution.get_termination_reason() == "Optimal" - assert solution.get_primal_objective() == pytest.approx(-32) - assert solution.get_primal_solution()[0] == pytest.approx(4.0) - assert solution.get_primal_solution()[1] == pytest.approx(2.0) diff --git a/python/cuopt/cuopt/tests/routing/test_distance_engine.py b/python/cuopt/cuopt/tests/routing/test_distance_engine.py index d2cd4c199f..1cb59b0e33 100644 --- a/python/cuopt/cuopt/tests/routing/test_distance_engine.py +++ b/python/cuopt/cuopt/tests/routing/test_distance_engine.py @@ -1,272 +1,14 @@ -# 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 numpy as np -import pandas as pd import pytest import cudf from cuopt import distance_engine, routing -from cuopt.routing import utils from cuopt.utilities import InputValidationError -depot = "HP 127" - -# Helper functions - - -def get_graph_data(file, type_t): - graph_info = [] - graph_info_file = open(file, "r") - for i, line in enumerate(graph_info_file): - graph_info.append(np.array(line.split(",")[:-1]).astype(type_t[i])) - - return graph_info[0], graph_info[1], graph_info[2] - - -def get_target_locations(matrix_df_all, raw_order_pdf): - target_locations = [] - - matrix_df_all_cols = matrix_df_all.index.tolist() - pickup = raw_order_pdf["source"].tolist() - delivery = raw_order_pdf["sink"].tolist() - orders = [depot] + pickup + delivery - - unique_locations = {} - order_locations = [] - locations = [] - cnt = 0 - 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)) - - for i in range(cnt): - target_locations.append(matrix_df_all_cols.index(locations[i])) - - return np.array(target_locations) - - -def get_date(date_time): - d, t = date_time.split(" ") - return d - - -def get_time(date_time, date, rt, t_type): - d, t = date_time.split(" ") - hh, mm, ss = t.split(":") - add_day = 0 - if d != date: - add_day = 24 * 60 * 60 - if ss == "00": - mytime = int(hh) * 60 * 60 + int(mm) * 60 + add_day - else: - mytime = int(hh) * 60 * 60 + int(mm) * 60 + int(ss) + add_day - if t_type == "earliest": - mytime = mytime - rt - else: - mytime = mytime - return mytime - - -def get_service_time(t): - hh, mm, ss = t.split(":") - return int(hh) * 60 * 60 + int(mm) * 60 + int(ss) - - -def check_matrix( - waypoint_matrix, - matrix_df, - matrix_df_all, - single_day_order_pdf, - speed, - weights, -): - # Set target locations - target_locations = get_target_locations( - matrix_df_all, single_day_order_pdf - ) - - # Compute cost matrix - computed_cost_matrix = waypoint_matrix.compute_cost_matrix( - target_locations - ) - computed_cost_matrix = computed_cost_matrix / speed - - # Compute time matrix with same weight - computed_time_matrix = waypoint_matrix.compute_shortest_path_costs( - target_locations, weights - ) - computed_time_matrix = computed_time_matrix / speed - - # Check matrix - assert np.isclose( - computed_cost_matrix.to_pandas(), matrix_df.to_pandas() - ).all() - - assert np.isclose( - computed_cost_matrix.to_pandas(), computed_time_matrix.to_pandas() - ).all() - - -def test_compute_cost_matrix(): - # Data loading - matrix_df_all = pd.read_csv( - utils.RAPIDS_DATASET_ROOT_DIR + "/distance_engine/traveltimes.csv", - index_col=0, - ) - order_pdf = pd.read_csv( - utils.RAPIDS_DATASET_ROOT_DIR + "/distance_engine/order_sample.csv", - encoding="ISO-8859-1", - ) - matrix_pdf = pd.read_csv( - utils.RAPIDS_DATASET_ROOT_DIR + "/distance_engine/ref_cost_matrix.csv", - sep=";", - header=0, - ) - - # Get orders delivered using direct transport - output_pdf = pd.read_csv( - utils.RAPIDS_DATASET_ROOT_DIR - + "/distance_engine/direct_transport_sample.csv", - index_col=0, - ) - - my_orders = output_pdf.Assignments.unique() - order_pdf = order_pdf[order_pdf["#"].isin(my_orders)] - - # Rename demand column - demand_column_name = "Resupply Quantity" - order_pdf = order_pdf.rename(columns={demand_column_name: "demand"}) - order_pdf = order_pdf.rename( - columns={ - "Stopping point at source stage": "source", - "Stopping point at sink stage": "sink", - } - ) - - # Set date - date = "18.02.2021" - order_pdf["earliest_date"] = order_pdf["Earliest delivery time"].apply( - lambda x: get_date(x) - ) - order_pdf["latest_date"] = order_pdf["Latest Provision Time"].apply( - lambda x: get_date(x) - ) - - # Add service time from output file - pickups = output_pdf[ - output_pdf["Activity"].isin(["Loading", "LoadingEmptyBoxes"]) - ][["Assignments", "Duration"]] - pickups = pickups.rename(columns={"Duration": "pickup_service_time"}) - deliveries = output_pdf[ - output_pdf["Activity"].isin(["Unloading", "UnloadingEmptyBoxes"]) - ][["Assignments", "Duration"]] - deliveries = deliveries.rename( - columns={"Duration": "delivery_service_time"} - ) - - order_pdf = order_pdf.merge( - pickups, how="left", left_on="#", right_on="Assignments" - ) - order_pdf = order_pdf.merge( - deliveries, how="left", left_on="#", right_on="Assignments" - ) - order_pdf["pickup_service_time"] = order_pdf["pickup_service_time"].apply( - lambda x: get_service_time(x) - ) - order_pdf["delivery_service_time"] = order_pdf[ - "delivery_service_time" - ].apply(lambda x: get_service_time(x)) - - # Compute earliest and latest time - # with earliest time relaxation of 60 minutes - relaxation_time = 3600 - order_pdf["earliest_time"] = order_pdf["Earliest delivery time"].apply( - lambda x: get_time(x, date, relaxation_time, "earliest") - ) - order_pdf["latest_time"] = order_pdf["Latest Provision Time"].apply( - lambda x: get_time(x, date, relaxation_time, "latest") - ) - - # Run in batches of 10mins - my_batch = [600 * i for i in range(0, 289)] - - num_vehicles = 600 - vehicle_speed = 2.2 - vehicle_capacity = 1 - break_earliest = [ - 21600, - 34200, - 47700, - 61200, - 72000, - 87300, - 108000, - 120600, - 134100, - 147600, - 158400, - ] - break_latest = [ - 21900, - 35100, - 50400, - 61500, - 74700, - 88200, - 108300, - 121500, - 136800, - 147900, - 161100, - ] - - # Set vehicle constraints - vehicle_constraints = routing.add_vehicle_constraints( - num_vehicles, - vehicle_capacity, - break_earliest, - break_latest, - vehicle_speed, - ) - - # Build waypoint matrix - offsets, indices, weights = get_graph_data( - utils.RAPIDS_DATASET_ROOT_DIR + "/distance_engine/waypoint_matrix.txt", - ["int", "int", "float"], - ) - waypoint_matrix = distance_engine.WaypointMatrix(offsets, indices, weights) - - # Run loop and check matrices are equal every time - for i in range(0, 288): - single_day_order_pdf = order_pdf[ - (order_pdf["latest_time"] > my_batch[i]) - & (order_pdf["latest_time"] <= my_batch[i + 1]) - ] - - if len(single_day_order_pdf) > 0: - ( - matrix_df, - order_data, - vehicle_data, - ) = routing.create_pickup_delivery_data( - matrix_pdf, single_day_order_pdf, depot, vehicle_constraints - ) - check_matrix( - waypoint_matrix, - matrix_df, - matrix_df_all, - single_day_order_pdf, - vehicle_constraints.speed, - weights, - ) - def start_compute_waypoint_sequence( locations, n_vehicles, min_vehicles, set_order_locations @@ -370,33 +112,6 @@ def start_compute_waypoint_sequence_no_matrix_call(locations): ) -def start_compute_shortest_path_costs(): - offsets = np.array([0, 2, 3, 4, 6, 8, 9, 10]) - edges = np.array([1, 6, 4, 3, 2, 4, 2, 6, 4, 0]) - weights = np.array([2, 10, 3, 2, 2, 5, 1, 1, 2, 10]) - target_locations = np.array([0, 3, 6]) - custom_weights = np.array( - [1, 10000000, 10, 1000, 1000, 10000, 100, 100000, 1000000, 10000000], - dtype="float", - ) - expected_custom_matrix = np.array( - [[0, 1111, 100011], [10110000, 0, 110000], [10000000, 10001111, 0]], - dtype="float", - ) - - w_matrix = distance_engine.WaypointMatrix(offsets, edges, weights) - - w_matrix.compute_cost_matrix(target_locations) - - custom_cost_matrix = w_matrix.compute_shortest_path_costs( - target_locations, custom_weights - ) - - assert np.array_equal( - custom_cost_matrix.to_numpy(), expected_custom_matrix - ) - - def start_waypoint_matrix_validity(): data = { "offsets": [0, 3, 5, 7, 8, 9], @@ -545,10 +260,6 @@ def test_compute_waypoint_sequence_no_matrix_call(): start_compute_waypoint_sequence_no_matrix_call([0, 1, 2, 4]) -def test_compute_shortest_path_costs(): - start_compute_shortest_path_costs() - - def test_waypoint_matrix_validity(): start_waypoint_matrix_validity() diff --git a/python/cuopt/cuopt/tests/routing/test_solver.py b/python/cuopt/cuopt/tests/routing/test_solver.py index b7cca6c628..5ace1c72fd 100644 --- a/python/cuopt/cuopt/tests/routing/test_solver.py +++ b/python/cuopt/cuopt/tests/routing/test_solver.py @@ -1,156 +1,12 @@ # SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -import os import numpy as np import pytest import cudf from cuopt import routing -from cuopt.routing import utils - -import math - -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():