From 2a4500a4fe2252bebbaacccc115d8d924b47cce9 Mon Sep 17 00:00:00 2001 From: Nikolai Poperechnyi Date: Mon, 11 May 2026 21:34:51 +0300 Subject: [PATCH] fix(routing): penalize missing required breaks in route cost Signed-off-by: Nikolai Poperechnyi --- cpp/src/routing/route/route.cuh | 36 ++++++++++++++++++- .../tests/routing/test_vehicle_properties.py | 31 ++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/cpp/src/routing/route/route.cuh b/cpp/src/routing/route/route.cuh index b624acb903..919198f533 100644 --- a/cpp/src/routing/route/route.cuh +++ b/cpp/src/routing/route/route.cuh @@ -646,7 +646,24 @@ class route_t { orig_route, intra_ejection_indices[n_ejections - 1] + 1, route_length + 1, curr_size); } - // extend for other things later + /** + * @brief Recomputes the objective and infeasibility cost of the current route. + * + * Resets the route cost accumulators, invokes cost computation for each active + * dimension, and returns the resulting objective and infeasibility costs. + * + * When both BREAK and TIME dimensions are present, this method also adds an + * explicit infeasibility penalty for missing required breaks. A break is treated + * as required if its latest time is no later than the vehicle arrival time at + * the depot. Any deficit between required breaks and breaks present in the + * route is added to the BREAK infeasibility component. + * + * @param check_single_threaded If true, assert that exactly one warp thread is active. + * + * @return Tuple of `{objective_cost, infeasibility_cost}` for the route. + * + * @note Mutates and returns `objective_cost[0]` and `infeasibility_cost[0]`. + */ DI thrust::tuple compute_cost( bool check_single_threaded = true) { @@ -663,6 +680,23 @@ class route_t { .compute_cost(this->vehicle_info(), *n_nodes, objective_cost[0], infeasibility_cost[0]); }); + // Penalize missing required breaks; the per-dim formula only catches excess. + if (dimensions_info().has_dimension(dim_t::BREAK) && + dimensions_info().has_dimension(dim_t::TIME)) { + auto vinfo = this->vehicle_info(); + const i_t n_breaks = vinfo.num_breaks(); + if (n_breaks > 0) { + const double arrival_at_depot = dimensions.time_dim.departure_forward[*n_nodes] + + dimensions.time_dim.excess_forward[*n_nodes]; + i_t required = 0; + for (i_t i = 0; i < n_breaks; ++i) { + required += (vinfo.break_latest[i] <= arrival_at_depot); + } + const i_t breaks_present = dimensions.break_dim.breaks_forward[*n_nodes]; + infeasibility_cost[0][dim_t::BREAK] += max(0.0, required - breaks_present); + } + } + return thrust::make_tuple(objective_cost[0], infeasibility_cost[0]); } diff --git a/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py b/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py index cdf25291f2..fbfe411dbf 100644 --- a/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py +++ b/python/cuopt/cuopt/tests/routing/test_vehicle_properties.py @@ -733,3 +733,34 @@ 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_required_break_unreachable_is_infeasible(): + """The problem is infeasible if required break is unreachable""" + 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, "break is unreachable, expected status 1"