Skip to content

Commit 4f79b65

Browse files
committed
[routing] improve lp scheduing
1 parent 0dd65ba commit 4f79b65

File tree

4 files changed

+182
-65
lines changed

4 files changed

+182
-65
lines changed

ortools/routing/decision_builders.cc

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
#include <cstdint>
1818
#include <functional>
1919
#include <limits>
20-
#include <new>
2120
#include <string>
2221
#include <utility>
2322
#include <vector>
@@ -256,6 +255,8 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder {
256255
solver->TopPeriodicCheck();
257256
std::vector<int64_t> cumul_values;
258257
std::vector<int64_t> break_start_end_values;
258+
// TODO(user): Distinguish between FEASIBLE and OPTIMAL statuses to
259+
// keep track of the FEASIBLE-only cases.
259260
if (!ComputeCumulAndBreakValuesForVehicle(vehicle, next, &cumul_values,
260261
&break_start_end_values)) {
261262
return false;
@@ -385,9 +386,6 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder {
385386
: optimizer->ComputeRouteCumuls(
386387
vehicle, next_accessor, dimension_travel_info, resource,
387388
cumul_values, break_start_end_values);
388-
if (status == DimensionSchedulingStatus::INFEASIBLE) {
389-
return false;
390-
}
391389
// If relaxation is not feasible, try the MP optimizer.
392390
if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) {
393391
DCHECK(!use_mp_optimizer);
@@ -399,11 +397,9 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder {
399397
: mp_optimizer_->ComputeRouteCumuls(
400398
vehicle, next_accessor, dimension_travel_info,
401399
resource, cumul_values, break_start_end_values);
402-
if (status == DimensionSchedulingStatus::INFEASIBLE) {
403-
return false;
404-
}
405-
} else {
406-
DCHECK(status == DimensionSchedulingStatus::OPTIMAL);
400+
}
401+
if (status == DimensionSchedulingStatus::INFEASIBLE) {
402+
return false;
407403
}
408404
return true;
409405
}
@@ -555,23 +551,17 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder {
555551
model->GetDimensionResourceGroupIndices(dimension).empty()
556552
? global_optimizer_
557553
: global_mp_optimizer_;
558-
const DimensionSchedulingStatus status = ComputeCumulBreakAndResourceValues(
554+
DimensionSchedulingStatus status = ComputeCumulBreakAndResourceValues(
559555
optimizer, &cumul_values_, &break_start_end_values_,
560556
&resource_indices_per_group_);
561-
557+
if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) {
558+
// If relaxation is not feasible, try the MILP optimizer.
559+
status = ComputeCumulBreakAndResourceValues(
560+
global_mp_optimizer_, &cumul_values_, &break_start_end_values_,
561+
&resource_indices_per_group_);
562+
}
562563
if (status == DimensionSchedulingStatus::INFEASIBLE) {
563564
return false;
564-
} else if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) {
565-
// If relaxation is not feasible, try the MILP optimizer.
566-
const DimensionSchedulingStatus mp_status =
567-
ComputeCumulBreakAndResourceValues(
568-
global_mp_optimizer_, &cumul_values_, &break_start_end_values_,
569-
&resource_indices_per_group_);
570-
if (mp_status != DimensionSchedulingStatus::OPTIMAL) {
571-
return false;
572-
}
573-
} else {
574-
DCHECK(status == DimensionSchedulingStatus::OPTIMAL);
575565
}
576566
// Concatenate cumul_values_, break_start_end_values_ and all
577567
// resource_indices_per_group_ into cp_values_.

ortools/routing/filters.cc

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2742,31 +2742,43 @@ bool LPCumulFilter::Accept(const Assignment* delta,
27422742
if (!filter_objective_cost_) {
27432743
// No need to compute the cost of the LP, only verify its feasibility.
27442744
delta_cost_without_transit_ = 0;
2745-
const DimensionSchedulingStatus status = lp_optimizer_.ComputeCumuls(
2745+
DimensionSchedulingStatus status = lp_optimizer_.ComputeCumuls(
27462746
next_accessor, {}, nullptr, nullptr, nullptr);
2747-
if (status == DimensionSchedulingStatus::OPTIMAL) return true;
2748-
if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY &&
2749-
mp_optimizer_.ComputeCumuls(next_accessor, {}, nullptr, nullptr,
2750-
nullptr) ==
2751-
DimensionSchedulingStatus::OPTIMAL) {
2752-
return true;
2747+
if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) {
2748+
status = mp_optimizer_.ComputeCumuls(next_accessor, {}, nullptr, nullptr,
2749+
nullptr);
27532750
}
2754-
return false;
2751+
DCHECK(status != DimensionSchedulingStatus::FEASIBLE)
2752+
<< "FEASIBLE without filtering objective cost should be OPTIMAL";
2753+
return status == DimensionSchedulingStatus::OPTIMAL;
27552754
}
27562755

2757-
const DimensionSchedulingStatus status =
2756+
DimensionSchedulingStatus status =
27582757
lp_optimizer_.ComputeCumulCostWithoutFixedTransits(
27592758
next_accessor, &delta_cost_without_transit_);
2760-
if (status == DimensionSchedulingStatus::INFEASIBLE) {
2761-
delta_cost_without_transit_ = std::numeric_limits<int64_t>::max();
2762-
return false;
2759+
2760+
if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY ||
2761+
status == DimensionSchedulingStatus::FEASIBLE) {
2762+
const DimensionSchedulingStatus lp_status = status;
2763+
int64_t mp_cost;
2764+
status = mp_optimizer_.ComputeCumulCostWithoutFixedTransits(next_accessor,
2765+
&mp_cost);
2766+
if (lp_status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY &&
2767+
status == DimensionSchedulingStatus::OPTIMAL) {
2768+
// TRICKY: If the MP is only feasible, the computed cost isn't a lower
2769+
// bound to the problem, so we keep the LP relaxation's lower bound
2770+
// found by Glop.
2771+
delta_cost_without_transit_ = mp_cost;
2772+
} else if (lp_status == DimensionSchedulingStatus::FEASIBLE &&
2773+
status != DimensionSchedulingStatus::INFEASIBLE) {
2774+
// TRICKY: Since feasible costs are not lower bounds, we keep the lowest
2775+
// of the costs between the LP-feasible and CP-SAT (feasible or optimal).
2776+
delta_cost_without_transit_ =
2777+
std::min(delta_cost_without_transit_, mp_cost);
2778+
}
27632779
}
2764-
if (delta_cost_without_transit_ > objective_max) return false;
27652780

2766-
if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY &&
2767-
mp_optimizer_.ComputeCumulCostWithoutFixedTransits(
2768-
next_accessor, &delta_cost_without_transit_) !=
2769-
DimensionSchedulingStatus::OPTIMAL) {
2781+
if (status == DimensionSchedulingStatus::INFEASIBLE) {
27702782
delta_cost_without_transit_ = std::numeric_limits<int64_t>::max();
27712783
return false;
27722784
}
@@ -2796,22 +2808,17 @@ void LPCumulFilter::OnSynchronize(const Assignment* /*delta*/) {
27962808
next_accessor, &synchronized_cost_without_transit_)
27972809
: lp_optimizer_.ComputeCumuls(next_accessor, {}, nullptr, nullptr,
27982810
nullptr);
2799-
if (status == DimensionSchedulingStatus::INFEASIBLE) {
2800-
// TODO(user): This should only happen if the LP solver times out.
2801-
// DCHECK the fail wasn't due to an infeasible model.
2802-
synchronized_cost_without_transit_ = 0;
2803-
}
28042811
if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) {
28052812
status = filter_objective_cost_
28062813
? mp_optimizer_.ComputeCumulCostWithoutFixedTransits(
28072814
next_accessor, &synchronized_cost_without_transit_)
28082815
: mp_optimizer_.ComputeCumuls(next_accessor, {}, nullptr,
28092816
nullptr, nullptr);
2810-
if (status != DimensionSchedulingStatus::OPTIMAL) {
2811-
// TODO(user): This should only happen if the MP solver times out.
2812-
// DCHECK the fail wasn't due to an infeasible model.
2813-
synchronized_cost_without_transit_ = 0;
2814-
}
2817+
}
2818+
if (status == DimensionSchedulingStatus::INFEASIBLE) {
2819+
// TODO(user): This should only happen if the LP/MIP solver times out.
2820+
// DCHECK the fail wasn't due to an infeasible model.
2821+
synchronized_cost_without_transit_ = 0;
28152822
}
28162823
}
28172824

@@ -3159,7 +3166,8 @@ ResourceGroupAssignmentFilter::ComputeRouteCumulCostWithoutResourceAssignment(
31593166
}
31603167
break;
31613168
default:
3162-
DCHECK(status == DimensionSchedulingStatus::OPTIMAL);
3169+
DCHECK(status == DimensionSchedulingStatus::OPTIMAL ||
3170+
status == DimensionSchedulingStatus::FEASIBLE);
31633171
}
31643172
return route_cost;
31653173
}

