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
36 changes: 35 additions & 1 deletion cpp/src/routing/route/route.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -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<objective_cost_t, infeasible_cost_t> compute_cost(
bool check_single_threaded = true)
{
Expand All @@ -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]);
}

Expand Down
31 changes: 31 additions & 0 deletions python/cuopt/cuopt/tests/routing/test_vehicle_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"