From 8f2b4212b9d968603b961befef0980fa5c28fea9 Mon Sep 17 00:00:00 2001 From: Jose Maria Baca Date: Fri, 2 Oct 2026 09:32:45 +0200 Subject: [PATCH 1/4] feat(routing): add optional distance matrices to C++ models Signed-off-by: Jose Maria Baca --- .../cuopt/routing/cpu_routing_problem.hpp | 1 + cpp/include/cuopt/routing/data_model_view.hpp | 17 ++ cpp/src/routing/cpu_routing_problem.cu | 33 ++++ cpp/src/routing/data_model_view.cu | 21 +++ cpp/src/routing/fleet_info.cu | 24 +++ cpp/src/routing/fleet_info.hpp | 13 +- cpp/src/routing/solver.cu | 6 +- cpp/src/routing/utilities/md_utils.hpp | 146 +++++++++++++----- cpp/src/routing/vehicle_info.hpp | 8 +- 9 files changed, 225 insertions(+), 44 deletions(-) diff --git a/cpp/include/cuopt/routing/cpu_routing_problem.hpp b/cpp/include/cuopt/routing/cpu_routing_problem.hpp index bfbe3193ea..796215001b 100644 --- a/cpp/include/cuopt/routing/cpu_routing_problem.hpp +++ b/cpp/include/cuopt/routing/cpu_routing_problem.hpp @@ -79,6 +79,7 @@ class cpu_routing_problem_t { std::vector cost_matrices; std::vector transit_time_matrices; + std::vector distance_matrices; std::vector vehicle_start_locations; std::vector vehicle_return_locations; diff --git a/cpp/include/cuopt/routing/data_model_view.hpp b/cpp/include/cuopt/routing/data_model_view.hpp index 2c44b2eeeb..1c815e53fa 100644 --- a/cpp/include/cuopt/routing/data_model_view.hpp +++ b/cpp/include/cuopt/routing/data_model_view.hpp @@ -76,6 +76,16 @@ class data_model_view_t { */ void add_cost_matrix(f_t const* matrix, uint8_t vehicle_type = 0); + /** + * @brief Register an optional physical distance matrix, separate from cost and time. + * + * @param[in] matrix Non-owning device pointer to a square row-major matrix. + * @param[in] + * vehicle_type Vehicle-type identifier. + * @throws cuopt::logic_error if matrix is null. + */ + void add_distance_matrix(f_t const* matrix, uint8_t vehicle_type = 0); + /** * @brief Set a transit time matrix for all locations (depot included) at * once. The time matrix is used to check constraints satisfiability rather @@ -436,6 +446,9 @@ class data_model_view_t { */ f_t const* get_cost_matrix(uint8_t vehicle_type = 0) const noexcept; + /** @brief Return the distance matrix for a vehicle type, or nullptr if not set. */ + f_t const* get_distance_matrix(uint8_t vehicle_type = 0) const noexcept; + /** * @brief Get transit time matrix * @return Matrix pointer @@ -448,6 +461,9 @@ class data_model_view_t { */ std::unordered_map get_cost_matrices() const noexcept; + /** @brief Return all registered physical distance matrices. */ + std::unordered_map get_distance_matrices() const noexcept; + /** * @brief Get all transit time matrices as a map * @return map of vehicle type to transit time matrix @@ -661,6 +677,7 @@ class data_model_view_t { i_t n_requests_{}; raft::device_span vehicle_types_; std::unordered_map cost_matrices_{}; + std::unordered_map distance_matrices_{}; std::unordered_map transit_time_matrices_{}; i_t const* order_locations_{nullptr}; i_t const* break_locations_{nullptr}; diff --git a/cpp/src/routing/cpu_routing_problem.cu b/cpp/src/routing/cpu_routing_problem.cu index 03bf7e5569..c041e7f57d 100644 --- a/cpp/src/routing/cpu_routing_problem.cu +++ b/cpp/src/routing/cpu_routing_problem.cu @@ -15,6 +15,8 @@ #include #include +#include +#include #include #include @@ -24,6 +26,7 @@ namespace routing { struct cpu_routing_problem_t::device_data_t { std::vector>> cost_matrices; std::vector>> transit_time_matrices; + std::vector>> distance_matrices; std::unique_ptr> vehicle_start_locations; std::unique_ptr> vehicle_return_locations; @@ -117,8 +120,13 @@ cpu_routing_problem_t::to_device(raft::handle_t* handle) const int32_t orders = (num_orders < 0) ? num_locations : num_orders; data_model_view_t view(handle, num_locations, fleet_size, orders); + const auto matrix_size = static_cast(num_locations) * num_locations; for (auto const& cm : cost_matrices) { + if (cm.matrix.size() != matrix_size) { + throw std::invalid_argument( + "cpu_routing_problem_t::to_device: cost matrix size must equal num_locations squared"); + } auto d = copy_vector(cm.matrix, stream); if (!d) { throw std::invalid_argument("cpu_routing_problem_t::to_device: empty cost matrix"); } view.add_cost_matrix(d->data(), cm.vehicle_type); @@ -126,6 +134,11 @@ cpu_routing_problem_t::to_device(raft::handle_t* handle) const } for (auto const& tm : transit_time_matrices) { + if (tm.matrix.size() != matrix_size) { + throw std::invalid_argument( + "cpu_routing_problem_t::to_device: transit time matrix size must equal num_locations " + "squared"); + } auto d = copy_vector(tm.matrix, stream); if (!d) { throw std::invalid_argument("cpu_routing_problem_t::to_device: empty transit time matrix"); @@ -134,6 +147,26 @@ cpu_routing_problem_t::to_device(raft::handle_t* handle) const data->transit_time_matrices.push_back(std::move(d)); } + for (auto const& dm : distance_matrices) { + if (dm.matrix.size() != matrix_size) { + throw std::invalid_argument( + "cpu_routing_problem_t::to_device: distance matrix size must equal num_locations squared"); + } + if (std::any_of(dm.matrix.begin(), dm.matrix.end(), [](float value) { + return std::isnan(value) || value < 0.f; + })) { + throw std::invalid_argument( + "cpu_routing_problem_t::to_device: distance matrix values must be non-negative and not " + "NaN"); + } + auto d = copy_vector(dm.matrix, stream); + if (!d) { + throw std::invalid_argument("cpu_routing_problem_t::to_device: empty distance matrix"); + } + view.add_distance_matrix(d->data(), dm.vehicle_type); + data->distance_matrices.push_back(std::move(d)); + } + if (!vehicle_start_locations.empty() && !vehicle_return_locations.empty()) { data->vehicle_start_locations = copy_vector(vehicle_start_locations, stream); data->vehicle_return_locations = copy_vector(vehicle_return_locations, stream); diff --git a/cpp/src/routing/data_model_view.cu b/cpp/src/routing/data_model_view.cu index 40b82a2f09..cccd677875 100644 --- a/cpp/src/routing/data_model_view.cu +++ b/cpp/src/routing/data_model_view.cu @@ -76,6 +76,13 @@ data_model_view_t::data_model_view_t(raft::handle_t* handle_ptr, "Number of nodes should be lower than 65535"); } +template +void data_model_view_t::add_distance_matrix(f_t const* matrix, uint8_t vehicle_type) +{ + cuopt_expects(matrix != nullptr, error_type_t::ValidationError, "Matrix input cannot be null"); + distance_matrices_[vehicle_type] = matrix; +} + template void data_model_view_t::add_cost_matrix(f_t const* matrix, uint8_t vehicle_type) { @@ -805,6 +812,20 @@ raft::handle_t const* data_model_view_t::get_handle_ptr() const noexce return handle_ptr_; } +template +f_t const* data_model_view_t::get_distance_matrix(uint8_t vehicle_type) const noexcept +{ + auto const matrix = distance_matrices_.find(vehicle_type); + return matrix == distance_matrices_.end() ? nullptr : matrix->second; +} + +template +std::unordered_map data_model_view_t::get_distance_matrices() + const noexcept +{ + return distance_matrices_; +} + template class CUOPT_EXPORT data_model_view_t; } // namespace routing } // namespace cuopt diff --git a/cpp/src/routing/fleet_info.cu b/cpp/src/routing/fleet_info.cu index 71997db103..325ccc327a 100644 --- a/cpp/src/routing/fleet_info.cu +++ b/cpp/src/routing/fleet_info.cu @@ -26,6 +26,7 @@ void populate_matrices(data_model_view_t const& data_model, d_mdarray_ // Check for consistency of cost matrices const auto& cost_matrices = data_model.get_cost_matrices(); + const auto& distance_matrices = data_model.get_distance_matrices(); const auto& transit_time_matrices = data_model.get_transit_time_matrices(); if (cost_matrices.empty()) { EXE_CUOPT_FAIL("Cost matrix (or matrices) must be specified!"); } @@ -50,6 +51,29 @@ void populate_matrices(data_model_view_t const& data_model, d_mdarray_ } } + if (!distance_matrices.empty()) { + for (auto const& [vtype, distance_matrix] : distance_matrices) { + cuopt_expects(cost_matrices.count(vtype) > 0, + error_type_t::ValidationError, + "Distance matrix vehicle types must have corresponding cost matrices"); + const size_t matrix_size = static_cast(nlocations) * nlocations; + const bool valid = + thrust::all_of(handle_ptr_->get_thrust_policy(), + distance_matrix, + distance_matrix + matrix_size, + [] __device__(f_t value) { return value == value && value >= f_t{0}; }); + cuopt_expects(valid, + error_type_t::ValidationError, + "Distance matrix values must be non-negative and not NaN"); + } + auto const types = get_unique_vehicle_types(data_model.get_vehicle_types(), stream_view_); + for (auto const& [vtype, mapped_type] : types) { + cuopt_expects(distance_matrices.count(vtype) > 0, + error_type_t::ValidationError, + "All vehicle distance matrices should be set"); + } + } + auto n_matrix_types = detail::get_cost_matrix_type_dim(data_model); matrices_ = detail::create_device_mdarray(nlocations, n_vehicle_types, n_matrix_types, stream_view_); diff --git a/cpp/src/routing/fleet_info.hpp b/cpp/src/routing/fleet_info.hpp index a40fefc04e..49eb8d57d5 100644 --- a/cpp/src/routing/fleet_info.hpp +++ b/cpp/src/routing/fleet_info.hpp @@ -52,7 +52,10 @@ class fleet_info_t { auto constexpr get_num_vehicles() const { return v_earliest_time_.size(); } - constexpr bool has_time_matrix() const { return matrices_.extent[1] > 1; } + constexpr bool has_time_matrix() const + { + return matrices_.time_matrix_index != matrices_.cost_matrix_index; + } constexpr bool is_homogenous() const { return is_homogenous_; } @@ -93,6 +96,9 @@ class fleet_info_t { h.buckets = host_copy(v_buckets_, stream); h.matrices = detail::create_host_mdarray( matrices_.extent[2], matrices_.extent[0], matrices_.extent[1]); + h.matrices.cost_matrix_index = matrices_.cost_matrix_index; + h.matrices.distance_matrix_index = matrices_.distance_matrix_index; + h.matrices.time_matrix_index = matrices_.time_matrix_index; raft::copy(h.matrices.buffer.data(), matrices_.buffer.data(), matrices_.buffer.size(), stream); return h; } @@ -178,7 +184,10 @@ class fleet_info_t { constexpr i_t is_homogenous_fleet() const { return is_homogenous; } - constexpr bool has_time_matrix() const { return matrices.extent[1] > 1; } + constexpr bool has_time_matrix() const + { + return matrices.time_matrix_index != matrices.cost_matrix_index; + } i_t num_vehicles = 0; mdarray_view_t matrices{}; const i_t* break_offset{nullptr}; diff --git a/cpp/src/routing/solver.cu b/cpp/src/routing/solver.cu index b160a2d140..d40fac91d9 100644 --- a/cpp/src/routing/solver.cu +++ b/cpp/src/routing/solver.cu @@ -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 */ @@ -43,8 +43,8 @@ solver_t::solver_t(data_model_view_t const& data_model, solver_settings_t const& settings) : handle_ptr_(data_model.get_handle_ptr()), settings_(settings) { - auto n_matrix_types = detail::get_cost_matrix_type_dim(data_model); - if (n_matrix_types == 1 && !data_model.get_vehicle_max_times().empty()) { + if (!detail::has_transit_time_matrix(data_model) && + !data_model.get_vehicle_max_times().empty()) { cuopt_expects(false, error_type_t::ValidationError, "Time matrix should be set in order to use vehicle max time constraints"); diff --git a/cpp/src/routing/utilities/md_utils.hpp b/cpp/src/routing/utilities/md_utils.hpp index 7e59dae362..f142c3e432 100644 --- a/cpp/src/routing/utilities/md_utils.hpp +++ b/cpp/src/routing/utilities/md_utils.hpp @@ -36,17 +36,31 @@ struct mdarray_view_t { constexpr auto get_cost_matrix(uint8_t vehicle_type) const { - return get_cost_matrix(vehicle_type, 0); + return get_cost_matrix(vehicle_type, cost_matrix_index); + } + + constexpr auto get_distance_matrix(uint8_t vehicle_type) const + { + return get_cost_matrix(vehicle_type, distance_matrix_index); } constexpr auto get_time_matrix(uint8_t vehicle_type) const { - return get_cost_matrix(vehicle_type, extent[1] - 1); + if (time_matrix_index != cost_matrix_index) { + return get_cost_matrix(vehicle_type, time_matrix_index); + } + if (extent[1] == 2 && distance_matrix_index == cost_matrix_index) { + return get_cost_matrix(vehicle_type, static_cast(extent[1] - 1)); + } + return get_cost_matrix(vehicle_type, cost_matrix_index); } f_t const* buffer_ptr{nullptr}; // dim4(n_vehicle_types, n_matrix_types, n_loc, n_loc) size_t extent[NCON_DIMS]; + uint8_t cost_matrix_index{0}; + uint8_t distance_matrix_index{0}; + uint8_t time_matrix_index{0}; }; template @@ -74,17 +88,28 @@ struct h_mdarray_t { return vehicle_type_matrices + (extent[3] * extent[2] * matrix_type); } - constexpr auto get_cost_matrix(uint8_t vehicle_type) { return get_cost_matrix(vehicle_type, 0); } + constexpr auto get_cost_matrix(uint8_t vehicle_type) + { + return get_cost_matrix(vehicle_type, cost_matrix_index); + } + + constexpr auto get_distance_matrix(uint8_t vehicle_type) + { + return get_cost_matrix(vehicle_type, distance_matrix_index); + } constexpr auto get_time_matrix(uint8_t vehicle_type) { - return get_cost_matrix(vehicle_type, extent[1] - 1); + return get_cost_matrix(vehicle_type, time_matrix_index); } auto view() const { mdarray_view_t view; - view.buffer_ptr = buffer.data(); + view.buffer_ptr = buffer.data(); + view.cost_matrix_index = cost_matrix_index; + view.distance_matrix_index = distance_matrix_index; + view.time_matrix_index = time_matrix_index; for (size_t i = 0; i < NCON_DIMS; ++i) { view.extent[i] = extent[i]; } @@ -92,6 +117,9 @@ struct h_mdarray_t { } size_t extent[NCON_DIMS]; std::vector buffer; + uint8_t cost_matrix_index{0}; + uint8_t distance_matrix_index{0}; + uint8_t time_matrix_index{0}; }; template @@ -120,17 +148,28 @@ struct d_mdarray_t { return vehicle_type_matrices + (extent[3] * extent[2] * matrix_type); } - constexpr auto get_cost_matrix(uint8_t vehicle_type) { return get_cost_matrix(vehicle_type, 0); } + constexpr auto get_cost_matrix(uint8_t vehicle_type) + { + return get_cost_matrix(vehicle_type, cost_matrix_index); + } + + constexpr auto get_distance_matrix(uint8_t vehicle_type) + { + return get_cost_matrix(vehicle_type, distance_matrix_index); + } constexpr auto get_time_matrix(uint8_t vehicle_type) { - return get_cost_matrix(vehicle_type, extent[1] - 1); + return get_cost_matrix(vehicle_type, time_matrix_index); } auto view() const { mdarray_view_t view; - view.buffer_ptr = buffer.data(); + view.buffer_ptr = buffer.data(); + view.cost_matrix_index = cost_matrix_index; + view.distance_matrix_index = distance_matrix_index; + view.time_matrix_index = time_matrix_index; for (size_t i = 0; i < NCON_DIMS; ++i) { view.extent[i] = extent[i]; } @@ -140,6 +179,9 @@ struct d_mdarray_t { size_t extent[NCON_DIMS]; rmm::device_uvector buffer; cuda::stream_ref stream; + uint8_t cost_matrix_index{0}; + uint8_t distance_matrix_index{0}; + uint8_t time_matrix_index{0}; }; namespace detail { @@ -147,8 +189,8 @@ namespace detail { template bool limit_matrix_entries(f_t* matrix, i_t width, raft::handle_t const* handle_ptr) { - i_t mat_size = width * width; - f_t max_value = 1.0e+30; + size_t mat_size = static_cast(width) * static_cast(width); + f_t max_value = 1.0e+30; bool exceeds_max = thrust::any_of(handle_ptr->get_thrust_policy(), @@ -172,14 +214,13 @@ void fill_data_model_matrices(data_model_view_t& data_model, d_mdarray { i_t n_vehicle_types = matrices.extent[0]; - i_t n_matrix_types = matrices.extent[1]; for (auto vehicle_type = 0; vehicle_type < n_vehicle_types; ++vehicle_type) { - for (auto matrix_type = 0; matrix_type < n_matrix_types; ++matrix_type) { - auto const matrix = matrices.get_cost_matrix(vehicle_type, matrix_type); - if (matrix_type == 0) - data_model.add_cost_matrix(matrix, vehicle_type); - else - data_model.add_transit_time_matrix(matrix, vehicle_type); + data_model.add_cost_matrix(matrices.get_cost_matrix(vehicle_type), vehicle_type); + if (matrices.distance_matrix_index != matrices.cost_matrix_index) { + data_model.add_distance_matrix(matrices.get_distance_matrix(vehicle_type), vehicle_type); + } + if (matrices.time_matrix_index != matrices.cost_matrix_index) { + data_model.add_transit_time_matrix(matrices.get_time_matrix(vehicle_type), vehicle_type); } } } @@ -189,6 +230,7 @@ auto create_host_mdarray(size_t nlocations, uint8_t n_vehicle_types, uint8_t n_m { std::vector full_matrix_extent{n_vehicle_types, n_matrix_types, nlocations, nlocations}; h_mdarray_t matrices{full_matrix_extent}; + matrices.time_matrix_index = n_matrix_types > 1 ? n_matrix_types - 1 : matrices.cost_matrix_index; return matrices; } @@ -200,6 +242,7 @@ auto create_device_mdarray(size_t nlocations, { std::vector full_matrix_extent{n_vehicle_types, n_matrix_types, nlocations, nlocations}; d_mdarray_t matrices{full_matrix_extent, stream}; + matrices.time_matrix_index = n_matrix_types > 1 ? n_matrix_types - 1 : matrices.cost_matrix_index; return matrices; } @@ -222,55 +265,84 @@ inline auto get_unique_vehicle_types(const raft::device_span& veh } template -auto get_cost_matrix_type_dim(data_model_view_t const& data_model) +bool has_distance_matrix(data_model_view_t const& data_model) +{ + return !data_model.get_distance_matrices().empty(); +} + +template +bool has_transit_time_matrix(data_model_view_t const& data_model) { - auto n_matrix_types = 1; auto vehicle_types_map = get_unique_vehicle_types(data_model.get_vehicle_types(), data_model.get_handle_ptr()->get_stream()); for (auto& [old_type, new_type] : vehicle_types_map) { - if (data_model.get_transit_time_matrix(old_type)) { - ++n_matrix_types; - break; - } + if (data_model.get_transit_time_matrix(old_type)) { return true; } } + return false; +} + +template +auto get_cost_matrix_type_dim(data_model_view_t const& data_model) +{ + auto n_matrix_types = 1; + if (has_distance_matrix(data_model)) { ++n_matrix_types; } + if (has_transit_time_matrix(data_model)) { ++n_matrix_types; } return n_matrix_types; } template -std::tuple get_vehicle_matrices( +std::tuple get_vehicle_matrices( data_model_view_t const& data_model, uint8_t vehicle_type) { auto cost_matrix = data_model.get_cost_matrix(vehicle_type); if (!cost_matrix) cuopt_expects( false, error_type_t::ValidationError, "Set vehicle types when using multiple matrices"); - auto time_matrix = data_model.get_transit_time_matrix(vehicle_type); + auto time_matrix = data_model.get_transit_time_matrix(vehicle_type); + auto distance_matrix = data_model.get_distance_matrix(vehicle_type); if (!time_matrix) time_matrix = cost_matrix; - return std::make_tuple(cost_matrix, time_matrix); + return std::make_tuple(cost_matrix, distance_matrix, time_matrix); } template void fill_mdarray_from_data_model(d_mdarray_t& matrices, data_model_view_t const& data_model) { - auto stream = data_model.get_handle_ptr()->get_stream(); - auto vehicle_types = data_model.get_vehicle_types(); - auto nlocations = data_model.get_num_locations(); - auto vehicle_types_map = get_unique_vehicle_types(vehicle_types, stream); + auto stream = data_model.get_handle_ptr()->get_stream(); + auto vehicle_types = data_model.get_vehicle_types(); + auto nlocations = data_model.get_num_locations(); + auto vehicle_types_map = get_unique_vehicle_types(vehicle_types, stream); + const bool has_distance = has_distance_matrix(data_model); + const bool has_time = has_transit_time_matrix(data_model); + const size_t matrix_size = static_cast(nlocations) * nlocations; + matrices.cost_matrix_index = 0; + uint8_t next_index = 1; + matrices.distance_matrix_index = has_distance ? next_index++ : matrices.cost_matrix_index; + matrices.time_matrix_index = has_time ? next_index++ : matrices.cost_matrix_index; for (auto& [old_type, new_type] : vehicle_types_map) { - auto [cost_matrix, time_matrix] = get_vehicle_matrices(data_model, old_type); - auto cost_matrix_span = matrices.get_cost_matrix(new_type); - auto time_matrix_span = matrices.get_time_matrix(new_type); - raft::copy(cost_matrix_span, cost_matrix, nlocations * nlocations, stream); + auto [cost_matrix, distance_matrix, time_matrix] = + get_vehicle_matrices(data_model, old_type); + auto cost_matrix_span = matrices.get_cost_matrix(new_type); + raft::copy(cost_matrix_span, cost_matrix, matrix_size, stream); if (limit_matrix_entries(cost_matrix_span, nlocations, data_model.get_handle_ptr())) { std::cout << "\nMax cost matrix value overriden to 1.0e+30"; } - raft::copy(time_matrix_span, time_matrix, nlocations * nlocations, stream); - if (limit_matrix_entries(time_matrix_span, nlocations, data_model.get_handle_ptr())) { - std::cout << "\nMax time matrix value overriden to 1.0e+30"; + if (has_distance) { + auto distance_matrix_span = matrices.get_distance_matrix(new_type); + raft::copy(distance_matrix_span, distance_matrix, matrix_size, stream); + if (limit_matrix_entries(distance_matrix_span, nlocations, data_model.get_handle_ptr())) { + std::cout << "\nMax distance matrix value overriden to 1.0e+30"; + } + } + if (has_time) { + auto time_matrix_span = matrices.get_time_matrix(new_type); + raft::copy(time_matrix_span, time_matrix, matrix_size, stream); + if (limit_matrix_entries(time_matrix_span, nlocations, data_model.get_handle_ptr())) { + std::cout << "\nMax time matrix value overriden to 1.0e+30"; + } } } } diff --git a/cpp/src/routing/vehicle_info.hpp b/cpp/src/routing/vehicle_info.hpp index d7b4e049a7..c4a5d231ab 100644 --- a/cpp/src/routing/vehicle_info.hpp +++ b/cpp/src/routing/vehicle_info.hpp @@ -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 */ @@ -18,7 +18,11 @@ namespace detail { template struct VehicleInfo { - constexpr bool has_time_matrix() const { return matrices.extent[1] > 1; } + constexpr bool has_time_matrix() const + { + if (matrices.time_matrix_index != matrices.cost_matrix_index) { return true; } + return matrices.extent[1] == 2 && matrices.distance_matrix_index == matrices.cost_matrix_index; + } bool operator==(VehicleInfo const& rhs) const { From 602c2b2b4c6731f3366ff0d62dc2b02a2ef4a409 Mon Sep 17 00:00:00 2001 From: Jose Maria Baca Date: Fri, 2 Oct 2026 09:32:45 +0200 Subject: [PATCH 2/4] test(routing): validate independent C++ distance matrices Signed-off-by: Jose Maria Baca --- cpp/tests/routing/CMakeLists.txt | 13 + .../routing/unit_tests/distance_matrices.cu | 237 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 cpp/tests/routing/unit_tests/distance_matrices.cu diff --git a/cpp/tests/routing/CMakeLists.txt b/cpp/tests/routing/CMakeLists.txt index 4beb6c3315..a713a3e29f 100644 --- a/cpp/tests/routing/CMakeLists.txt +++ b/cpp/tests/routing/CMakeLists.txt @@ -59,6 +59,19 @@ ConfigureTest(ROUTING_UNIT_TEST LABELS routing STATIC_LIB) +# Distance-matrix tests use the public routing API and inline matrix helpers. +add_executable(DISTANCE_MATRIX_TEST + ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/distance_matrices.cu +) +target_include_directories(DISTANCE_MATRIX_TEST PRIVATE "${CUOPT_SOURCE_DIR}/src") +target_link_libraries(DISTANCE_MATRIX_TEST + PRIVATE cuopt_routing cuopt_client GTest::gtest_main OpenMP::OpenMP_CUDA +) +add_test(NAME DISTANCE_MATRIX_TEST COMMAND DISTANCE_MATRIX_TEST) +set_tests_properties(DISTANCE_MATRIX_TEST PROPERTIES LABELS "routing") +install(TARGETS DISTANCE_MATRIX_TEST COMPONENT testing + DESTINATION bin/gtests/libcuopt EXCLUDE_FROM_ALL) + # ################################################################################################## # - gRPC VRP test driver --------------------------------------------------------------------------- if(CUOPT_ENABLE_GRPC) diff --git a/cpp/tests/routing/unit_tests/distance_matrices.cu b/cpp/tests/routing/unit_tests/distance_matrices.cu new file mode 100644 index 0000000000..70fea630dc --- /dev/null +++ b/cpp/tests/routing/unit_tests/distance_matrices.cu @@ -0,0 +1,237 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace cuopt::routing::test { + +TEST(distance_matrices, host_builders_preserve_legacy_time_slot) +{ + auto single = detail::create_host_mdarray(2, 1, 1); + EXPECT_EQ(single.get_time_matrix(0), single.get_cost_matrix(0)); + auto pair = detail::create_host_mdarray(2, 1, 2); + pair.buffer = {0.f, 3.f, 5.f, 0.f, 0.f, 11.f, 17.f, 0.f}; + EXPECT_FLOAT_EQ(pair.get_time_matrix(0)[1], 11.f); + EXPECT_FLOAT_EQ(pair.view().get_time_matrix(0)[1], 11.f); +} + +TEST(distance_matrices, two_slot_distance_layout_does_not_report_transit_time) +{ + auto matrices = detail::create_host_mdarray(2, 1, 2); + matrices.distance_matrix_index = 1; + matrices.time_matrix_index = 0; + matrices.buffer = {0.f, 3.f, 5.f, 0.f, 0.f, 100.f, 200.f, 0.f}; + detail::VehicleInfo vehicle; + vehicle.matrices = matrices.view(); + EXPECT_FALSE(vehicle.has_time_matrix()); + EXPECT_FLOAT_EQ(matrices.view().get_cost_matrix(0)[1], 3.f); + EXPECT_FLOAT_EQ(matrices.view().get_distance_matrix(0)[1], 100.f); + EXPECT_FLOAT_EQ(matrices.view().get_time_matrix(0)[1], 3.f); +} + +TEST(distance_matrices, cpu_problem_and_device_layout_keep_three_metrics_separate) +{ + cpu_routing_problem_t problem; + problem.num_locations = 2; + problem.fleet_size = 1; + problem.num_orders = 1; + problem.cost_matrices = {{0, {0.f, 3.f, 5.f, 0.f}}}; + problem.distance_matrices = {{0, {0.f, 100.f, 200.f, 0.f}}}; + problem.transit_time_matrices = {{0, {0.f, 11.f, 17.f, 0.f}}}; + problem.order_locations = {1}; + raft::handle_t handle; + auto [model, storage] = problem.to_device(&handle); + auto matrices = detail::create_device_mdarray(2, 1, 3, handle.get_stream()); + detail::fill_mdarray_from_data_model(matrices, model); + EXPECT_EQ(matrices.distance_matrix_index, 1); + EXPECT_EQ(matrices.time_matrix_index, 2); + auto copied = + cuopt::host_copy(matrices.buffer.data(), matrices.buffer.size(), handle.get_stream()); + EXPECT_EQ(copied, + (std::vector{0.f, 3.f, 5.f, 0.f, 0.f, 100.f, 200.f, 0.f, 0.f, 11.f, 17.f, 0.f})); + solver_settings_t settings; + settings.set_time_limit(0.1); + auto solution = solve(model, settings); + EXPECT_EQ(solution.get_status(), solution_status_t::SUCCESS); + EXPECT_DOUBLE_EQ(solution.get_total_objective(), 8.); + auto arrivals = cuopt::host_copy( + solution.get_arrival_stamp().data(), solution.get_arrival_stamp().size(), handle.get_stream()); + EXPECT_EQ(arrivals, (std::vector{0., 11., 28.})); +} + +TEST(distance_matrices, cpu_problem_rejects_invalid_distance_data) +{ + cpu_routing_problem_t problem; + problem.num_locations = 2; + problem.fleet_size = 1; + problem.num_orders = 1; + problem.cost_matrices = {{0, {0.f, 3.f, 5.f, 0.f}}}; + raft::handle_t handle; + for (auto const& values : + {std::vector{}, + std::vector{0.f, 1.f}, + std::vector{0.f, -1.f, 1.f, 0.f}, + std::vector{0.f, std::numeric_limits::quiet_NaN(), 1.f, 0.f}, + std::vector{0.f, -std::numeric_limits::infinity(), 1.f, 0.f}}) { + problem.distance_matrices = {{0, values}}; + EXPECT_THROW(problem.to_device(&handle), std::invalid_argument); + } +} + +TEST(distance_matrices, distance_matrix_does_not_satisfy_maximum_time_requirement) +{ + cpu_routing_problem_t problem; + problem.num_locations = 2; + problem.fleet_size = 1; + problem.num_orders = 1; + problem.cost_matrices = {{0, {0.f, 3.f, 5.f, 0.f}}}; + problem.distance_matrices = {{0, {0.f, 100.f, 200.f, 0.f}}}; + problem.vehicle_max_times = {50.f}; + problem.order_locations = {1}; + raft::handle_t handle; + auto [model, storage] = problem.to_device(&handle); + solver_settings_t settings; + settings.set_time_limit(0.1); + auto solution = solve(model, settings); + EXPECT_EQ(solution.get_status(), solution_status_t::ERROR); + EXPECT_NE(std::string(solution.get_error_status().what()).find("Time matrix should be set"), + std::string::npos); +} + +TEST(distance_matrices, cpu_problem_requires_matching_sizes_for_all_matrices) +{ + cpu_routing_problem_t problem; + problem.num_locations = 2; + problem.fleet_size = 1; + problem.num_orders = 1; + problem.distance_matrices = {{0, {0.f, 1.f, 1.f, 0.f}}}; + problem.cost_matrices = {{0, {0.f}}}; + raft::handle_t handle; + EXPECT_THROW(problem.to_device(&handle), std::invalid_argument); + problem.cost_matrices = {{0, {0.f, 3.f, 5.f, 0.f}}}; + problem.transit_time_matrices = {{0, {0.f}}}; + EXPECT_THROW(problem.to_device(&handle), std::invalid_argument); +} + +TEST(distance_matrices, registration_is_optional_and_null_input_is_rejected) +{ + raft::handle_t handle; + data_model_view_t model(&handle, 2, 1, 1); + EXPECT_EQ(model.get_distance_matrix(), nullptr); + EXPECT_TRUE(model.get_distance_matrices().empty()); + EXPECT_THROW(model.add_distance_matrix(nullptr), cuopt::logic_error); + auto distance = cuopt::device_copy(std::vector{0.f, 5.f, 7.f, 0.f}, handle.get_stream()); + model.add_distance_matrix(distance.data(), 3); + EXPECT_EQ(model.get_distance_matrix(3), distance.data()); + EXPECT_EQ(model.get_distance_matrices().size(), 1u); + EXPECT_EQ(model.get_distance_matrix(0), nullptr); +} + +TEST(distance_matrices, device_input_is_validated_before_solving) +{ + raft::handle_t handle; + auto stream = handle.get_stream(); + auto cost = cuopt::device_copy(std::vector{0.f, 3.f, 5.f, 0.f}, stream); + auto order = cuopt::device_copy(std::vector{1}, stream); + solver_settings_t settings; + settings.set_time_limit(0.1); + for (float invalid : + {-1.f, std::numeric_limits::quiet_NaN(), -std::numeric_limits::infinity()}) { + auto distance = cuopt::device_copy(std::vector{0.f, invalid, 1.f, 0.f}, stream); + data_model_view_t model(&handle, 2, 1, 1); + model.add_cost_matrix(cost.data()); + model.add_distance_matrix(distance.data()); + model.set_order_locations(order.data()); + auto solution = solve(model, settings); + EXPECT_EQ(solution.get_status(), solution_status_t::ERROR); + EXPECT_NE(std::string(solution.get_error_status().what()).find("non-negative and not NaN"), + std::string::npos); + } +} + +TEST(distance_matrices, distance_vehicle_types_require_corresponding_cost_matrices) +{ + cpu_routing_problem_t problem; + problem.num_locations = 2; + problem.fleet_size = 1; + problem.num_orders = 1; + problem.cost_matrices = {{0, {0.f, 3.f, 5.f, 0.f}}}; + problem.distance_matrices = {{1, {0.f, 100.f, 200.f, 0.f}}}; + problem.order_locations = {1}; + raft::handle_t handle; + auto [model, storage] = problem.to_device(&handle); + solver_settings_t settings; + auto solution = solve(model, settings); + EXPECT_EQ(solution.get_status(), solution_status_t::ERROR); +} + +TEST(distance_matrices, unreachable_entries_are_clamped_in_distance_buffer_only) +{ + cpu_routing_problem_t problem; + problem.num_locations = 2; + problem.fleet_size = 1; + problem.num_orders = 1; + problem.cost_matrices = {{0, {0.f, 3.f, 5.f, 0.f}}}; + problem.distance_matrices = {{0, {0.f, std::numeric_limits::infinity(), 1.e30f, 0.f}}}; + raft::handle_t handle; + auto [model, storage] = problem.to_device(&handle); + auto matrices = detail::create_device_mdarray(2, 1, 2, handle.get_stream()); + detail::fill_mdarray_from_data_model(matrices, model); + auto values = + cuopt::host_copy(matrices.buffer.data(), matrices.buffer.size(), handle.get_stream()); + EXPECT_EQ(values, (std::vector{0.f, 3.f, 5.f, 0.f, 0.f, 1.e30f, 1.e30f, 0.f})); +} + +TEST(distance_matrices, omitted_distance_matrix_preserves_cost_only_model) +{ + cpu_routing_problem_t problem; + problem.num_locations = 2; + problem.fleet_size = 1; + problem.num_orders = 1; + problem.cost_matrices = {{0, {0.f, 3.f, 5.f, 0.f}}}; + problem.order_locations = {1}; + raft::handle_t handle; + auto [model, storage] = problem.to_device(&handle); + EXPECT_TRUE(model.get_distance_matrices().empty()); + EXPECT_EQ((detail::get_cost_matrix_type_dim(model)), 1); + solver_settings_t settings; + settings.set_time_limit(0.1); + auto solution = solve(model, settings); + EXPECT_EQ(solution.get_status(), solution_status_t::SUCCESS); + EXPECT_DOUBLE_EQ(solution.get_total_objective(), 8.); +} + +TEST(distance_matrices, partial_distance_coverage_of_fleet_is_rejected) +{ + cpu_routing_problem_t problem; + problem.num_locations = 2; + problem.fleet_size = 2; + problem.num_orders = 1; + problem.cost_matrices = {{1, {0.f, 3.f, 5.f, 0.f}}, {3, {0.f, 7.f, 9.f, 0.f}}}; + problem.distance_matrices = {{1, {0.f, 100.f, 200.f, 0.f}}}; + problem.vehicle_types = {1, 3}; + problem.order_locations = {1}; + raft::handle_t handle; + auto [model, storage] = problem.to_device(&handle); + solver_settings_t settings; + auto solution = solve(model, settings); + EXPECT_EQ(solution.get_status(), solution_status_t::ERROR); + EXPECT_NE(std::string(solution.get_error_status().what()).find("All vehicle distance matrices"), + std::string::npos); +} + +} // namespace cuopt::routing::test From 8177d0663894f6727e3468cde71f22ecd29c0886 Mon Sep 17 00:00:00 2001 From: Jose Maria Baca Date: Fri, 2 Oct 2026 09:32:46 +0200 Subject: [PATCH 3/4] docs(routing): describe C++ distance matrix inputs Signed-off-by: Jose Maria Baca --- docs/cuopt/source/routing-features.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/cuopt/source/routing-features.rst b/docs/cuopt/source/routing-features.rst index 63a2697efa..9c853805a5 100644 --- a/docs/cuopt/source/routing-features.rst +++ b/docs/cuopt/source/routing-features.rst @@ -129,6 +129,23 @@ Fixed Cost per Vehicle ----------------------- Vehicles can have different fixed costs associated with them. This helps in scenarios where a single vehicle with a higher cost can be avoided if it can be done with two or more vehicles with lesser costs. This would be dependent on the objective function. +Independent Distance Matrices +---------------------------- +The C++ routing data model accepts optional physical-distance matrices through +``data_model_view_t::add_distance_matrix``. The CPU problem representation stores +them in ``cpu_routing_problem_t::distance_matrices`` and copies them to device +memory when constructing the data model. + +Distance matrices must have the same dimensions as the cost matrices and a +corresponding cost matrix for each vehicle type. When provided, distance matrices +must cover every vehicle type used by the fleet. Entries must be non-negative +and not NaN. Positive infinity and values at or above ``1e30`` encode unreachable +distance arcs and are limited to the solver's finite matrix sentinel. + +Registering a distance matrix does not change the ``COST`` objective, constrain +routes, or replace the transit-time matrix. The distance input is auxiliary data; +it does not introduce an objective to minimize physical distance. + Mapping Orders to Vehicles, and Vehicles to Orders --------------------------------------------------- By default, cuOpt will assign orders to vehicles based on the optimal routes. However, in some cases, it makes sense to assign specific orders to specific vehicles, or, conversely, specific vehicles to specific orders. From 2f3697dca2d9a615c6222d534c06917fc49854b3 Mon Sep 17 00:00:00 2001 From: Jose Maria Baca Date: Fri, 2 Oct 2026 11:02:18 +0200 Subject: [PATCH 4/4] docs(routing): fix distance matrix section underline Signed-off-by: Jose Maria Baca --- docs/cuopt/source/routing-features.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cuopt/source/routing-features.rst b/docs/cuopt/source/routing-features.rst index 9c853805a5..f114b6263f 100644 --- a/docs/cuopt/source/routing-features.rst +++ b/docs/cuopt/source/routing-features.rst @@ -130,7 +130,7 @@ Fixed Cost per Vehicle Vehicles can have different fixed costs associated with them. This helps in scenarios where a single vehicle with a higher cost can be avoided if it can be done with two or more vehicles with lesser costs. This would be dependent on the objective function. Independent Distance Matrices ----------------------------- +----------------------------- The C++ routing data model accepts optional physical-distance matrices through ``data_model_view_t::add_distance_matrix``. The CPU problem representation stores them in ``cpu_routing_problem_t::distance_matrices`` and copies them to device