ortools/routing/lp_scheduling.cc

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
#include <vector>
2828

2929
#include "absl/algorithm/container.h"
30+
#include "absl/container/flat_hash_map.h"
31+
#include "absl/container/flat_hash_set.h"
3032
#include "absl/log/check.h"
3133
#include "absl/strings/str_format.h"
3234
#include "absl/strings/string_view.h"
@@ -36,6 +38,7 @@
3638
#include "ortools/base/logging.h"
3739
#include "ortools/base/map_util.h"
3840
#include "ortools/base/mathutil.h"
41+
#include "ortools/base/strong_vector.h"
3942
#include "ortools/base/types.h"
4043
#include "ortools/constraint_solver/constraint_solver.h"
4144
#include "ortools/glop/parameters.pb.h"
@@ -1039,15 +1042,12 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeAndPack(
10391042
packing_parameters.set_use_preprocessing(true);
10401043
solver->SetParameters(packing_parameters.SerializeAsString());
10411044
}
1042-
DimensionSchedulingStatus status = DimensionSchedulingStatus::OPTIMAL;
1043-
if (Optimize(next_accessor, dimension_travel_info_per_route, solver,
1045+
DimensionSchedulingStatus status =
1046+
Optimize(next_accessor, dimension_travel_info_per_route, solver,
10441047
/*cumul_values=*/nullptr, /*break_values=*/nullptr,
10451048
/*resource_indices_per_group=*/nullptr, &cost,
10461049
/*transit_cost=*/nullptr,
1047-
/*clear_lp=*/false, /*optimize_resource_assignment=*/false) ==
1048-
DimensionSchedulingStatus::INFEASIBLE) {
1049-
status = DimensionSchedulingStatus::INFEASIBLE;
1050-
}
1050+
/*clear_lp=*/false, /*optimize_resource_assignment=*/false);
10511051
if (status != DimensionSchedulingStatus::INFEASIBLE) {
10521052
std::vector<int> vehicles(dimension()->model()->vehicles());
10531053
std::iota(vehicles.begin(), vehicles.end(), 0);
@@ -2147,6 +2147,114 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints(
21472147
}
21482148
}
21492149

