Skip to content

Commit df28118

Browse files
committed
[CP-SAT] fix bug with reservoir expansion; improve hint processing; improve work sharing
1 parent 7f55c23 commit df28118

14 files changed

+312
-56
lines changed

ortools/sat/cp_model_checker.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1720,7 +1720,7 @@ class ConstraintChecker {
17201720
current_level += delta.second;
17211721
if (current_level < min_level || current_level > max_level) {
17221722
VLOG(1) << "Reservoir level " << current_level
1723-
<< " is out of bounds at time" << delta.first;
1723+
<< " is out of bounds at time: " << delta.first;
17241724
return false;
17251725
}
17261726
}

ortools/sat/cp_model_expand.cc

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ void ExpandReservoirUsingCircuit(int64_t sum_of_positive_demand,
169169
context->UpdateRuleStats("reservoir: expanded using circuit.");
170170
}
171171

172-
void ExpandReservoirUsingPrecedences(int64_t sum_of_positive_demand,
173-
int64_t sum_of_negative_demand,
172+
void ExpandReservoirUsingPrecedences(bool max_level_is_constraining,
173+
bool min_level_is_constraining,
174174
ConstraintProto* reservoir_ct,
175175
PresolveContext* context) {
176176
const ReservoirConstraintProto& reservoir = reservoir_ct->reservoir();
@@ -192,23 +192,21 @@ void ExpandReservoirUsingPrecedences(int64_t sum_of_positive_demand,
192192

193193
// No need for some constraints if the reservoir is just constrained in
194194
// one direction.
195-
if (demand_i > 0 && sum_of_positive_demand <= reservoir.max_level()) {
196-
continue;
197-
}
198-
if (demand_i < 0 && sum_of_negative_demand >= reservoir.min_level()) {
199-
continue;
200-
}
195+
if (demand_i > 0 && !max_level_is_constraining) continue;
196+
if (demand_i < 0 && !min_level_is_constraining) continue;
201197

202-
ConstraintProto* new_ct = context->working_model->add_constraints();
203-
LinearConstraintProto* new_linear = new_ct->mutable_linear();
204-
205-
// Add contributions from previous events.
198+
ConstraintProto* new_cumul = context->working_model->add_constraints();
199+
LinearConstraintProto* new_linear = new_cumul->mutable_linear();
206200
int64_t offset = 0;
201+
202+
// Add contributions from events that happened at time_j <= time_i.
207203
const LinearExpressionProto& time_i = reservoir.time_exprs(i);
208204
for (int j = 0; j < num_events; ++j) {
209205
if (i == j) continue;
210206
const int active_j = is_active_literal(j);
211207
if (context->LiteralIsFalse(active_j)) continue;
208+
const int64_t demand_j = context->FixedValue(reservoir.level_changes(j));
209+
if (demand_j == 0) continue;
212210

213211
// Get or create the literal equivalent to
214212
// active_i && active_j && time[j] <= time[i].
@@ -218,44 +216,30 @@ void ExpandReservoirUsingPrecedences(int64_t sum_of_positive_demand,
218216
const LinearExpressionProto& time_j = reservoir.time_exprs(j);
219217
const int j_lesseq_i = context->GetOrCreateReifiedPrecedenceLiteral(
220218
time_j, time_i, active_j, active_i);
221-
context->working_model->mutable_variables(j_lesseq_i)
222-
->set_name(absl::StrCat(j, " before ", i));
223-
224-
const int64_t demand = context->FixedValue(reservoir.level_changes(j));
225-
if (RefIsPositive(j_lesseq_i)) {
226-
new_linear->add_vars(j_lesseq_i);
227-
new_linear->add_coeffs(demand);
228-
} else {
229-
new_linear->add_vars(NegatedRef(j_lesseq_i));
230-
new_linear->add_coeffs(-demand);
231-
offset -= demand;
232-
}
219+
AddWeightedLiteralToLinearConstraint(j_lesseq_i, demand_j, new_linear,
220+
&offset);
233221
}
234222

235223
// Add contribution from event i.
236224
//
237225
// TODO(user): Alternatively we can mark the whole constraint as enforced
238226
// only if active_i is true. Experiments with both version, right now we
239227
// miss enough benchmarks to conclude.
240-
if (RefIsPositive(active_i)) {
241-
new_linear->add_vars(active_i);
242-
new_linear->add_coeffs(demand_i);
243-
} else {
244-
new_linear->add_vars(NegatedRef(active_i));
245-
new_linear->add_coeffs(-demand_i);
246-
offset -= demand_i;
247-
}
228+
AddWeightedLiteralToLinearConstraint(active_i, demand_i, new_linear,
229+
&offset);
248230

249231
// Note that according to the sign of demand_i, we only need one side.
232+
// We apply the offset here to make sure we use int64_t min and max.
250233
if (demand_i > 0) {
251234
new_linear->add_domain(std::numeric_limits<int64_t>::min());
252-
new_linear->add_domain(reservoir.max_level());
235+
new_linear->add_domain(reservoir.max_level() - offset);
253236
} else {
254-
new_linear->add_domain(reservoir.min_level());
237+
new_linear->add_domain(reservoir.min_level() - offset);
255238
new_linear->add_domain(std::numeric_limits<int64_t>::max());
256239
}
257240

258-
context->CanonicalizeLinearConstraint(new_ct);
241+
// Canonicalize the newly created constraint.
242+
context->CanonicalizeLinearConstraint(new_cumul);
259243
}
260244

261245
reservoir_ct->Clear();
@@ -367,9 +351,10 @@ void ExpandReservoir(ConstraintProto* reservoir_ct, PresolveContext* context) {
367351
} else {
368352
// This one is the faster option usually.
369353
if (all_demands_are_fixed) {
370-
ExpandReservoirUsingPrecedences(sum_of_positive_demand,
371-
sum_of_negative_demand, reservoir_ct,
372-
context);
354+
ExpandReservoirUsingPrecedences(
355+
sum_of_positive_demand > reservoir_ct->reservoir().max_level(),
356+
sum_of_negative_demand < reservoir_ct->reservoir().min_level(),
357+
reservoir_ct, context);
373358
} else {
374359
context->UpdateRuleStats(
375360
"reservoir: skipped expansion due to variable demands");

ortools/sat/cp_model_expand_test.cc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,29 @@ TEST(ReservoirExpandTest, NoOptionalAndInitiallyFeasible) {
9292
EXPECT_EQ(27, solutions.size());
9393
}
9494

95+
TEST(ReservoirExpandTest, SimpleSemaphore) {
96+
const CpModelProto initial_model = ParseTestProto(R"pb(
97+
variables { domain: 0 domain: 10 }
98+
variables { domain: 0 domain: 10 }
99+
variables { domain: 0 domain: 1 }
100+
constraints {
101+
reservoir {
102+
max_level: 2
103+
time_exprs { vars: 0 coeffs: 1 }
104+
time_exprs { vars: 1 coeffs: 1 }
105+
active_literals: [ 2, 2 ]
106+
level_changes { offset: -1 }
107+
level_changes { offset: 1 }
108+
}
109+
}
110+
)pb");
111+
absl::btree_set<std::vector<int>> solutions;
112+
const CpSolverResponse response =
113+
SolveAndCheck(initial_model, "", &solutions);
114+
EXPECT_EQ(OPTIMAL, response.status());
115+
EXPECT_EQ(187, solutions.size());
116+
}
117+
95118
TEST(ReservoirExpandTest, GizaReport) {
96119
const CpModelProto initial_model = ParseTestProto(R"pb(
97120
variables { domain: 0 domain: 10 }

ortools/sat/cp_model_presolve.cc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,11 @@ bool CpModelPresolver::PresolveBoolAnd(ConstraintProto* ct) {
444444
return MarkConstraintAsFalse(ct);
445445
}
446446
if (context_->VariableIsUniqueAndRemovable(literal)) {
447+
// This is a "dual" reduction.
447448
changed = true;
449+
context_->UpdateRuleStats(
450+
"bool_and: setting unused literal in rhs to true");
451+
context_->UpdateLiteralSolutionHint(literal, true);
448452
if (!context_->SetLiteralToTrue(literal)) return true;
449453
continue;
450454
}
@@ -6879,8 +6883,8 @@ bool CpModelPresolver::PresolveReservoir(ConstraintProto* ct) {
68796883
(num_positives == 0 || num_negatives == 0)) {
68806884
// If all level_changes have the same sign, and if the initial state is
68816885
// always feasible, we do not care about the order, just the sum.
6882-
auto* const sum =
6883-
context_->working_model->add_constraints()->mutable_linear();
6886+
auto* const sum_ct = context_->working_model->add_constraints();
6887+
auto* const sum = sum_ct->mutable_linear();
68846888
int64_t fixed_contrib = 0;
68856889
for (int i = 0; i < proto.level_changes_size(); ++i) {
68866890
const int64_t demand = context_->FixedValue(proto.level_changes(i));
@@ -6898,6 +6902,7 @@ bool CpModelPresolver::PresolveReservoir(ConstraintProto* ct) {
68986902
}
68996903
sum->add_domain(proto.min_level() - fixed_contrib);
69006904
sum->add_domain(proto.max_level() - fixed_contrib);
6905+
CanonicalizeLinear(sum_ct);
69016906
context_->UpdateRuleStats("reservoir: converted to linear");
69026907
return RemoveConstraint(ct);
69036908
}

ortools/sat/cp_model_utils.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,20 @@ void AddLinearExpressionToLinearConstraint(const LinearExpressionProto& expr,
643643
}
644644
}
645645

646+
void AddWeightedLiteralToLinearConstraint(int lit, int64_t coeff,
647+
LinearConstraintProto* linear,
648+
int64_t* offset) {
649+
if (coeff == 0) return;
650+
if (RefIsPositive(lit)) {
651+
linear->add_vars(lit);
652+
linear->add_coeffs(coeff);
653+
} else {
654+
linear->add_vars(NegatedRef(lit));
655+
linear->add_coeffs(-coeff);
656+
*offset += coeff;
657+
}
658+
}
659+
646660
bool SafeAddLinearExpressionToLinearConstraint(
647661
const LinearExpressionProto& expr, int64_t coefficient,
648662
LinearConstraintProto* linear) {

ortools/sat/cp_model_utils.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ void AddLinearExpressionToLinearConstraint(const LinearExpressionProto& expr,
237237
int64_t coefficient,
238238
LinearConstraintProto* linear);
239239

240+
// Same as above, but with a single term (lit, coeff). Note that lit can be
241+
// negative. The offset is relative to the linear expression (and should be
242+
// negated when added to the rhs of the linear constraint proto).
243+
void AddWeightedLiteralToLinearConstraint(int lit, int64_t coeff,
244+
LinearConstraintProto* linear,
245+
int64_t* offset);
246+
240247
// Same method, but returns if the addition was possible without overflowing.
241248
bool SafeAddLinearExpressionToLinearConstraint(
242249
const LinearExpressionProto& expr, int64_t coefficient,

ortools/sat/parameters_validation.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ std::string ValidateParameters(const SatParameters& params) {
106106
TEST_IN_RANGE(interleave_batch_size, 0, kMaxReasonableParallelism);
107107
TEST_IN_RANGE(shared_tree_open_leaves_per_worker, 1,
108108
kMaxReasonableParallelism);
109+
TEST_IN_RANGE(shared_tree_balance_tolerance, 0,
110+
log2(kMaxReasonableParallelism));
109111

110112
// TODO(user): Consider using annotations directly in the proto for these
111113
// validation. It is however not open sourced.

ortools/sat/presolve_context.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2319,7 +2319,7 @@ int PresolveContext::GetOrCreateReifiedPrecedenceLiteral(
23192319
if (!LiteralIsTrue(active_i)) {
23202320
AddImplication(result, active_i);
23212321
}
2322-
if (!LiteralIsTrue(active_j)) {
2322+
if (!LiteralIsTrue(active_j) && active_i != active_j) {
23232323
AddImplication(result, active_j);
23242324
}
23252325

@@ -2341,7 +2341,7 @@ int PresolveContext::GetOrCreateReifiedPrecedenceLiteral(
23412341
if (!LiteralIsTrue(active_i)) {
23422342
greater->add_enforcement_literal(active_i);
23432343
}
2344-
if (!LiteralIsTrue(active_j)) {
2344+
if (!LiteralIsTrue(active_j) && active_i != active_j) {
23452345
greater->add_enforcement_literal(active_j);
23462346
}
23472347
CanonicalizeLinearConstraint(greater);

ortools/sat/presolve_context.h

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -603,14 +603,18 @@ class PresolveContext {
603603
hint_[var] == (RefIsPositive(lit) ? value : !value);
604604
}
605605

606+
// If the given literal is already hinted, updates its hint.
607+
// Otherwise do nothing.
606608
void UpdateLiteralSolutionHint(int lit, bool value) {
607-
UpdateSolutionHint(PositiveRef(lit), RefIsPositive(lit) ? value : !value);
609+
UpdateSolutionHint(PositiveRef(lit), RefIsPositive(lit) == value ? 1 : 0);
608610
}
609611

610-
// Updates the hint of an existing variable with an existing hint.
612+
// If the given variable is already hinted, updates its hint value.
613+
// Otherwise, do nothing.
611614
void UpdateSolutionHint(int var, int64_t value) {
612-
CHECK(hint_is_loaded_);
613-
CHECK(hint_has_value_[var]);
615+
DCHECK(RefIsPositive(var));
616+
if (!hint_is_loaded_) return;
617+
if (!hint_has_value_[var]) return;
614618
hint_[var] = value;
615619
}
616620

ortools/sat/sat_parameters.proto

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ option java_multiple_files = true;
2323
// Contains the definitions for all the sat algorithm parameters and their
2424
// default values.
2525
//
26-
// NEXT TAG: 305
26+
// NEXT TAG: 306
2727
message SatParameters {
2828
// In some context, like in a portfolio of search, it makes sense to name a
2929
// given parameters set for logging purpose.
@@ -1141,6 +1141,15 @@ message SatParameters {
11411141
optional SharedTreeSplitStrategy shared_tree_split_strategy = 239
11421142
[default = SPLIT_STRATEGY_AUTO];
11431143

1144+
// How much deeper compared to the ideal max depth of the tree is considered
1145+
// "balanced" enough to still accept a split. Without such a tolerance,
1146+
// sometimes the tree can only be split by a single worker, and they may not
1147+
// generate a split for some time. In contrast, with a tolerance of 1, at
1148+
// least half of all workers should be able to split the tree as soon as a
1149+
// split becomes required. This only has an effect on
1150+
// SPLIT_STRATEGY_BALANCED_TREE and SPLIT_STRATEGY_DISCREPANCY.
1151+
optional int32 shared_tree_balance_tolerance = 305 [default = 1];
1152+
11441153
// Whether we enumerate all solutions of a problem without objective. Note
11451154
// that setting this to true automatically disable some presolve reduction
11461155
// that can remove feasible solution. That is it has the same effect as

0 commit comments

Comments
 (0)