Skip to content
Draft
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
3 changes: 2 additions & 1 deletion cpp/src/routing/adapters/adapted_generator.cu
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */
Expand Down Expand Up @@ -125,6 +125,7 @@ void adapted_generator_t<i_t, f_t, REQUEST>::generate_solution(
}

resource.ges.repair_empty_routes();
if (dim_info.has_dimension(dim_t::BREAK)) { resource.ges.try_squeeze_breaks_feasible(); }

sol.populate_host_data(true);
cuopt_func_call(sol.check_device_host_coherence());
Expand Down
3 changes: 2 additions & 1 deletion cpp/src/routing/ges/squeeze.cu
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */
Expand Down Expand Up @@ -410,6 +410,7 @@ bool guided_ejection_search_t<i_t, f_t, REQUEST>::try_squeeze_breaks_feasible()

local_search_ptr_->set_active_weights(local_search_ptr_->move_candidates.weights,
original_incl_objective);
squeeze_breaks();
return solution_ptr->is_feasible();
}

Expand Down
11 changes: 10 additions & 1 deletion cpp/src/routing/ges/squeeze.cuh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */
Expand Down Expand Up @@ -439,6 +439,15 @@ __global__ void squeeze_breaks_kernel(typename solution_t<i_t, f_t, REQUEST>::vi

for (int break_dim_idx = 0; break_dim_idx < n_break_dims; ++break_dim_idx) {
if (break_dim_counters[break_dim_idx] == 1) { continue; }
if (sh_route.get_num_service_nodes() > 0 &&
sh_route.dimensions_info().has_dimension(dim_t::TIME)) {
const auto vehicle_info = sh_route.vehicle_info();
const auto route_end_node = sh_route.get_node(sh_route.get_num_nodes()).time_dim;
const double route_end_time =
route_end_node.departure_forward + route_end_node.excess_forward;
// Breaks become mandatory only once the route reaches their deadline.
if (route_end_time < vehicle_info.break_latest[break_dim_idx]) { continue; }
}
const auto old_objective_cost = sh_route.get_objective_cost();
const auto old_infeasbility_cost = sh_route.get_infeasibility_cost();
auto break_nodes =
Expand Down
6 changes: 4 additions & 2 deletions cpp/src/routing/local_search/breaks_insertion.cu
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 @@ -33,7 +33,9 @@ __global__ void find_break_insertions_kernel(
i_t route_id = blockIdx.x / n_max_break_dims;
i_t ejected_break_dim = blockIdx.x % n_max_break_dims;
auto global_route = solution.routes[route_id];
if (ejected_break_dim >= global_route.get_num_breaks()) { return; }
if (ejected_break_dim >= solution.problem.get_break_dimensions(global_route.get_vehicle_id())) {
return;
}

for (int i = 0; i < global_route.get_num_nodes(); ++i) {
auto node = global_route.get_node(i);
Expand Down
23 changes: 23 additions & 0 deletions cpp/src/routing/route/route.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,29 @@ class route_t {
.compute_cost(this->vehicle_info(), *n_nodes, objective_cost[0], infeasibility_cost[0]);
});

if (dimensions_info().has_dimension(dim_t::BREAK) &&
dimensions_info().has_dimension(dim_t::TIME)) {
const auto vehicle_info = this->vehicle_info();
const i_t n_breaks = vehicle_info.num_breaks();
if (n_breaks > 0) {
const auto route_end_node = this->get_node(*n_nodes).time_dim;
const double route_end_time =
route_end_node.departure_forward + route_end_node.excess_forward;
i_t missing_required_breaks = 0;
for (i_t break_dim = 0; break_dim < n_breaks; ++break_dim) {
if (vehicle_info.break_latest[break_dim] > route_end_time) { continue; }

bool has_break = false;
for (i_t node_idx = 0; node_idx < *n_nodes; ++node_idx) {
const auto node_info = this->get_node(node_idx).node_info();
has_break = has_break || (node_info.is_break() && node_info.break_dim() == break_dim);
}
missing_required_breaks += !has_break;
}
infeasibility_cost[0][dim_t::BREAK] += missing_required_breaks;
}
}

return thrust::make_tuple(objective_cost[0], infeasibility_cost[0]);
}

Expand Down
85 changes: 81 additions & 4 deletions python/cuopt/cuopt/tests/routing/test_vehicle_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ def test_heterogenous_breaks():
routing_solution = routing.Solve(d, s)

# TO DO: Check if breaks are adhered to
assert routing_solution.get_status() == 0
assert routing_solution.get_status() == 0, routing_solution.get_message()
counters = {}
routes = routing_solution.get_route().to_pandas()
break_locations_1_list = break_locations_1.to_arrow().to_pylist()
Expand All @@ -521,11 +521,23 @@ def test_heterogenous_breaks():
counters[truck_id] = counters[truck_id] + 1

# Make sure the achieved number of breaks is same as the specified
for truck_id, num_breaks in counters.items():
arrival_col = next(
col
for col in ("arrival_stamp", "arrival_time", "arrival")
if col in routes.columns
)
for truck_id in routes.truck_id.unique():
truck_id = int(truck_id)
route_end = routes[routes.truck_id == truck_id][arrival_col].max()
if truck_id < num_v_type_1:
assert num_breaks == num_breaks_1
expected_breaks = sum(
break_latest <= route_end for _, break_latest in break_times_1
)
else:
assert num_breaks == num_breaks_2
expected_breaks = sum(
break_latest <= route_end for _, break_latest in break_times_2
)
assert counters.get(truck_id, 0) == expected_breaks


# ----- Vehicle dependent service times -----
Expand Down Expand Up @@ -733,3 +745,68 @@ def test_empty_routes_with_breaks():
h_route = solution_vehicle_x["route"].to_arrow().to_pylist()
route_len = len(h_route)
assert route_len > 3


def test_break_after_route_end_is_not_inserted():
coords = np.array(
[[0.0, 0.0], [10.0, 0.0], [0.0, 200.0]], dtype=np.float32
)
diff = coords[:, None] - coords[None, :]
matrix = cudf.DataFrame(np.linalg.norm(diff, axis=-1).astype(np.float32))

dm = routing.DataModel(3, n_fleet=1, n_orders=1)
dm.add_cost_matrix(matrix)
dm.add_transit_time_matrix(matrix)
dm.set_order_locations(cudf.Series([1], dtype=np.int32))
dm.set_order_time_windows(
cudf.Series([0], dtype=np.int32),
cudf.Series([1000], dtype=np.int32),
)
dm.set_vehicle_time_windows(
cudf.Series([0], dtype=np.int32),
cudf.Series([1000], dtype=np.int32),
)
dm.set_break_locations(cudf.Series([2], dtype=np.int32))
dm.add_break_dimension(
cudf.Series([150], dtype=np.int32),
cudf.Series([200], dtype=np.int32),
cudf.Series([5], dtype=np.int32),
)

sol = routing.Solve(dm)
assert sol.get_status() == 0
assert abs(sol.get_total_objective() - 20) < 0.01

route = sol.get_route()
assert "Break" not in route["type"].to_arrow().to_pylist()
assert route["location"].to_arrow().to_pylist() == [0, 1, 0]


def test_required_break_unreachable_is_infeasible():
coords = np.array(
[[0.0, 0.0], [10.0, 0.0], [0.0, 200.0]], dtype=np.float32
)
diff = coords[:, None] - coords[None, :]
matrix = cudf.DataFrame(np.linalg.norm(diff, axis=-1).astype(np.float32))

dm = routing.DataModel(3, n_fleet=1, n_orders=1)
dm.add_cost_matrix(matrix)
dm.add_transit_time_matrix(matrix)
dm.set_order_locations(cudf.Series([1], dtype=np.int32))
dm.set_order_time_windows(
cudf.Series([0], dtype=np.int32),
cudf.Series([1000], dtype=np.int32),
)
dm.set_vehicle_time_windows(
cudf.Series([0], dtype=np.int32),
cudf.Series([1000], dtype=np.int32),
)
dm.set_break_locations(cudf.Series([2], dtype=np.int32))
dm.add_break_dimension(
cudf.Series([0], dtype=np.int32),
cudf.Series([5], dtype=np.int32),
cudf.Series([5], dtype=np.int32),
)

sol = routing.Solve(dm)
assert sol.get_status() == 1