2150+
// TODO(user): find why adding these constraints make CPSAT slower.
2151+
if (!solver->IsCPSATSolver()) {
2152+
for (const auto& [limit, min_break_duration] :
2153+
dimension_->GetBreakDistanceDurationOfVehicle(vehicle)) {
2154+
int64_t min_num_breaks = 0;
2155+
if (limit > 0) {
2156+
min_num_breaks =
2157+
std::max<int64_t>(0, CapSub(total_fixed_transit, 1) / limit);
2158+
}
2159+
if (CapSub(current_route_min_cumuls_.back(),
2160+
current_route_max_cumuls_.front()) > limit) {
2161+
min_num_breaks = std::max<int64_t>(min_num_breaks, 1);
2162+
}
2163+
if (num_breaks < min_num_breaks) return false;
2164+
if (min_num_breaks == 0) continue;
2165+
2166+
// Adds an LP relaxation of interbreak constraints.
2167+
// For all 0 <= pl < pr < path_size, for k > 0,
2168+
// if sum_{p in [pl, pr)} fixed_transit[p] > k * limit,
2169+
// then sum_{p in [pl, pr)} slack[p] >= k * min_break_duration.
2170+
//
2171+
// Moreover, if end_min[pr] - start_max[pl] > limit,
2172+
// the sum_{p in [pl, pr)} slack[p] >= min_break_duration.
2173+
//
2174+
// We want to apply the constraints above, without the ones that are
2175+
// dominated:
2176+
// - do not add the same constraint for k' < k, keep the largest k.
2177+
// - do not add the constraint for both (pl', pr') and (pl, pr)
2178+
// if [pl', pr') is a subset of [pl, pr), keep the smallest interval.
2179+
// TODO(user): reduce the number of constraints further;
2180+
// for instance if the constraint holds for (k, pl, pr) and (k', pl', pr')
2181+
// with pr <= pr', then no need to add the constraint for (k+k', pl, pr').
2182+
//
2183+
// We need fast access to sum_{p in [pl, pr)} fixed_transit[p].
2184+
// This will be sum_transits[pr] - sum_transits[pl]. Note that
2185+
// sum_transits[0] = 0, sum_transits[path_size-1] = total_fixed_transit.
2186+
std::vector<int64_t> sum_transits(path_size);
2187+
{
2188+
sum_transits[0] = 0;
2189+
for (int pos = 1; pos < path_size; ++pos) {
2190+
sum_transits[pos] = sum_transits[pos - 1] + fixed_transit[pos - 1];
2191+
}
2192+
}
2193+
// To add the slack sum constraints, we need slack sum variables.
2194+
// Those are created lazily in a sparse vector, then only those useful
2195+
// variables are linked to slack variables after slack sum constraints
2196+
// have been added.
2197+
std::vector<int> slack_sum_vars(path_size, -1);
2198+
// Given a number of breaks k, an interval of path positions [pl, pr),
2199+
// returns true if the interbreak constraint triggers for k breaks.
2200+
// TODO(user): find tighter end_min/start_max conditions.
2201+
// Mind that a break may be longer than min_break_duration.
2202+
auto trigger = [&](int k, int pl, int pr) -> bool {
2203+
if (k == 1) {
2204+
const int64_t span_lb =
2205+
current_route_min_cumuls_[pr] - current_route_max_cumuls_[pl];
2206+
if (span_lb > limit) return true;
2207+
}
2208+
return sum_transits[pr] - sum_transits[pl] > k * limit;
2209+
};
2210+
int min_sum_var_index = path_size;
2211+
int max_sum_var_index = -1;
2212+
for (int k = 1; k <= min_num_breaks; ++k) {
2213+
int pr = 0;
2214+
for (int pl = 0; pl < path_size - 1; ++pl) {
2215+
pr = std::max(pr, pl + 1);
2216+
// Increase pr until transit(pl, pr) > k * limit.
2217+
while (pr < path_size && !trigger(k, pl, pr)) ++pr;
2218+
if (pr == path_size) break;
2219+
// Reduce [pl, pr) from the left.
2220+
while (pl < pr && trigger(k, pl + 1, pr)) ++pl;
2221+
if (slack_sum_vars[pl] == -1) {
2222+
slack_sum_vars[pl] = solver->CreateNewPositiveVariable();
2223+
min_sum_var_index = std::min(min_sum_var_index, pl);
2224+
}
2225+
if (slack_sum_vars[pr] == -1) {
2226+
slack_sum_vars[pr] = solver->CreateNewPositiveVariable();
2227+
max_sum_var_index = std::max(max_sum_var_index, pr);
2228+
}
2229+
// If k is the largest for this interval, add the constraint.
2230+
// The call trigger(k', pl, pr) may hold for both k and k+1 with an
2231+
// irreducible interval [pl, pr] when there is a transit > limit at
2232+
// the beginning and at the end of the sub-route. sum_slacks[pr] -
2233+
// sum_slacks[pl] >= k * min_break_duration.
2234+
if (k < min_num_breaks && trigger(k + 1, pl, pr)) continue;
2235+
solver->AddLinearConstraint(
2236+
k * min_break_duration, kint64max,
2237+
{{slack_sum_vars[pr], 1}, {slack_sum_vars[pl], -1}});
2238+
}
2239+
}
2240+
if (min_sum_var_index < max_sum_var_index) {
2241+
slack_sum_vars[min_sum_var_index] = solver->AddVariable(0, 0);
2242+
int prev_index = min_sum_var_index;
2243+
for (int pos = min_sum_var_index + 1; pos <= max_sum_var_index; ++pos) {
2244+
if (slack_sum_vars[pos] == -1) continue;
2245+
// slack_sum_var[pos] =
2246+
// slack_sum_var[prev_index] + sum_{p in [prev_index, pos)} slack[p].
2247+
const int ct = solver->AddLinearConstraint(
2248+
0, 0,
2249+
{{slack_sum_vars[pos], 1}, {slack_sum_vars[prev_index], -1}});
2250+
for (int p = prev_index; p < pos; ++p) {
2251+
solver->SetCoefficient(ct, lp_slacks[p], -1);
2252+
}
2253+
prev_index = pos;
2254+
}
2255+
}
2256+
}
2257+
}
21502258
if (!solver->IsCPSATSolver()) return true;
21512259
if (!dimension_->GetBreakDistanceDurationOfVehicle(vehicle).empty()) {
21522260
// If there is an optional interval, the following model would be wrong.
@@ -2266,7 +2374,7 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints(
22662374
}
22672375

22682376
return true;
2269-
}
2377+
} // NOLINT(readability/fn_size)
22702378

22712379
namespace {
22722380
bool AllValuesContainedExcept(const IntVar& var, absl::Span<const int> values,

0 commit comments

Comments
 (0)