[CP-SAT] improve scheduling default search
This commit is contained in:
@@ -135,9 +135,11 @@ struct VarValue {
|
||||
|
||||
namespace {
|
||||
|
||||
bool ModelContainsNoOverlaps(const CpModelProto& cp_model_proto) {
|
||||
// TODO(user): Save this somewhere instead of recomputing it.
|
||||
bool ModelHasSchedulingConstraints(const CpModelProto& cp_model_proto) {
|
||||
for (const ConstraintProto& ct : cp_model_proto.constraints()) {
|
||||
if (ct.constraint_case() == ConstraintProto::kNoOverlap) return true;
|
||||
if (ct.constraint_case() == ConstraintProto::kCumulative) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -305,9 +307,7 @@ std::function<BooleanOrIntegerLiteral()> ConstructSearchStrategy(
|
||||
|
||||
// If there are some scheduling constraint, we complete with a custom
|
||||
// "scheduling" strategy.
|
||||
//
|
||||
// TODO(user): also deal with cumulative.
|
||||
if (ModelContainsNoOverlaps(cp_model_proto)) {
|
||||
if (ModelHasSchedulingConstraints(cp_model_proto)) {
|
||||
heuristics.push_back(SchedulingSearchHeuristic(model));
|
||||
}
|
||||
|
||||
@@ -492,6 +492,15 @@ std::vector<SatParameters> GetDiverseSetOfParameters(
|
||||
strategies["less_encoding"] = new_params;
|
||||
}
|
||||
|
||||
// We only use a "fixed search" worker if some strategy is specified or
|
||||
// if we have a scheduling model.
|
||||
//
|
||||
// TODO(user): For scheduling, this is important to find good first solution
|
||||
// but afterwards it is not really great and should probably be replaced by a
|
||||
// LNS worker.
|
||||
const bool use_fixed_strategy = !cp_model.search_strategy().empty() ||
|
||||
ModelHasSchedulingConstraints(cp_model);
|
||||
|
||||
// Our current set of strategies
|
||||
//
|
||||
// TODO(user, fdid): Avoid launching two strategies if they are the same,
|
||||
@@ -502,26 +511,30 @@ std::vector<SatParameters> GetDiverseSetOfParameters(
|
||||
// Low memory mode for interleaved search in single thread.
|
||||
if (cp_model.has_objective()) {
|
||||
names.push_back("default_lp");
|
||||
names.push_back(!cp_model.search_strategy().empty() ? "fixed"
|
||||
: "pseudo_costs");
|
||||
names.push_back(use_fixed_strategy ? "fixed" : "pseudo_costs");
|
||||
names.push_back(cp_model.objective().vars_size() > 1 ? "core" : "no_lp");
|
||||
names.push_back("max_lp");
|
||||
} else {
|
||||
names.push_back("default_lp");
|
||||
names.push_back(cp_model.search_strategy_size() > 0 ? "fixed" : "no_lp");
|
||||
names.push_back(use_fixed_strategy ? "fixed" : "no_lp");
|
||||
names.push_back("less_encoding");
|
||||
names.push_back("max_lp");
|
||||
names.push_back("quick_restart");
|
||||
}
|
||||
} else if (cp_model.has_objective()) {
|
||||
names.push_back("default_lp");
|
||||
names.push_back(!cp_model.search_strategy().empty() ? "fixed"
|
||||
: "reduced_costs");
|
||||
if (use_fixed_strategy) {
|
||||
names.push_back("fixed");
|
||||
if (num_workers > 8) names.push_back("reduced_costs");
|
||||
} else {
|
||||
names.push_back("reduced_costs");
|
||||
}
|
||||
names.push_back("pseudo_costs");
|
||||
names.push_back("no_lp");
|
||||
names.push_back("max_lp");
|
||||
if (cp_model.objective().vars_size() > 1) names.push_back("core");
|
||||
|
||||
// TODO(user): Experiment with core and LP.
|
||||
if (cp_model.objective().vars_size() > 1) names.push_back("core");
|
||||
|
||||
// Only add this strategy if we have enough worker left for LNS.
|
||||
if (num_workers > 8 || base_params.interleave_search()) {
|
||||
@@ -532,7 +545,7 @@ std::vector<SatParameters> GetDiverseSetOfParameters(
|
||||
}
|
||||
} else {
|
||||
names.push_back("default_lp");
|
||||
if (cp_model.search_strategy_size() > 0) names.push_back("fixed");
|
||||
if (use_fixed_strategy) names.push_back("fixed");
|
||||
names.push_back("less_encoding");
|
||||
names.push_back("no_lp");
|
||||
names.push_back("max_lp");
|
||||
|
||||
@@ -62,8 +62,6 @@ std::function<void(Model*)> Disjunctive(
|
||||
SchedulingConstraintHelper* helper =
|
||||
new SchedulingConstraintHelper(vars, model);
|
||||
model->TakeOwnership(helper);
|
||||
model->GetOrCreate<ModelNoOverlapHelperRepository>()->no_overlaps.push_back(
|
||||
helper);
|
||||
|
||||
// Experiments to use the timetable only to propagate the disjunctive.
|
||||
if (/*DISABLES_CODE*/ (false)) {
|
||||
|
||||
@@ -30,12 +30,6 @@
|
||||
namespace operations_research {
|
||||
namespace sat {
|
||||
|
||||
// Regroup all the set of intervals that are in no-overlap relation.
|
||||
// This is used by the simple scheduling search strategy.
|
||||
struct ModelNoOverlapHelperRepository {
|
||||
std::vector<SchedulingConstraintHelper*> no_overlaps;
|
||||
};
|
||||
|
||||
// Enforces a disjunctive (or no overlap) constraint on the given interval
|
||||
// variables. The intervals are interpreted as [start, end) and the constraint
|
||||
// enforces that no time point belongs to two intervals.
|
||||
|
||||
@@ -578,6 +578,7 @@ bool IntegerTrail::Propagate(Trail* trail) {
|
||||
|
||||
void IntegerTrail::Untrail(const Trail& trail, int literal_trail_index) {
|
||||
++num_untrails_;
|
||||
conditional_lbs_.clear();
|
||||
const int level = trail.CurrentDecisionLevel();
|
||||
var_to_current_lb_interval_index_.SetLevel(level);
|
||||
propagation_trail_index_ =
|
||||
@@ -1057,6 +1058,21 @@ bool IntegerTrail::ConditionalEnqueue(
|
||||
}
|
||||
|
||||
// We can't push anything in this case.
|
||||
//
|
||||
// We record it for this propagation phase (until the next untrail) as this
|
||||
// is relatively fast and heuristics can exploit this.
|
||||
//
|
||||
// Note that currently we only use ConditionalEnqueue() in scheduling
|
||||
// propagator, and these propagator are quite slow so this is not visible.
|
||||
//
|
||||
// TODO(user): We could even keep the reason and maybe do some reasoning using
|
||||
// at_least_one constraint on a set of the Boolean used here.
|
||||
const auto [it, inserted] =
|
||||
conditional_lbs_.insert({{lit.Index(), i_lit.var}, i_lit.bound});
|
||||
if (!inserted) {
|
||||
it->second = std::max(it->second, i_lit.bound);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -705,6 +705,11 @@ class IntegerTrail : public SatPropagator {
|
||||
// Returns true if the variable is fixed at level 0.
|
||||
bool IsFixedAtLevelZero(IntegerVariable var) const;
|
||||
|
||||
// Advanced usage.
|
||||
// Returns the current lower bound assuming the literal is true.
|
||||
IntegerValue ConditionalLowerBound(Literal l, IntegerVariable i) const;
|
||||
IntegerValue ConditionalLowerBound(Literal l, AffineExpression expr) const;
|
||||
|
||||
// Advanced usage. Given the reason for
|
||||
// (Sum_i coeffs[i] * reason[i].var >= current_lb) initially in reason,
|
||||
// this function relaxes the reason given that we only need the explanation of
|
||||
@@ -1087,6 +1092,12 @@ class IntegerTrail : public SatPropagator {
|
||||
Trail* trail_;
|
||||
const SatParameters& parameters_;
|
||||
|
||||
// Temporary "hash" to keep track of all the conditional enqueue that were
|
||||
// done. Note that we currently do not keep any reason for them, and as such,
|
||||
// we can only use this in heuristics. See ConditionalLowerBound().
|
||||
absl::flat_hash_map<std::pair<LiteralIndex, IntegerVariable>, IntegerValue>
|
||||
conditional_lbs_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(IntegerTrail);
|
||||
};
|
||||
|
||||
@@ -1343,14 +1354,26 @@ inline bool IntegerTrail::IsFixed(IntegerVariable i) const {
|
||||
return vars_[i].current_bound == -vars_[NegationOf(i)].current_bound;
|
||||
}
|
||||
|
||||
// TODO(user): Use capped arithmetic? It might be slow though and we better just
|
||||
// make sure there is no overflow at model creation.
|
||||
inline IntegerValue IntegerTrail::LowerBound(AffineExpression expr) const {
|
||||
if (expr.var == kNoIntegerVariable) return expr.constant;
|
||||
return LowerBound(expr.var) * expr.coeff + expr.constant;
|
||||
}
|
||||
|
||||
// TODO(user): Use capped arithmetic? same remark as for LowerBound().
|
||||
inline IntegerValue IntegerTrail::ConditionalLowerBound(
|
||||
Literal l, IntegerVariable i) const {
|
||||
const auto it = conditional_lbs_.find({l.Index(), i});
|
||||
if (it != conditional_lbs_.end()) {
|
||||
return std::max(vars_[i].current_bound, it->second);
|
||||
}
|
||||
return vars_[i].current_bound;
|
||||
}
|
||||
|
||||
inline IntegerValue IntegerTrail::ConditionalLowerBound(
|
||||
Literal l, AffineExpression expr) const {
|
||||
if (expr.var == kNoIntegerVariable) return expr.constant;
|
||||
return ConditionalLowerBound(l, expr.var) * expr.coeff + expr.constant;
|
||||
}
|
||||
|
||||
inline IntegerValue IntegerTrail::UpperBound(AffineExpression expr) const {
|
||||
if (expr.var == kNoIntegerVariable) return expr.constant;
|
||||
return UpperBound(expr.var) * expr.coeff + expr.constant;
|
||||
|
||||
@@ -346,16 +346,10 @@ std::function<BooleanOrIntegerLiteral()> PseudoCost(Model* model) {
|
||||
};
|
||||
}
|
||||
|
||||
// A simple heuristic for scheduling with no overlaps.
|
||||
//
|
||||
// TODO(user): Extend to cumulative constraint. A better design is probably
|
||||
// just to collect for each interval, the start_min that was pushed during the
|
||||
// last propagation if that interval was present. In case of optional interval,
|
||||
// we cannot "commit" the push, but we still do it, so if we record in the
|
||||
// interval repository that value, that should be enough for a good heuristic.
|
||||
// A simple heuristic for scheduling models.
|
||||
std::function<BooleanOrIntegerLiteral()> SchedulingSearchHeuristic(
|
||||
Model* model) {
|
||||
auto* repo = model->GetOrCreate<ModelNoOverlapHelperRepository>();
|
||||
auto* repo = model->GetOrCreate<IntervalsRepository>();
|
||||
auto* heuristic = model->GetOrCreate<SearchHeuristics>();
|
||||
auto* trail = model->GetOrCreate<Trail>();
|
||||
auto* integer_trail = model->GetOrCreate<IntegerTrail>();
|
||||
@@ -367,85 +361,48 @@ std::function<BooleanOrIntegerLiteral()> SchedulingSearchHeuristic(
|
||||
AffineExpression end;
|
||||
|
||||
// Information to select best.
|
||||
int index = 0;
|
||||
int task;
|
||||
IntegerValue size_min = kMaxIntegerValue;
|
||||
IntegerValue time = kMaxIntegerValue;
|
||||
};
|
||||
ToSchedule best;
|
||||
|
||||
int index = 0;
|
||||
for (SchedulingConstraintHelper* helper : repo->no_overlaps) {
|
||||
if (!helper->SynchronizeAndSetTimeDirection(true)) {
|
||||
return BooleanOrIntegerLiteral();
|
||||
}
|
||||
// TODO(user): we should also precompute fixed precedences and only fix
|
||||
// interval that have all their predecessors fixed.
|
||||
const int num_intervals = repo->NumIntervals();
|
||||
for (IntervalVariable i(0); i < num_intervals; ++i) {
|
||||
if (repo->IsAbsent(i)) continue;
|
||||
if (!repo->IsPresent(i) || !integer_trail->IsFixed(repo->Start(i)) ||
|
||||
!integer_trail->IsFixed(repo->End(i))) {
|
||||
IntegerValue time = integer_trail->LowerBound(repo->Start(i));
|
||||
if (repo->IsOptional(i)) {
|
||||
// For task whose presence is still unknown, our propagators should
|
||||
// have propagated the minimium time as if it was present. So this
|
||||
// should reflect the earliest time at which this interval can be
|
||||
// scheduled.
|
||||
time = std::max(time, integer_trail->ConditionalLowerBound(
|
||||
repo->PresenceLiteral(i), repo->Start(i)));
|
||||
}
|
||||
|
||||
// The heuristic idea is to find the interval that can be scheduled first,
|
||||
// and fully schedule it. Note however than for optional interval,
|
||||
// depending on the model, one cannot use the current start min to decide
|
||||
// this since it might not be propagated.
|
||||
//
|
||||
// For now, we assume that this heuristic was used before, so the max
|
||||
// of the fixed interval is used as the earliest start time.
|
||||
//
|
||||
// TODO(user): we should also precompute fixed precedences and only fix
|
||||
// interval that have all their predecessors fixed.
|
||||
//
|
||||
// TODO(user): We could actually compute the earliest possible start of
|
||||
// each task. This is best in the presence of fixed interval from the
|
||||
// start or during a non-fixed search to be smarter about the next integer
|
||||
// variable to instantiate.
|
||||
IntegerValue first_free_spot = kMinIntegerValue;
|
||||
for (int t = 0; t < helper->NumTasks(); ++t) {
|
||||
if (!helper->IsPresent(t)) continue;
|
||||
if (!integer_trail->IsFixed(helper->Starts()[t])) continue;
|
||||
if (!integer_trail->IsFixed(helper->Ends()[t])) continue;
|
||||
first_free_spot = std::max(first_free_spot, helper->EndMin(t));
|
||||
}
|
||||
for (int t = 0; t < helper->NumTasks(); ++t) {
|
||||
if (helper->IsAbsent(t)) continue;
|
||||
if (!helper->IsPresent(t) ||
|
||||
!integer_trail->IsFixed(helper->Starts()[t]) ||
|
||||
!integer_trail->IsFixed(helper->Ends()[t])) {
|
||||
IntegerValue time;
|
||||
if (helper->IsPresent(t)) {
|
||||
// If t is present, we asssume its start min was propagated
|
||||
// correctly.
|
||||
time = helper->StartMin(t);
|
||||
} else {
|
||||
// Otherwise we use "first_free_spot" even though it is not perfect
|
||||
// in case not everything was fixed by this heuristic there.
|
||||
time = std::max(helper->StartMin(t), first_free_spot);
|
||||
}
|
||||
|
||||
// For variable size, we compute the min size once the start is fixed
|
||||
// to time. This is needed to never pick the "artificial" makespan
|
||||
// interval at the end in priority compared to intervals that still
|
||||
// need to be scheduled.
|
||||
const IntegerValue size_min =
|
||||
std::max(helper->SizeMin(t), helper->EndMin(t) - time);
|
||||
if (time < best.time ||
|
||||
(time == best.time && size_min < best.size_min)) {
|
||||
best.presence = helper->IsOptional(t)
|
||||
? helper->PresenceLiteral(t).Index()
|
||||
: kNoLiteralIndex;
|
||||
best.start = helper->Starts()[t];
|
||||
best.end = helper->Ends()[t];
|
||||
best.size_min = size_min;
|
||||
best.task = t;
|
||||
best.index = index;
|
||||
best.time = time;
|
||||
}
|
||||
// For variable size, we compute the min size once the start is fixed
|
||||
// to time. This is needed to never pick the "artificial" makespan
|
||||
// interval at the end in priority compared to intervals that still
|
||||
// need to be scheduled.
|
||||
const IntegerValue size_min =
|
||||
std::max(integer_trail->LowerBound(repo->Size(i)),
|
||||
integer_trail->LowerBound(repo->End(i)) - time);
|
||||
if (time < best.time ||
|
||||
(time == best.time && size_min < best.size_min)) {
|
||||
best.presence = repo->IsOptional(i) ? repo->PresenceLiteral(i).Index()
|
||||
: kNoLiteralIndex;
|
||||
best.start = repo->Start(i);
|
||||
best.end = repo->End(i);
|
||||
best.time = time;
|
||||
best.size_min = size_min;
|
||||
}
|
||||
}
|
||||
++index;
|
||||
}
|
||||
if (best.time == kMaxIntegerValue) return BooleanOrIntegerLiteral();
|
||||
|
||||
VLOG(1) << "========= Schedule on " << best.index << " @" << best.time
|
||||
<< " (" << repo->no_overlaps[best.index]->TaskDebugString(best.task)
|
||||
<< ")";
|
||||
|
||||
// Use the next_decision_override to fix in turn all the variables from
|
||||
// the selected interval.
|
||||
int num_times = 0;
|
||||
@@ -486,8 +443,7 @@ std::function<BooleanOrIntegerLiteral()> SchedulingSearchHeuristic(
|
||||
}
|
||||
|
||||
// Everything is fixed, dettach the override.
|
||||
VLOG(1) << "Fixed on " << best.index << " @["
|
||||
<< integer_trail->LowerBound(best.start) << ", "
|
||||
VLOG(1) << "Fixed @[" << integer_trail->LowerBound(best.start) << ", "
|
||||
<< integer_trail->LowerBound(best.end) << "]";
|
||||
return BooleanOrIntegerLiteral();
|
||||
};
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/sat/disjunctive.h"
|
||||
#include "ortools/sat/integer.h"
|
||||
#include "ortools/sat/linear_programming_constraint.h"
|
||||
#include "ortools/sat/pseudo_costs.h"
|
||||
|
||||
@@ -82,7 +82,6 @@ PROTO2_RETURN(operations_research::sat::CpSolverResponse,
|
||||
%}
|
||||
|
||||
%typemap(in) std::function<void(const std::string&)> %{
|
||||
|
||||
// $input will be deleted once this function return.
|
||||
// So we create a JNI global reference to keep it alive.
|
||||
jobject $input_object = jenv->NewGlobalRef($input);
|
||||
|
||||
Reference in New Issue
Block a user