From 2e465ae4dced32df2e7765a0c13f052573737975 Mon Sep 17 00:00:00 2001 From: Rajesh Gandham Date: Wed, 8 Apr 2026 16:11:39 -0700 Subject: [PATCH 1/3] Implement vehicle order cost --- cpp/include/cuopt/routing/data_model_view.hpp | 21 +++++ .../cuopt/routing/routing_structures.hpp | 1 + cpp/src/routing/arc_value.hpp | 6 +- cpp/src/routing/data_model_view.cu | 17 ++++ cpp/src/routing/dimensions.cuh | 3 +- cpp/src/routing/fleet_info.hpp | 4 +- cpp/src/routing/fleet_order_constraints.cu | 90 +++++++++++++++---- cpp/src/routing/fleet_order_constraints.hpp | 49 +++++----- .../lexicographic_search.cu | 3 +- cpp/src/routing/node/mismatch_node.cuh | 56 ++++++++---- cpp/src/routing/problem/problem.cu | 18 +++- cpp/src/routing/route/mismatch_route.cuh | 54 +++++++---- cpp/src/routing/vehicle_info.hpp | 6 +- .../routing/unit_tests/vehicle_order_match.cu | 61 +++++++++++++ 14 files changed, 307 insertions(+), 82 deletions(-) diff --git a/cpp/include/cuopt/routing/data_model_view.hpp b/cpp/include/cuopt/routing/data_model_view.hpp index b025469144..d931cd06e7 100644 --- a/cpp/include/cuopt/routing/data_model_view.hpp +++ b/cpp/include/cuopt/routing/data_model_view.hpp @@ -287,6 +287,26 @@ class data_model_view_t { const i_t nvehicles, bool validate_input = true); + /** + * @brief Set the cost of assigning a specific vehicle to each order. + * costs[order_id] = cost of vehicle vehicle_id serving order order_id. + * A cost of 0 means a free assignment; a finite cost > 0 will be minimized + * as the VEHICLE_ORDER_COST objective. If add_vehicle_order_match has already + * marked a (vehicle, order) pair as infeasible, specifying a finite cost for + * that pair is an error. + * + * @param vehicle_id vehicle id for which costs are specified + * @param costs device memory pointer to n_orders double values + * @param n_orders number of orders (must match the problem size) + */ + void set_vehicle_order_cost(const i_t vehicle_id, double const* costs, const i_t n_orders); + + /** + * @brief Get the vehicle order cost map + */ + const std::unordered_map>& get_vehicle_order_cost() + const noexcept; + /** * @brief In fully heterogenous fleet mode, vehicle can take different amount * of times to complete a task based on their profile and the order being @@ -647,6 +667,7 @@ class data_model_view_t { bool const* skip_first_trip_{nullptr}; std::unordered_map> vehicle_order_match_; std::unordered_map> order_vehicle_match_; + std::unordered_map> vehicle_order_cost_; std::unordered_map> order_service_times_; objective_t const* objective_{}; f_t const* objective_weights_{}; diff --git a/cpp/include/cuopt/routing/routing_structures.hpp b/cpp/include/cuopt/routing/routing_structures.hpp index 64bcc22ce5..a4c8001f0f 100644 --- a/cpp/include/cuopt/routing/routing_structures.hpp +++ b/cpp/include/cuopt/routing/routing_structures.hpp @@ -29,6 +29,7 @@ enum class objective_t { VARIANCE_ROUTE_SERVICE_TIME, // Variance in route service times PRIZE, // Sum of prizes of all orders that are served VEHICLE_FIXED_COST, // Used when fixed vehicle cost are enabled + VEHICLE_ORDER_COST, // Sum of vehicle-order assignment costs (mismatch dimension) SIZE // Helper enum to keep track of number of supported objective functions }; diff --git a/cpp/src/routing/arc_value.hpp b/cpp/src/routing/arc_value.hpp index 77f41805b9..a10f1f9ffd 100644 --- a/cpp/src/routing/arc_value.hpp +++ b/cpp/src/routing/arc_value.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 */ @@ -112,7 +112,9 @@ static constexpr double get_arc_of_dimension(const NodeInfo& l1, } else if constexpr (dim == dim_t::SERVICE_TIME) { return l1.is_depot() ? 0. : vehicle_info.order_service_times[l1.node()]; } else if constexpr (dim == dim_t::MISMATCH) { - return !l1.is_service_node() ? 0. : (double)(1 - vehicle_info.order_match[l1.node()]); + return (!l1.is_service_node() || vehicle_info.order_costs.empty()) + ? 0. + : vehicle_info.order_costs[l1.node()]; } else if constexpr (dim == dim_t::BREAK) { return l1.is_break(); } else { diff --git a/cpp/src/routing/data_model_view.cu b/cpp/src/routing/data_model_view.cu index 392c7a10fb..5dee2b33f7 100644 --- a/cpp/src/routing/data_model_view.cu +++ b/cpp/src/routing/data_model_view.cu @@ -338,6 +338,16 @@ void data_model_view_t::add_order_vehicle_match(const i_t order_id, order_vehicle_match_[order_id] = raft::device_span(vehicles, nvehicles); } +template +void data_model_view_t::set_vehicle_order_cost(const i_t vehicle_id, + double const* costs, + const i_t n_orders) +{ + cuopt_expects( + costs != nullptr, error_type_t::ValidationError, "vehicle_order_cost cannot be null"); + vehicle_order_cost_[vehicle_id] = raft::device_span(costs, n_orders); +} + template void data_model_view_t::set_order_service_times(i_t const* service_times, const i_t truck_id, @@ -667,6 +677,13 @@ data_model_view_t::get_order_vehicle_match() const noexcept return order_vehicle_match_; } +template +const std::unordered_map>& +data_model_view_t::get_vehicle_order_cost() const noexcept +{ + return vehicle_order_cost_; +} + template const std::unordered_map>& data_model_view_t::get_order_service_times() const noexcept diff --git a/cpp/src/routing/dimensions.cuh b/cpp/src/routing/dimensions.cuh index af50b2c56d..0ccc751b52 100644 --- a/cpp/src/routing/dimensions.cuh +++ b/cpp/src/routing/dimensions.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * 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 */ /* clang-format on */ @@ -215,6 +215,7 @@ struct service_time_dimension_info_t { struct mismatch_dimension_info_t { bool has_vehicle_order_match = false; + bool has_vehicle_order_cost = false; constexpr bool has_constraints() const { return has_vehicle_order_match; } }; diff --git a/cpp/src/routing/fleet_info.hpp b/cpp/src/routing/fleet_info.hpp index 1a37c66554..4d8c6adc52 100644 --- a/cpp/src/routing/fleet_info.hpp +++ b/cpp/src/routing/fleet_info.hpp @@ -128,7 +128,7 @@ class fleet_info_t { info.fixed_cost = fixed_costs[vehicle_id]; info.matrices = matrices.view(); info.order_service_times = fleet_order_constraints.get_order_service_times(vehicle_id); - info.order_match = fleet_order_constraints.get_order_match(vehicle_id); + info.order_costs = fleet_order_constraints.get_order_costs(vehicle_id); size_t stride = num_vehicles; i_t n_cap_dim = capacities.size() / num_vehicles; @@ -275,7 +275,7 @@ class fleet_info_t { info.fixed_cost = v_fixed_costs_.element(vehicle_id, handle_ptr_->get_stream()); info.matrices = matrices_.view(); info.order_service_times = fleet_order_constraints_.get_order_service_times(vehicle_id); - info.order_match = fleet_order_constraints_.get_order_match(vehicle_id); + info.order_costs = fleet_order_constraints_.get_order_costs(vehicle_id); size_t stride = num_vehicles; i_t n_cap_dim = v_capacities_.size() / num_vehicles; diff --git a/cpp/src/routing/fleet_order_constraints.cu b/cpp/src/routing/fleet_order_constraints.cu index 4778d18b3d..ca79a2888c 100644 --- a/cpp/src/routing/fleet_order_constraints.cu +++ b/cpp/src/routing/fleet_order_constraints.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * 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 */ /* clang-format on */ @@ -9,7 +9,10 @@ #include #include +#include #include +#include +#include #include #include @@ -17,8 +20,11 @@ namespace cuopt { namespace routing { namespace detail { +// Generate a flat [n_vehicles x n_orders] matrix of order costs. +// Infeasible pairs (excluded by vehicle_order_match/order_vehicle_match) get +inf. +// Compatible pairs default to 0.0. template -rmm::device_uvector generate_vehicle_order_match_matrix( +rmm::device_uvector generate_vehicle_order_match_matrix( data_model_view_t const& data_model, bool& is_homogenous) { auto handle_ptr_ = data_model.get_handle_ptr(); @@ -32,10 +38,10 @@ rmm::device_uvector generate_vehicle_order_match_matrix( const i_t order_begin = depot_included ? 1 : 0; if (!vehicle_order_match.empty() || !order_vehicle_match.empty()) { - std::vector vehicle_order_match_h(n_orders * fleet_size, true); + // Use 0.0 for compatible, +inf for infeasible. + std::vector order_costs_h(n_orders * fleet_size, 0.0); std::set> not_allowed_pairs; - // loop over specified vehicles and set the entries corresponding to specified - // order list to true and remaining orders to false + for (const auto& [vehicle_id, order_ids] : vehicle_order_match) { const auto order_ids_vec_h = cuopt::host_copy(order_ids, stream_view); const auto order_ids_h = @@ -43,7 +49,7 @@ rmm::device_uvector generate_vehicle_order_match_matrix( for (i_t order_id = order_begin; order_id < n_orders; ++order_id) { if (!order_ids_h.count(order_id)) { - vehicle_order_match_h[vehicle_id * n_orders + order_id] = false; + order_costs_h[vehicle_id * n_orders + order_id] = std::numeric_limits::infinity(); not_allowed_pairs.insert({order_id, vehicle_id}); } } @@ -55,7 +61,7 @@ rmm::device_uvector generate_vehicle_order_match_matrix( std::unordered_set(vehicle_ids_vec_h.begin(), vehicle_ids_vec_h.end()); for (i_t vehicle_id = 0; vehicle_id < fleet_size; ++vehicle_id) { if (!vehicle_ids_h.count(vehicle_id)) { - vehicle_order_match_h[order_id + vehicle_id * n_orders] = false; + order_costs_h[order_id + vehicle_id * n_orders] = std::numeric_limits::infinity(); } else { cuopt_expects( not_allowed_pairs.count({order_id, vehicle_id}) == 0u, @@ -65,12 +71,12 @@ rmm::device_uvector generate_vehicle_order_match_matrix( } } - if (is_homogenous && !vehicle_order_match_h.empty()) { + if (is_homogenous && !order_costs_h.empty()) { for (i_t vehicle_id = 1; vehicle_id < fleet_size; ++vehicle_id) { if (is_homogenous) { for (i_t order_id = order_begin; order_id < n_orders; ++order_id) { - if (vehicle_order_match_h[order_id + (vehicle_id - 1) * n_orders] != - vehicle_order_match_h[order_id + vehicle_id * n_orders]) { + if (order_costs_h[order_id + (vehicle_id - 1) * n_orders] != + order_costs_h[order_id + vehicle_id * n_orders]) { is_homogenous = false; break; } @@ -79,21 +85,21 @@ rmm::device_uvector generate_vehicle_order_match_matrix( } } - return cuopt::device_copy(vehicle_order_match_h, stream_view); + return cuopt::device_copy(order_costs_h, stream_view); } - return rmm::device_uvector(0, stream_view); + return rmm::device_uvector(0, stream_view); } template __global__ void modify_service_times(raft::device_span service_times, - raft::device_span order_vehicle_match) + raft::device_span order_costs) { - cuopt_assert(service_times.size() == order_vehicle_match.size(), - "service times and order vehicle match matrix should have same sizes"); + cuopt_assert(service_times.size() == order_costs.size(), + "service times and order costs matrix should have same sizes"); size_t idx = threadIdx.x + blockDim.x * blockIdx.x; for (; idx < service_times.size(); idx += blockDim.x * gridDim.x) { - if (!order_vehicle_match[idx]) { service_times[idx] = std::numeric_limits::max(); } + if (isinf(order_costs[idx])) { service_times[idx] = std::numeric_limits::max(); } } } @@ -102,14 +108,64 @@ void populate_vehicle_order_match(data_model_view_t const& data_model, detail::fleet_order_constraints_t& fleet_order_constraints_, bool& is_homogenous) { - fleet_order_constraints_.order_match = + fleet_order_constraints_.order_costs = generate_vehicle_order_match_matrix(data_model, is_homogenous); } +template +void populate_vehicle_order_cost(data_model_view_t const& data_model, + detail::fleet_order_constraints_t& fleet_order_constraints_) +{ + auto handle_ptr_ = data_model.get_handle_ptr(); + auto stream_view = handle_ptr_->get_stream(); + const i_t fleet_size = data_model.get_fleet_size(); + const i_t n_orders = data_model.get_num_orders(); + + const auto& vehicle_order_cost = data_model.get_vehicle_order_cost(); + if (vehicle_order_cost.empty()) { return; } + + // Ensure order_costs array is allocated (may not be if order_match wasn't set) + if (fleet_order_constraints_.order_costs.is_empty()) { + fleet_order_constraints_.order_costs = + rmm::device_uvector(n_orders * fleet_size, stream_view); + thrust::fill(handle_ptr_->get_thrust_policy(), + fleet_order_constraints_.order_costs.begin(), + fleet_order_constraints_.order_costs.end(), + 0.0); + } + + // Copy per-vehicle cost arrays from user input, with consistency checks + auto order_costs_h = cuopt::host_copy(fleet_order_constraints_.order_costs, stream_view); + handle_ptr_->sync_stream(); + + for (const auto& [vehicle_id, costs_span] : vehicle_order_cost) { + const auto costs_h = cuopt::host_copy(costs_span, stream_view); + handle_ptr_->sync_stream(); + cuopt_expects((i_t)costs_h.size() == n_orders, + error_type_t::ValidationError, + "vehicle_order_cost size must equal number of orders"); + for (i_t order_id = 0; order_id < n_orders; ++order_id) { + double existing = order_costs_h[vehicle_id * n_orders + order_id]; + double new_cost = costs_h[order_id]; + cuopt_expects(!(std::isinf(existing) && std::isfinite(new_cost)), + error_type_t::ValidationError, + "Inconsistency: vehicle_order_match marks pair as infeasible but " + "vehicle_order_cost specifies a finite cost for the same pair"); + if (!std::isinf(existing)) { order_costs_h[vehicle_id * n_orders + order_id] = new_cost; } + } + } + + fleet_order_constraints_.order_costs = cuopt::device_copy(order_costs_h, stream_view); +} + template void populate_vehicle_order_match( data_model_view_t const& data_model, detail::fleet_order_constraints_t& fleet_order_constraints_, bool& is_homogenous); + +template void populate_vehicle_order_cost( + data_model_view_t const& data_model, + detail::fleet_order_constraints_t& fleet_order_constraints_); } // namespace detail } // namespace routing } // namespace cuopt diff --git a/cpp/src/routing/fleet_order_constraints.hpp b/cpp/src/routing/fleet_order_constraints.hpp index c6be63a87c..90bdc651b8 100644 --- a/cpp/src/routing/fleet_order_constraints.hpp +++ b/cpp/src/routing/fleet_order_constraints.hpp @@ -9,6 +9,7 @@ #include #include +#include #include @@ -28,7 +29,7 @@ struct fleet_order_constraints_t { n_vehicles(n_vehicles_), n_orders(n_orders_), order_service_times(n_vehicles * n_orders, handle_ptr_->get_stream()), - order_match(0, handle_ptr_->get_stream()) + order_costs(0, handle_ptr_->get_stream()) { } @@ -53,16 +54,17 @@ struct fleet_order_constraints_t { n_orders); } - constexpr auto get_order_match(i_t truck_id) const + constexpr auto get_order_costs(i_t truck_id) const { - if (order_match.empty()) { return raft::span{}; } - cuopt_assert(order_match.size() == n_orders * n_vehicles, - "size mismatch of order_match vector"); - return raft::span(order_match.data() + truck_id * n_orders, n_orders); + if (order_costs.empty()) { return raft::span{}; } + cuopt_assert(order_costs.size() == (size_t)(n_orders * n_vehicles), + "size mismatch of order_costs vector"); + return raft::span(order_costs.data() + truck_id * n_orders, + n_orders); } std::vector order_service_times; - thrust::host_vector order_match; + std::vector order_costs; i_t n_orders; i_t n_vehicles; }; @@ -71,8 +73,7 @@ struct fleet_order_constraints_t { { host_t h; h.order_service_times = host_copy(order_service_times, stream); - auto tmp_order_match = host_copy(order_match, stream); - h.order_match = thrust::host_vector(tmp_order_match); + h.order_costs = host_copy(order_costs, stream); h.n_orders = n_orders; h.n_vehicles = n_vehicles; return h; @@ -85,18 +86,18 @@ struct fleet_order_constraints_t { n_orders); } - raft::device_span get_order_match(i_t truck_id) const + raft::device_span get_order_costs(i_t truck_id) const { - if (order_match.empty()) { return raft::device_span{}; } - cuopt_assert(order_match.size() == n_orders * n_vehicles, - "size mismatch of order_match vector"); - return raft::device_span(order_match.data() + truck_id * n_orders, n_orders); + if (order_costs.empty()) { return raft::device_span{}; } + cuopt_assert(order_costs.size() == (size_t)(n_orders * n_vehicles), + "size mismatch of order_costs vector"); + return raft::device_span(order_costs.data() + truck_id * n_orders, n_orders); } i_t n_vehicles{}; i_t n_orders{}; raft::device_span order_service_times{}; - raft::device_span order_match{}; + raft::device_span order_costs{}; }; constexpr raft::device_span get_order_service_times(i_t truck_id) const @@ -104,12 +105,12 @@ struct fleet_order_constraints_t { return raft::device_span(order_service_times.data() + truck_id * n_orders, n_orders); } - raft::device_span get_order_match(i_t truck_id) const + raft::device_span get_order_costs(i_t truck_id) const { - if (order_match.is_empty()) { return raft::device_span{}; } - cuopt_assert(order_match.size() == n_orders * n_vehicles, - "size mismatch of order_match vector"); - return raft::device_span(order_match.data() + truck_id * n_orders, n_orders); + if (order_costs.is_empty()) { return raft::device_span{}; } + cuopt_assert(order_costs.size() == (size_t)(n_orders * n_vehicles), + "size mismatch of order_costs vector"); + return raft::device_span(order_costs.data() + truck_id * n_orders, n_orders); } view_t view() const @@ -117,7 +118,7 @@ struct fleet_order_constraints_t { view_t v; v.order_service_times = raft::device_span(order_service_times.data(), order_service_times.size()); - v.order_match = raft::device_span(order_match.data(), order_match.size()); + v.order_costs = raft::device_span(order_costs.data(), order_costs.size()); v.n_vehicles = n_vehicles; v.n_orders = n_orders; return v; @@ -127,7 +128,7 @@ struct fleet_order_constraints_t { i_t n_vehicles{}; i_t n_orders{}; rmm::device_uvector order_service_times; - rmm::device_uvector order_match; + rmm::device_uvector order_costs; }; /** @@ -143,6 +144,10 @@ void populate_vehicle_order_match(data_model_view_t const& data_model, fleet_order_constraints_t& fleet_order_constraints, bool& is_homogenous); +template +void populate_vehicle_order_cost(data_model_view_t const& data_model, + fleet_order_constraints_t& fleet_order_constraints); + } // namespace detail } // namespace routing } // namespace cuopt diff --git a/cpp/src/routing/ges/lexicographic_search/lexicographic_search.cu b/cpp/src/routing/ges/lexicographic_search/lexicographic_search.cu index fa3a62d482..cb08eb6515 100644 --- a/cpp/src/routing/ges/lexicographic_search/lexicographic_search.cu +++ b/cpp/src/routing/ges/lexicographic_search/lexicographic_search.cu @@ -157,7 +157,8 @@ __global__ void lexicographic_search(typename solution_t::vie * This avoids storing unnecessary dim_between for mismatch dimension */ if (route.dimensions_info().has_dimension(dim_t::MISMATCH) && - !route.vehicle_info().order_match[pickup_node.id()]) { + !route.vehicle_info().order_costs.empty() && + isinf(route.vehicle_info().order_costs[pickup_node.id()])) { return; } diff --git a/cpp/src/routing/node/mismatch_node.cuh b/cpp/src/routing/node/mismatch_node.cuh index 6837794fe8..80c32b84eb 100644 --- a/cpp/src/routing/node/mismatch_node.cuh +++ b/cpp/src/routing/node/mismatch_node.cuh @@ -1,11 +1,12 @@ /* clang-format off */ /* - * 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 */ /* clang-format on */ #pragma once +#include #include namespace cuopt { @@ -15,21 +16,41 @@ namespace detail { template class mismatch_node_t { public: - //! Mismatch gathered to node + //! Infeasible vehicle-order assignment count accumulated forward i_t mismatch_forward = 0; - //! Mismatch gathered after node + //! Infeasible vehicle-order assignment count accumulated backward i_t mismatch_backward = 0; + //! Finite vehicle-order assignment costs accumulated forward + double cost_forward = 0.; + //! Finite vehicle-order assignment costs accumulated backward + double cost_backward = 0.; - /*! \brief { Calculate next node forward gathered distance data based on actual node} */ - void HDI calculate_forward(mismatch_node_t& next, f_t mismatch_between) const noexcept + /*! \brief { Calculate next node forward state based on arc from this->next. + If arc is infinite, increment mismatch; otherwise accumulate cost. } */ + template + void HDI calculate_forward(mismatch_node_t& next, double arc) const noexcept { - next.mismatch_forward = mismatch_forward + mismatch_between; + if (isinf(arc)) { + next.mismatch_forward = mismatch_forward + 1; + next.cost_forward = cost_forward; + } else { + next.mismatch_forward = mismatch_forward; + next.cost_forward = cost_forward + arc; + } } - /*! \brief { Calculate prev node gathered distance backward data based on actual node} */ - void HDI calculate_backward(mismatch_node_t& prev, f_t mismatch_between) const noexcept + /*! \brief { Calculate prev node backward state based on arc from prev->this. + If arc is infinite, increment mismatch; otherwise accumulate cost. } */ + template + void HDI calculate_backward(mismatch_node_t& prev, double arc) const noexcept { - prev.mismatch_backward = mismatch_backward + mismatch_between; + if (isinf(arc)) { + prev.mismatch_backward = mismatch_backward + 1; + prev.cost_backward = cost_backward; + } else { + prev.mismatch_backward = mismatch_backward; + prev.cost_backward = cost_backward + arc; + } } HDI double forward_excess([[maybe_unused]] const VehicleInfo& vehicle_info) const noexcept @@ -49,14 +70,16 @@ class mismatch_node_t { return mismatch_forward * weight <= excess_limit; } - /*! \brief { Combine information from begining and ending fragments.} - \return { Distance excess of route represented by nodes prev and next }*/ + /*! \brief { Combine infeasibility from prefix (prev) and suffix (next) with arc at the join. + Infeasible arc contributes 1 to mismatch; finite arcs are already in fwd/bwd costs. + } + \return { Infeasibility at the split point } */ static HDI double combine(const mismatch_node_t& prev, const mismatch_node_t& next, [[maybe_unused]] const VehicleInfo& vehicle_info, - double mismatch_between) noexcept + double arc) noexcept { - return prev.mismatch_forward + next.mismatch_backward + mismatch_between; + return prev.mismatch_forward + next.mismatch_backward + (isinf(arc) ? 1 : 0); } HDI bool backward_feasible([[maybe_unused]] const VehicleInfo& vehicle_info, @@ -69,11 +92,14 @@ class mismatch_node_t { template HDI void get_cost([[maybe_unused]] const mismatch_node_t& prev_node, [[maybe_unused]] const VehicleInfo& vehicle_info, - [[maybe_unused]] const mismatch_dimension_info_t& dim_info, - [[maybe_unused]] objective_cost_t& obj_cost, + const mismatch_dimension_info_t& dim_info, + objective_cost_t& obj_cost, infeasible_cost_t& inf_cost) const noexcept { inf_cost[dim_t::MISMATCH] = ((double)mismatch_forward + (double)mismatch_backward); + if (dim_info.has_vehicle_order_cost) { + obj_cost[objective_t::VEHICLE_ORDER_COST] = cost_forward + cost_backward; + } } }; diff --git a/cpp/src/routing/problem/problem.cu b/cpp/src/routing/problem/problem.cu index 4335b93734..29a03e4568 100644 --- a/cpp/src/routing/problem/problem.cu +++ b/cpp/src/routing/problem/problem.cu @@ -40,6 +40,9 @@ problem_t::problem_t(const data_model_view_t& data_model_vie populate_demand_container(data_model_view_, fleet_info, order_info); populate_vehicle_order_match( data_model_view_, fleet_info.fleet_order_constraints_, fleet_info.is_homogenous_); + populate_vehicle_order_cost(data_model_view_, fleet_info.fleet_order_constraints_); + // If per-vehicle order costs differ between vehicles, the fleet is heterogeneous + if (!data_model_view_.get_vehicle_order_cost().empty()) { fleet_info.is_homogenous_ = false; } populate_vehicle_infos(data_model_view_, fleet_info); // populate host vectors populate_host_arrays(); @@ -324,12 +327,21 @@ void problem_t::populate_dimensions_info() // mismatch dimension info const auto& vehicle_order_match = data_view_ptr->get_vehicle_order_match(); const auto& order_vehicle_match = data_view_ptr->get_order_vehicle_match(); + const auto& vehicle_order_cost = data_view_ptr->get_vehicle_order_cost(); const bool vehicle_order_match_exists = !vehicle_order_match.empty() || !order_vehicle_match.empty(); - if (vehicle_order_match_exists) { + const bool vehicle_order_cost_exists = !vehicle_order_cost.empty(); + if (vehicle_order_match_exists || vehicle_order_cost_exists) { dimensions_info.enable_dimension(dim_t::MISMATCH); - auto& mismatch_dim_info = dimensions_info.mismatch_dim; - mismatch_dim_info.has_vehicle_order_match = true; + auto& mismatch_dim_info = dimensions_info.mismatch_dim; + if (vehicle_order_match_exists) { mismatch_dim_info.has_vehicle_order_match = true; } + if (vehicle_order_cost_exists) { + mismatch_dim_info.has_vehicle_order_cost = true; + double cost_weight = specified_weights.count(objective_t::VEHICLE_ORDER_COST) + ? specified_weights.at(objective_t::VEHICLE_ORDER_COST) + : 1.0; + dimensions_info.enable_objective(objective_t::VEHICLE_ORDER_COST, cost_weight); + } } // break dimension info diff --git a/cpp/src/routing/route/mismatch_route.cuh b/cpp/src/routing/route/mismatch_route.cuh index 78975750e0..844865e90b 100644 --- a/cpp/src/routing/route/mismatch_route.cuh +++ b/cpp/src/routing/route/mismatch_route.cuh @@ -28,7 +28,9 @@ class mismatch_route_t { mismatch_dimension_info_t& dim_info_) : dim_info(dim_info_), mismatch_forward(0, sol_handle_->get_stream()), - mismatch_backward(0, sol_handle_->get_stream()) + mismatch_backward(0, sol_handle_->get_stream()), + cost_forward(0, sol_handle_->get_stream()), + cost_backward(0, sol_handle_->get_stream()) { } @@ -36,7 +38,9 @@ class mismatch_route_t { solution_handle_t const* sol_handle_) : dim_info(mismatch_route.dim_info), mismatch_forward(mismatch_route.mismatch_forward, sol_handle_->get_stream()), - mismatch_backward(mismatch_route.mismatch_backward, sol_handle_->get_stream()) + mismatch_backward(mismatch_route.mismatch_backward, sol_handle_->get_stream()), + cost_forward(mismatch_route.cost_forward, sol_handle_->get_stream()), + cost_backward(mismatch_route.cost_backward, sol_handle_->get_stream()) { } @@ -46,6 +50,8 @@ class mismatch_route_t { { mismatch_forward.resize(max_nodes_per_route, stream); mismatch_backward.resize(max_nodes_per_route, stream); + cost_forward.resize(max_nodes_per_route, stream); + cost_backward.resize(max_nodes_per_route, stream); } struct view_t { @@ -55,6 +61,8 @@ class mismatch_route_t { mismatch_node_t mismatch_node; mismatch_node.mismatch_forward = mismatch_forward[idx]; mismatch_node.mismatch_backward = mismatch_backward[idx]; + mismatch_node.cost_forward = cost_forward[idx]; + mismatch_node.cost_backward = cost_backward[idx]; return mismatch_node; } @@ -67,11 +75,13 @@ class mismatch_route_t { DI void set_forward_data(i_t idx, const mismatch_node_t& node) { mismatch_forward[idx] = node.mismatch_forward; + cost_forward[idx] = node.cost_forward; } DI void set_backward_data(i_t idx, const mismatch_node_t& node) { mismatch_backward[idx] = node.mismatch_backward; + cost_backward[idx] = node.cost_backward; } DI void copy_forward_data(const view_t& orig_route, i_t start_idx, i_t end_idx, i_t write_start) @@ -80,6 +90,8 @@ class mismatch_route_t { block_copy(mismatch_forward.subspan(write_start), orig_route.mismatch_forward.subspan(start_idx), size); + block_copy( + cost_forward.subspan(write_start), orig_route.cost_forward.subspan(start_idx), size); } DI void copy_backward_data(const view_t& orig_route, @@ -91,6 +103,8 @@ class mismatch_route_t { block_copy(mismatch_backward.subspan(write_start), orig_route.mismatch_backward.subspan(start_idx), size); + block_copy( + cost_backward.subspan(write_start), orig_route.cost_backward.subspan(start_idx), size); } DI void copy_fixed_route_data(const view_t& orig_route, @@ -98,7 +112,7 @@ class mismatch_route_t { i_t to_idx, i_t write_start) { - // there is no fixed route data associated with distance + // there is no fixed route data associated with mismatch } DI void compute_cost(const VehicleInfo& vehicle_info, @@ -107,24 +121,29 @@ class mismatch_route_t { infeasible_cost_t& inf_cost) const noexcept { inf_cost[dim_t::MISMATCH] = mismatch_forward[n_nodes_route]; + if (dim_info.has_vehicle_order_cost) { + obj_cost[objective_t::VEHICLE_ORDER_COST] = cost_forward[n_nodes_route]; + } } static DI thrust::tuple create_shared_route( - i_t* shmem, const mismatch_dimension_info_t dim_info, i_t n_nodes_route) + i_t* shmem, const mismatch_dimension_info_t dim_info_, i_t n_nodes_route) { view_t v; - v.dim_info = dim_info; - v.mismatch_forward = raft::device_span{(i_t*)shmem, (size_t)n_nodes_route + 1}; - v.mismatch_backward = raft::device_span{ - (i_t*)&v.mismatch_forward.data()[n_nodes_route + 1], (size_t)n_nodes_route + 1}; - - i_t* sh_ptr = (i_t*)&v.mismatch_backward.data()[n_nodes_route + 1]; + i_t* sh_ptr = shmem; + v.dim_info = dim_info_; + thrust::tie(v.mismatch_forward, sh_ptr) = wrap_ptr_as_span(sh_ptr, n_nodes_route + 1); + thrust::tie(v.mismatch_backward, sh_ptr) = wrap_ptr_as_span(sh_ptr, n_nodes_route + 1); + thrust::tie(v.cost_forward, sh_ptr) = wrap_ptr_as_span(sh_ptr, n_nodes_route + 1); + thrust::tie(v.cost_backward, sh_ptr) = wrap_ptr_as_span(sh_ptr, n_nodes_route + 1); return thrust::make_tuple(v, sh_ptr); } mismatch_dimension_info_t dim_info; raft::device_span mismatch_forward; raft::device_span mismatch_backward; + raft::device_span cost_forward; + raft::device_span cost_backward; }; view_t view() @@ -134,28 +153,31 @@ class mismatch_route_t { v.mismatch_forward = raft::device_span{mismatch_forward.data(), mismatch_forward.size()}; v.mismatch_backward = raft::device_span{mismatch_backward.data(), mismatch_backward.size()}; + v.cost_forward = raft::device_span{cost_forward.data(), cost_forward.size()}; + v.cost_backward = raft::device_span{cost_backward.data(), cost_backward.size()}; return v; } /** - * @brief Get the shared memory size required to store a distance route of a given size + * @brief Get the shared memory size required to store a mismatch route of a given size * * @param route_size * @return size_t */ HDI static size_t get_shared_size(i_t route_size, - [[maybe_unused]] mismatch_dimension_info_t dim_info) + [[maybe_unused]] mismatch_dimension_info_t dim_info_) { - // forward, backward - return 2 * route_size * sizeof(i_t); + // 2 i_t arrays (mismatch_forward, mismatch_backward) + 2 double arrays (cost_forward, + // cost_backward) + return 2 * (route_size + 1) * sizeof(i_t) + 2 * (route_size + 1) * sizeof(double); } mismatch_dimension_info_t dim_info; - // forward data rmm::device_uvector mismatch_forward; - // backward data rmm::device_uvector mismatch_backward; + rmm::device_uvector cost_forward; + rmm::device_uvector cost_backward; }; } // namespace detail diff --git a/cpp/src/routing/vehicle_info.hpp b/cpp/src/routing/vehicle_info.hpp index d7b4e049a7..ce66bf9cd3 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 */ @@ -24,7 +24,7 @@ struct VehicleInfo { { return drop_return_trip == rhs.drop_return_trip && skip_first_trip == rhs.skip_first_trip && type == rhs.type && order_service_times == rhs.order_service_times && - order_match == rhs.order_match && capacities == rhs.capacities && + order_costs == rhs.order_costs && capacities == rhs.capacities && break_durations == rhs.break_durations && break_earliest == rhs.break_earliest && break_latest == rhs.break_latest && earliest == rhs.earliest && latest == rhs.latest && start == rhs.start && end == rhs.end && max_cost == rhs.max_cost && @@ -51,7 +51,7 @@ struct VehicleInfo { uint8_t type{0}; mdarray_view_t matrices{}; raft::span order_service_times{}; - raft::span order_match{}; + raft::span order_costs{}; cuopt::strided_span capacities{}; raft::span break_durations{}; raft::span break_earliest{}; diff --git a/cpp/tests/routing/unit_tests/vehicle_order_match.cu b/cpp/tests/routing/unit_tests/vehicle_order_match.cu index 22691b3b86..0d8fa4059e 100644 --- a/cpp/tests/routing/unit_tests/vehicle_order_match.cu +++ b/cpp/tests/routing/unit_tests/vehicle_order_match.cu @@ -98,5 +98,66 @@ TEST(vehicle_order_match, one_order_per_vehicle) } } +/** + * @brief Test vehicle_order_cost: higher-cost assignments should be avoided. + * + * 2 vehicles, 3 orders (depot=0, orders=1,2,3). + * Vehicle 0 has cost 0 for order 1, 1000 for orders 2 and 3. + * Vehicle 1 has cost 0 for order 2, 1000 for orders 1 and 3. + * Vehicle 2 has cost 0 for order 3, 1000 for orders 1 and 2. + * Optimal: vehicle 0 takes order 1, vehicle 1 takes order 2, vehicle 2 takes order 3. + */ +TEST(vehicle_order_match, vehicle_order_cost_steers_assignment) +{ + i_t n_vehicles = 3; + i_t n_locations = 4; // depot=0, orders=1,2,3 + std::vector time_mat = {0., 1., 1., 1., 1., 0., 1., 1., 1., 1., 0., 1., 1., 1., 1., 0.}; + + raft::handle_t handle; + auto stream = handle.get_stream(); + + cuopt::routing::data_model_view_t data_model(&handle, n_locations, n_vehicles); + + auto time_mat_d = cuopt::device_copy(time_mat, stream); + data_model.add_cost_matrix(time_mat_d.data()); + + // n_orders = n_locations (depot included means n_orders = n_locations) + // n_orders as reported to the solver = n_locations (depot at index 0) + // costs are indexed [0..n_locations-1]: depot gets 0, orders get costs + const i_t n_orders = n_locations; + std::vector costs_v0 = {0., 0., 1000., 1000.}; // prefers order 1 + std::vector costs_v1 = {0., 1000., 0., 1000.}; // prefers order 2 + std::vector costs_v2 = {0., 1000., 1000., 0.}; // prefers order 3 + + auto costs_v0_d = cuopt::device_copy(costs_v0, stream); + auto costs_v1_d = cuopt::device_copy(costs_v1, stream); + auto costs_v2_d = cuopt::device_copy(costs_v2, stream); + + data_model.set_vehicle_order_cost(0, costs_v0_d.data(), n_orders); + data_model.set_vehicle_order_cost(1, costs_v1_d.data(), n_orders); + data_model.set_vehicle_order_cost(2, costs_v2_d.data(), n_orders); + + // Set minimum number of vehicles to 3 to ensure all vehicles are used + data_model.set_min_vehicles(n_vehicles); + + auto routing_solution = cuopt::routing::solve(data_model); + + EXPECT_EQ(routing_solution.get_status(), cuopt::routing::solution_status_t::SUCCESS); + + auto route_id = cuopt::host_copy(routing_solution.get_route(), stream); + auto truck_id = cuopt::host_copy(routing_solution.get_truck_id(), stream); + + // Build assignment map: order -> vehicle + std::unordered_map assignment; + for (size_t i = 0; i < route_id.size(); ++i) { + if (route_id[i] > 0) { assignment[route_id[i]] = truck_id[i]; } + } + + // Each order should be served by the vehicle with zero cost for it + EXPECT_EQ(assignment[1], 0); + EXPECT_EQ(assignment[2], 1); + EXPECT_EQ(assignment[3], 2); +} + } // namespace routing } // namespace cuopt From ebeced8a51254f28601fcf7b87ed7a61770e02b3 Mon Sep 17 00:00:00 2001 From: Rajesh Gandham Date: Mon, 13 Apr 2026 14:15:20 -0700 Subject: [PATCH 2/3] Missing objectives in python API --- .../cuopt/routing/structure/routing_utilities.pxd | 2 ++ .../cuopt/cuopt/routing/vehicle_routing_wrapper.pyx | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/python/cuopt/cuopt/routing/structure/routing_utilities.pxd b/python/cuopt/cuopt/routing/structure/routing_utilities.pxd index 1b833a23dc..47219f0a77 100644 --- a/python/cuopt/cuopt/routing/structure/routing_utilities.pxd +++ b/python/cuopt/cuopt/routing/structure/routing_utilities.pxd @@ -41,6 +41,8 @@ cdef extern from "cuopt/routing/routing_structures.hpp" namespace "cuopt::routin VARIANCE_ROUTE_SERVICE_TIME "cuopt::routing::objective_t::VARIANCE_ROUTE_SERVICE_TIME" # noqa PRIZE "cuopt::routing::objective_t::PRIZE" VEHICLE_FIXED_COST "cuopt::routing::objective_t::VEHICLE_FIXED_COST" + WEIGHTED_COMPLETION_TIME "cuopt::routing::objective_t::WEIGHTED_COMPLETION_TIME" + VEHICLE_ORDER_COST "cuopt::routing::objective_t::VEHICLE_ORDER_COST" cdef extern from "cuopt/routing/cython/generator.hpp" namespace "cuopt::routing::generator": # noqa diff --git a/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx b/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx index a290132d50..06a95d51c9 100644 --- a/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx +++ b/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx @@ -181,7 +181,15 @@ class Objective(IntEnum): PRIZE - Models with respect to prizes collected by the serviced orders - VEHICLE_FIXED_COST - Models cost per vehicle. Enabled when set_vehicle_fixed_costs is used. + VEHICLE_FIXED_COST - Models cost per vehicle. Enabled when set_vehicle_fixed_costs is used. + + WEIGHTED_COMPLETION_TIME - Sum of lot_weight * completion_time for each lot. + Used in lot scheduling problems. Enabled when + set_order_lot_weights is used. + + VEHICLE_ORDER_COST - Sum of vehicle-order assignment costs. Used to steer + tool-to-lot assignments in lot scheduling. Enabled + when set_vehicle_order_cost is used. """ COST = objective_t.COST @@ -190,6 +198,8 @@ class Objective(IntEnum): VARIANCE_ROUTE_SERVICE_TIME = objective_t.VARIANCE_ROUTE_SERVICE_TIME PRIZE = objective_t.PRIZE VEHICLE_FIXED_COST = objective_t.VEHICLE_FIXED_COST + WEIGHTED_COMPLETION_TIME = objective_t.WEIGHTED_COMPLETION_TIME + VEHICLE_ORDER_COST = objective_t.VEHICLE_ORDER_COST class NodeType(IntEnum): From 0af0ef3f9bc4f4bf7ede027349799f4a0de73019 Mon Sep 17 00:00:00 2001 From: Rajesh Gandham Date: Fri, 21 Aug 2026 13:11:15 -0700 Subject: [PATCH 3/3] Revert unintended commits --- .../cuopt/cuopt/routing/structure/routing_utilities.pxd | 3 +-- python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx | 8 +------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/python/cuopt/cuopt/routing/structure/routing_utilities.pxd b/python/cuopt/cuopt/routing/structure/routing_utilities.pxd index 47219f0a77..c720775f26 100644 --- a/python/cuopt/cuopt/routing/structure/routing_utilities.pxd +++ b/python/cuopt/cuopt/routing/structure/routing_utilities.pxd @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cython: profile=False @@ -41,7 +41,6 @@ cdef extern from "cuopt/routing/routing_structures.hpp" namespace "cuopt::routin VARIANCE_ROUTE_SERVICE_TIME "cuopt::routing::objective_t::VARIANCE_ROUTE_SERVICE_TIME" # noqa PRIZE "cuopt::routing::objective_t::PRIZE" VEHICLE_FIXED_COST "cuopt::routing::objective_t::VEHICLE_FIXED_COST" - WEIGHTED_COMPLETION_TIME "cuopt::routing::objective_t::WEIGHTED_COMPLETION_TIME" VEHICLE_ORDER_COST "cuopt::routing::objective_t::VEHICLE_ORDER_COST" diff --git a/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx b/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx index 06a95d51c9..782626d62d 100644 --- a/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx +++ b/python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 @@ -182,11 +182,6 @@ class Objective(IntEnum): PRIZE - Models with respect to prizes collected by the serviced orders VEHICLE_FIXED_COST - Models cost per vehicle. Enabled when set_vehicle_fixed_costs is used. - - WEIGHTED_COMPLETION_TIME - Sum of lot_weight * completion_time for each lot. - Used in lot scheduling problems. Enabled when - set_order_lot_weights is used. - VEHICLE_ORDER_COST - Sum of vehicle-order assignment costs. Used to steer tool-to-lot assignments in lot scheduling. Enabled when set_vehicle_order_cost is used. @@ -198,7 +193,6 @@ class Objective(IntEnum): VARIANCE_ROUTE_SERVICE_TIME = objective_t.VARIANCE_ROUTE_SERVICE_TIME PRIZE = objective_t.PRIZE VEHICLE_FIXED_COST = objective_t.VEHICLE_FIXED_COST - WEIGHTED_COMPLETION_TIME = objective_t.WEIGHTED_COMPLETION_TIME VEHICLE_ORDER_COST = objective_t.VEHICLE_ORDER_COST