Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ci/validate_wheel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ PYDISTCHECK_ARGS=(
if [[ "${package_dir}" == "python/libcuopt" ]]; then
if [[ "${RAPIDS_CUDA_MAJOR}" == "12" ]]; then
PYDISTCHECK_ARGS+=(
--max-allowed-size-compressed '695Mi'
--max-allowed-size-compressed '725Mi'
)
else
PYDISTCHECK_ARGS+=(
Expand Down
8 changes: 8 additions & 0 deletions cpp/include/cuopt/routing/cpu_routing_problem.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ struct cpu_vehicle_break_t {
std::vector<int32_t> locations;
};

struct cpu_vehicle_distance_break_t {
float distance_min = 0.f;
float distance_max = 0.f;
int32_t duration = 0;
std::vector<int32_t> locations;
};

struct cpu_uniform_break_t {
std::vector<int32_t> earliest;
std::vector<int32_t> latest;
Expand Down Expand Up @@ -96,6 +103,7 @@ class cpu_routing_problem_t {
std::vector<int32_t> break_locations;
std::vector<cpu_uniform_break_t> uniform_breaks;
std::map<int32_t, std::vector<cpu_vehicle_break_t>> vehicle_breaks;
std::map<int32_t, std::vector<cpu_vehicle_distance_break_t>> vehicle_distance_breaks;

std::map<int32_t, std::vector<int32_t>> vehicle_order_match;
std::map<int32_t, std::vector<int32_t>> order_vehicle_match;
Expand Down
37 changes: 35 additions & 2 deletions cpp/include/cuopt/routing/data_model_view.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,39 @@ class data_model_view_t {
i_t num_break_locations,
bool validate_input = true);

/**
* @brief Add a distance-windowed break for a vehicle.
*
* The solver inserts one break stop per call no later than distance_max,
* measured as cumulative distance along the cost matrix. distance_max is a
* hard upper bound. distance_min is a soft target whose shortfall contributes to
* objective_t::DISTANCE_BREAK_COST when that objective has a positive
* weight. The objective value is the maximum lower-bound shortfall on each
* route, summed across routes. Its default weight is 1.0 when distance
* breaks are configured; explicitly set its weight to 0.0 to disable it.
* Call this function multiple times for the same vehicle to model successive
* distance cycles (e.g. first stop: [0, 150], second stop: [150, 300]).
*
* @param vehicle_id Vehicle to apply the break to.
* @param distance_min Soft lower bound on cumulative route distance at
* the break.
* @param distance_max Latest cumulative route distance by which the
* vehicle must have stopped.
* @param break_duration Service time at the break location (same unit
* as other service times in the model).
* @param break_locations Device pointer to eligible break location IDs.
* Pass nullptr to allow any location.
* @param num_break_locations Number of entries in break_locations.
* @param validate_input Run input validation. Defaults to true.
*/
void add_vehicle_distance_break(i_t vehicle_id,
f_t distance_min,
f_t distance_max,
i_t break_duration,
i_t const* break_locations,
i_t num_break_locations,
bool validate_input = true);

/**
* @brief During improvement phase the solver only optimizes for the cost.
* This function is used to select the best solution accross all climbers
Expand Down Expand Up @@ -491,7 +524,7 @@ class data_model_view_t {
*/
std::vector<detail::break_dimension_t<i_t, f_t>> const& get_uniform_breaks() const noexcept;

std::map<i_t, std::vector<detail::vehicle_break_t<i_t>>> const& get_non_uniform_breaks()
std::map<i_t, std::vector<detail::vehicle_break_t<i_t, f_t>>> const& get_non_uniform_breaks()
const noexcept;

/**
Expand Down Expand Up @@ -662,7 +695,7 @@ class data_model_view_t {
raft::device_span<i_t const> initial_routes_{};
raft::device_span<node_type_t const> initial_types_{};
raft::device_span<i_t const> initial_sol_offsets_{};
std::map<i_t, std::vector<detail::vehicle_break_t<i_t>>> vehicle_breaks_{};
std::map<i_t, std::vector<detail::vehicle_break_t<i_t, f_t>>> vehicle_breaks_{};
};
} // namespace CUOPT_EXPORT routing
} // namespace cuopt
35 changes: 33 additions & 2 deletions cpp/include/cuopt/routing/routing_structures.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
DISTANCE_BREAK_COST, // Maximum distance-break lower-bound shortfall per route
SIZE // Helper enum to keep track of number of supported objective functions
};

Expand Down Expand Up @@ -58,18 +59,48 @@ class break_dimension_t {
i_t const* break_duration_;
};

template <typename i_t>
/**
* @brief A mandatory break a vehicle must take during its route, triggered either by time
* or by cumulative route distance. If @p locations is empty the break may be taken
* anywhere; otherwise it must occur at one of the specified location IDs.
*/
template <typename i_t, typename f_t>
class vehicle_break_t {
public:
/// Time-windowed break: must start within [earliest, latest].
vehicle_break_t(i_t earliest, i_t latest, i_t duration, raft::device_span<const i_t> locations)
: earliest_(earliest), latest_(latest), duration_(duration), locations_(locations)
: earliest_(earliest),
latest_(latest),
duration_(duration),
locations_(locations),
is_distance_based_(false),
distance_min_(0),
distance_max_(std::numeric_limits<f_t>::max())
{
}

/// Distance-windowed break: distance_min is soft and distance_max is hard.
vehicle_break_t(f_t distance_min,
f_t distance_max,
i_t duration,
raft::device_span<const i_t> locations)
: earliest_(0),
latest_(std::numeric_limits<i_t>::max()),
duration_(duration),
locations_(locations),
is_distance_based_(true),
distance_min_(distance_min),
distance_max_(distance_max)
{
}

i_t earliest_;
i_t latest_;
i_t duration_;
raft::device_span<const i_t> locations_{};
bool is_distance_based_;
f_t distance_min_;
f_t distance_max_;
};

template <typename i_t, typename f_t>
Expand Down
13 changes: 13 additions & 0 deletions cpp/src/grpc/routing/cuopt_routing.proto
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ message PerVehicleBreaks {
repeated VehicleBreak breaks = 2;
}

message VehicleDistanceBreak {
float distance_min = 1;
float distance_max = 2;
int32 duration = 3;
repeated int32 locations = 4;
}

message PerVehicleDistanceBreaks {
int32 vehicle_id = 1;
repeated VehicleDistanceBreak breaks = 2;
}

message UniformBreakDimension {
repeated int32 earliest = 1;
repeated int32 latest = 2;
Expand Down Expand Up @@ -105,6 +117,7 @@ message RoutingProblem {
repeated int32 break_locations = 60;
repeated UniformBreakDimension uniform_breaks = 61;
repeated PerVehicleBreaks vehicle_breaks = 62;
repeated PerVehicleDistanceBreaks vehicle_distance_breaks = 63;

// Matching
repeated MatchEntry vehicle_order_match = 70;
Expand Down
23 changes: 23 additions & 0 deletions cpp/src/grpc/routing/grpc_routing_problem_mapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ void map_proto_to_routing_problem(const cuopt::remote::RoutingProblem& pb,
}
p.vehicle_breaks[pvb.vehicle_id()] = std::move(breaks);
}
for (auto const& pvb : pb.vehicle_distance_breaks()) {
std::vector<cuopt::routing::cpu_vehicle_distance_break_t> breaks;
for (auto const& b : pvb.breaks()) {
cuopt::routing::cpu_vehicle_distance_break_t out;
out.distance_min = b.distance_min();
out.distance_max = b.distance_max();
out.duration = b.duration();
copy_repeated_to_vector(b.locations(), out.locations);
breaks.push_back(std::move(out));
}
p.vehicle_distance_breaks[pvb.vehicle_id()] = std::move(breaks);
}

for (auto const& m : pb.vehicle_order_match()) {
std::vector<int32_t> matches;
Expand Down Expand Up @@ -199,6 +211,17 @@ void map_routing_problem_to_proto(const cuopt::routing::cpu_routing_problem_t& p
copy_vector_to_repeated(b.locations, brk->mutable_locations());
}
}
for (auto const& [vehicle_id, breaks] : p.vehicle_distance_breaks) {
auto* out = pb->add_vehicle_distance_breaks();
out->set_vehicle_id(vehicle_id);
for (auto const& b : breaks) {
auto* brk = out->add_breaks();
brk->set_distance_min(b.distance_min);
brk->set_distance_max(b.distance_max);
brk->set_duration(b.duration);
copy_vector_to_repeated(b.locations, brk->mutable_locations());
}
}

for (auto const& [id, matches] : p.vehicle_order_match) {
auto* out = pb->add_vehicle_order_match();
Expand Down
11 changes: 11 additions & 0 deletions cpp/src/routing/cpu_routing_problem.cu
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,17 @@ cpu_routing_problem_t::to_device(raft::handle_t* handle) const
}
}

for (auto const& [vehicle_id, breaks] : vehicle_distance_breaks) {
for (auto const& brk : breaks) {
auto d_locs = copy_vector(brk.locations, stream);
int32_t n_locs = d_locs ? static_cast<int32_t>(d_locs->size()) : 0;
int32_t const* loc_ptr = d_locs ? d_locs->data() : nullptr;
view.add_vehicle_distance_break(
vehicle_id, brk.distance_min, brk.distance_max, brk.duration, loc_ptr, n_locs, false);
if (d_locs) { data->vehicle_break_locations.push_back(std::move(d_locs)); }
}
}

for (auto const& [vehicle_id, orders] : vehicle_order_match) {
auto d = copy_vector(orders, stream);
if (!d) { continue; }
Expand Down
97 changes: 79 additions & 18 deletions cpp/src/routing/data_model_view.cu
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,42 @@
#include <routing/utilities/check_input.hpp>
#include <unordered_set>

#include <thrust/sort.h>
#include <thrust/unique.h>

namespace {

/**
* @brief Validates that break locations are within the valid range
* of the location matrix and that all entries are unique.
*/
template <typename i_t>
void validate_break_locations(i_t const* locations,
i_t n,
i_t num_locations,
raft::handle_t const* handle)
{
cuopt::cuopt_expects(
n >= 0, cuopt::error_type_t::ValidationError, "Number of break locations must be non-negative");
if (n == 0) { return; }
cuopt::cuopt_expects(locations != nullptr,
cuopt::error_type_t::ValidationError,
"Break locations cannot be null when num_break_locations > 0");
cuopt::cuopt_expects(cuopt::routing::detail::check_min_max_values(
locations, n, i_t{0}, num_locations - 1, handle->get_stream()),
cuopt::error_type_t::ValidationError,
"Break locations should be in [0, num_locations) range");
rmm::device_uvector<i_t> tmp(n, handle->get_stream());
raft::copy(tmp.begin(), locations, n, handle->get_stream());
thrust::sort(handle->get_thrust_policy(), tmp.begin(), tmp.end());
auto end = thrust::unique(handle->get_thrust_policy(), tmp.begin(), tmp.end());
i_t unique_items = end - tmp.begin();
cuopt::cuopt_expects(n == unique_items,
cuopt::error_type_t::ValidationError,
"There should be unique break locations");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} // namespace

namespace cuopt {
namespace routing {

Expand Down Expand Up @@ -86,7 +120,7 @@ void data_model_view_t<i_t, f_t>::set_break_locations(i_t const* break_locations
detail::check_min_max_values(
break_locations, n_break_locations, 0, num_locations_ - 1, handle_ptr_->get_stream()),
error_type_t::ValidationError,
"Break locations should be at the end of the matrix");
"Break locations must be within [0, num_locations)");
rmm::device_uvector<i_t> tmp_break_nodes(n_break_locations, handle_ptr_->get_stream());
raft::copy(
tmp_break_nodes.begin(), break_locations, n_break_locations, handle_ptr_->get_stream());
Expand Down Expand Up @@ -156,28 +190,55 @@ void data_model_view_t<i_t, f_t>::add_vehicle_break(i_t vehicle_id,
i_t num_break_locations,
bool validate_input)
{
vehicle_breaks_[vehicle_id].push_back(detail::vehicle_break_t<i_t>(
cuopt_expects(0 <= vehicle_id && vehicle_id < fleet_size_,
error_type_t::ValidationError,
"vehicle_id must be in [0, fleet_size)");
cuopt_expects(break_earliest <= break_latest,
error_type_t::ValidationError,
"Break earliest must be less than or equal than break latest!");
cuopt_expects(
break_duration >= 0, error_type_t::ValidationError, "break_duration must be non-negative!");

if (validate_input) {
validate_break_locations(break_locations, num_break_locations, num_locations_, handle_ptr_);
}

vehicle_breaks_[vehicle_id].push_back(detail::vehicle_break_t<i_t, f_t>(
break_earliest,
break_latest,
break_duration,
raft::device_span<const i_t>(break_locations, num_break_locations)));
}

if (validate_input && num_break_locations > 0) {
cuopt_expects(
detail::check_min_max_values(
break_locations, num_break_locations, 0, num_locations_ - 1, handle_ptr_->get_stream()),
error_type_t::ValidationError,
"Break locations should be at the end of the matrix");
rmm::device_uvector<i_t> tmp_break_nodes(num_break_locations, handle_ptr_->get_stream());
raft::copy(
tmp_break_nodes.begin(), break_locations, num_break_locations, handle_ptr_->get_stream());
auto end = thrust::unique(
handle_ptr_->get_thrust_policy(), tmp_break_nodes.begin(), tmp_break_nodes.end());
i_t unique_items = end - tmp_break_nodes.begin();
cuopt_expects(num_break_locations == unique_items,
error_type_t::ValidationError,
"There should be unique break locations");
template <typename i_t, typename f_t>
void data_model_view_t<i_t, f_t>::add_vehicle_distance_break(i_t vehicle_id,
f_t distance_min,
f_t distance_max,
i_t break_duration,
i_t const* break_locations,
i_t num_break_locations,
bool validate_input)
{
cuopt_expects(0 <= vehicle_id && vehicle_id < fleet_size_,
error_type_t::ValidationError,
"vehicle_id must be in [0, fleet_size)");
cuopt_expects(
distance_min >= 0, error_type_t::ValidationError, "distance_min must be non-negative!");
cuopt_expects(distance_max > distance_min,
error_type_t::ValidationError,
"distance break distance_max must be greater than distance_min!");
cuopt_expects(
break_duration >= 0, error_type_t::ValidationError, "break_duration must be non-negative!");

if (validate_input) {
validate_break_locations(break_locations, num_break_locations, num_locations_, handle_ptr_);
}

vehicle_breaks_[vehicle_id].push_back(detail::vehicle_break_t<i_t, f_t>(
distance_min,
distance_max,
break_duration,
raft::device_span<const i_t>(break_locations, num_break_locations)));
}

template <typename i_t, typename f_t>
Expand Down Expand Up @@ -615,7 +676,7 @@ data_model_view_t<i_t, f_t>::get_uniform_breaks() const noexcept
}

template <typename i_t, typename f_t>
std::map<i_t, std::vector<detail::vehicle_break_t<i_t>>> const&
std::map<i_t, std::vector<detail::vehicle_break_t<i_t, f_t>>> const&
data_model_view_t<i_t, f_t>::get_non_uniform_breaks() const noexcept
{
return vehicle_breaks_;
Expand Down
8 changes: 5 additions & 3 deletions cpp/src/routing/dimensions.cuh
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -182,8 +182,10 @@ using infeasible_cost_t = static_vec_t<dim_t>;
using objective_cost_t = static_vec_t<objective_t>;

struct cost_dimension_info_t {
bool has_max_constraint = false;
HDI bool has_constraints() const { return has_max_constraint; }
bool has_max_constraint = false;
bool has_distance_window = false;
bool has_distance_break_cost = false;
HDI bool has_constraints() const { return has_max_constraint || has_distance_window; }
};

struct time_dimension_info_t {
Expand Down
Loading
Loading