Skip to content

Commit 2a4500a

Browse files
committed
fix(routing): penalize missing required breaks in route cost
Signed-off-by: Nikolai Poperechnyi <n.poperechnyi@gmail.com>
1 parent e7df73d commit 2a4500a

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

cpp/src/routing/route/route.cuh

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,24 @@ class route_t {
646646
orig_route, intra_ejection_indices[n_ejections - 1] + 1, route_length + 1, curr_size);
647647
}
648648

649-
// extend for other things later
649+
/**
650+
* @brief Recomputes the objective and infeasibility cost of the current route.
651+
*
652+
* Resets the route cost accumulators, invokes cost computation for each active
653+
* dimension, and returns the resulting objective and infeasibility costs.
654+
*
655+
* When both BREAK and TIME dimensions are present, this method also adds an
656+
* explicit infeasibility penalty for missing required breaks. A break is treated
657+
* as required if its latest time is no later than the vehicle arrival time at
658+
* the depot. Any deficit between required breaks and breaks present in the
659+
* route is added to the BREAK infeasibility component.
660+
*
661+
* @param check_single_threaded If true, assert that exactly one warp thread is active.
662+
*
663+
* @return Tuple of `{objective_cost, infeasibility_cost}` for the route.
664+
*
665+
* @note Mutates and returns `objective_cost[0]` and `infeasibility_cost[0]`.
666+
*/
650667
DI thrust::tuple<objective_cost_t, infeasible_cost_t> compute_cost(
651668
bool check_single_threaded = true)
652669
{
@@ -663,6 +680,23 @@ class route_t {
663680
.compute_cost(this->vehicle_info(), *n_nodes, objective_cost[0], infeasibility_cost[0]);
664681
});
665682

683+
// Penalize missing required breaks; the per-dim formula only catches excess.
684+
if (dimensions_info().has_dimension(dim_t::BREAK) &&
685+
dimensions_info().has_dimension(dim_t::TIME)) {
686+
auto vinfo = this->vehicle_info();
687+
const i_t n_breaks = vinfo.num_breaks();
688+
if (n_breaks > 0) {
689+
const double arrival_at_depot = dimensions.time_dim.departure_forward[*n_nodes] +
690+
dimensions.time_dim.excess_forward[*n_nodes];
691+
i_t required = 0;
692+
for (i_t i = 0; i < n_breaks; ++i) {
693+
required += (vinfo.break_latest[i] <= arrival_at_depot);
694+
}
695+
const i_t breaks_present = dimensions.break_dim.breaks_forward[*n_nodes];
696+
infeasibility_cost[0][dim_t::BREAK] += max(0.0, required - breaks_present);
697+
}
698+
}
699+
666700
return thrust::make_tuple(objective_cost[0], infeasibility_cost[0]);
667701
}
668702

python/cuopt/cuopt/tests/routing/test_vehicle_properties.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,3 +733,34 @@ def test_empty_routes_with_breaks():
733733
h_route = solution_vehicle_x["route"].to_arrow().to_pylist()
734734
route_len = len(h_route)
735735
assert route_len > 3
736+
737+
738+
def test_required_break_unreachable_is_infeasible():
739+
"""The problem is infeasible if required break is unreachable"""
740+
coords = np.array(
741+
[[0.0, 0.0], [10.0, 0.0], [0.0, 200.0]], dtype=np.float32
742+
)
743+
diff = coords[:, None] - coords[None, :]
744+
matrix = cudf.DataFrame(np.linalg.norm(diff, axis=-1).astype(np.float32))
745+
746+
dm = routing.DataModel(3, n_fleet=1, n_orders=1)
747+
dm.add_cost_matrix(matrix)
748+
dm.add_transit_time_matrix(matrix)
749+
dm.set_order_locations(cudf.Series([1], dtype=np.int32))
750+
dm.set_order_time_windows(
751+
cudf.Series([0], dtype=np.int32),
752+
cudf.Series([1000], dtype=np.int32),
753+
)
754+
dm.set_vehicle_time_windows(
755+
cudf.Series([0], dtype=np.int32),
756+
cudf.Series([1000], dtype=np.int32),
757+
)
758+
dm.set_break_locations(cudf.Series([2], dtype=np.int32))
759+
dm.add_break_dimension(
760+
cudf.Series([0], dtype=np.int32),
761+
cudf.Series([5], dtype=np.int32),
762+
cudf.Series([5], dtype=np.int32),
763+
)
764+
765+
sol = routing.Solve(dm)
766+
assert sol.get_status() == 1, "break is unreachable, expected status 1"

0 commit comments

Comments
 (0)