Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
linear_propagation.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <stdint.h>
17
18#include <algorithm>
19#include <functional>
20#include <limits>
21#include <string>
22#include <utility>
23#include <vector>
24
25#include "absl/base/log_severity.h"
26#include "absl/cleanup/cleanup.h"
27#include "absl/container/flat_hash_map.h"
28#include "absl/container/flat_hash_set.h"
29#include "absl/log/check.h"
30#include "absl/log/log.h"
31#include "absl/log/vlog_is_on.h"
32#include "absl/numeric/int128.h"
33#include "absl/strings/str_cat.h"
34#include "absl/types/span.h"
37#include "ortools/sat/integer.h"
39#include "ortools/sat/model.h"
43#include "ortools/sat/util.h"
44#include "ortools/util/bitset.h"
47
48namespace operations_research {
49namespace sat {
50
52 : trail_(model->GetOrCreate<Trail>()),
53 integer_trail_(model->GetOrCreate<IntegerTrail>()),
54 enforcement_propagator_(model->GetOrCreate<EnforcementPropagator>()),
55 enforcement_helper_(model->GetOrCreate<EnforcementHelper>()),
56 watcher_(model->GetOrCreate<GenericLiteralWatcher>()),
57 time_limit_(model->GetOrCreate<TimeLimit>()),
58 rev_int_repository_(model->GetOrCreate<RevIntRepository>()),
59 rev_integer_value_repository_(
60 model->GetOrCreate<RevIntegerValueRepository>()),
61 precedences_(model->GetOrCreate<EnforcedLinear2Bounds>()),
62 linear3_bounds_(model->GetOrCreate<Linear2BoundsFromLinear3>()),
63 random_(model->GetOrCreate<ModelRandomGenerator>()),
64 shared_stats_(model->GetOrCreate<SharedStatistics>()),
65 watcher_id_(watcher_->Register(this)),
66 order_(random_, time_limit_,
67 [this](int id) { return GetVariables(infos_[id]); }) {
68 // Note that we need this class always in sync.
69 integer_trail_->RegisterWatcher(&modified_vars_);
70 integer_trail_->RegisterReversibleClass(this);
71
72 // TODO(user): When we start to push too much (Cycle?) we should see what
73 // other propagator says before repropagating this one, system for call
74 // later?
75 watcher_->SetPropagatorPriority(watcher_id_, 0);
76}
77
79 if (!VLOG_IS_ON(1)) return;
80 if (shared_stats_ == nullptr) return;
81 std::vector<std::pair<std::string, int64_t>> stats;
82 stats.push_back({"linear_propag/num_pushes", num_pushes_});
83 stats.push_back(
84 {"linear_propag/num_enforcement_pushes", num_enforcement_pushes_});
85
86 stats.push_back({"linear_propag/num_cycles", num_cycles_});
87 stats.push_back({"linear_propag/num_failed_cycles", num_failed_cycles_});
88
89 stats.push_back({"linear_propag/num_short_reasons_", num_short_reasons_});
90 stats.push_back({"linear_propag/num_long_reasons_", num_long_reasons_});
91
92 stats.push_back({"linear_propag/num_scanned", num_scanned_});
93 stats.push_back({"linear_propag/num_explored_in_disassemble",
94 num_explored_in_disassemble_});
95 stats.push_back({"linear_propag/num_bool_aborts", num_bool_aborts_});
96 stats.push_back({"linear_propag/num_loop_aborts", num_loop_aborts_});
97 stats.push_back({"linear_propag/num_ignored", num_ignored_});
98 stats.push_back({"linear_propag/num_delayed", num_delayed_});
99 shared_stats_->AddStats(stats);
100}
101
103 if (level < previous_level_) {
104 order_.Clear();
105 modified_vars_.ResetAllToFalse();
106
107 // If the solver backtracked at any point, we invalidate all our queue
108 // and propagated_by information.
109 ClearPropagatedBy();
110 for (const int id : propagation_queue_) in_queue_.Clear(id);
111 propagation_queue_.clear();
112 for (int i = rev_unenforced_size_; i < unenforced_constraints_.size();
113 ++i) {
114 in_queue_.Clear(unenforced_constraints_[i]);
115 }
116 unenforced_constraints_.resize(rev_unenforced_size_);
117 } else if (level > previous_level_) {
118 rev_unenforced_size_ = unenforced_constraints_.size();
119 rev_int_repository_->SaveState(&rev_unenforced_size_);
120 }
121
122 // Tricky: if we aborted the current propagation because we pushed a Boolean,
123 // by default, the GenericLiteralWatcher will only call Propagate() again if
124 // one of the watched variable changed. With this, it is guaranteed to call
125 // it again if it wasn't in the queue already.
126 if (!propagation_queue_.empty() ||
127 !modified_vars_.PositionsSetAtLeastOnce().empty() || !order_.IsEmpty()) {
128 watcher_->CallOnNextPropagate(watcher_id_);
129 }
130
131 previous_level_ = level;
132}
133
134void LinearPropagator::SetPropagatedBy(IntegerVariable var, int id) {
135 int& ref_id = propagated_by_[var];
136 if (ref_id == id) return;
137
138 propagated_by_was_set_.Set(var);
139
140 DCHECK_GE(var, 0);
141 DCHECK_LT(var, propagated_by_.size());
142 if (ref_id != -1) {
143 DCHECK_GE(ref_id, 0);
144 DCHECK_LT(ref_id, id_to_propagation_count_.size());
145 id_to_propagation_count_[ref_id]--;
146 }
147 ref_id = id;
148 if (id != -1) id_to_propagation_count_[id]++;
149}
150
151void LinearPropagator::OnVariableChange(IntegerVariable var, IntegerValue lb,
152 int id) {
153 DCHECK_EQ(lb, integer_trail_->LowerBound(var));
154
155 // If no constraint use this var, we just ignore it.
156 const int size = var_to_constraint_ids_[var].size();
157 if (size == 0) return;
158
159 SetPropagatedBy(var, id);
160 order_.UpdateBound(var, lb);
161 Bitset64<int>::View in_queue = in_queue_.view();
162 time_limit_->AdvanceDeterministicTime(static_cast<double>(size) * 1e-9);
163 for (const int id : var_to_constraint_ids_[var]) {
164 if (in_queue[id]) continue;
165 in_queue.Set(id);
166 propagation_queue_.push_back(id);
167 }
168}
169
171 // Initial addition.
172 //
173 // We will clear modified_vars_ on exit since everything we propagate here
174 // is handled by PropagateOneConstraint().
175 for (const IntegerVariable var : modified_vars_.PositionsSetAtLeastOnce()) {
176 if (var >= var_to_constraint_ids_.size()) continue;
177 OnVariableChange(var, integer_trail_->LowerBound(var), -1);
178 }
179
180 // Cleanup.
181 num_terms_for_dtime_update_ = 0;
182 const auto cleanup = ::absl::MakeCleanup([this]() {
183 time_limit_->AdvanceDeterministicTime(
184 static_cast<double>(num_terms_for_dtime_update_) * 1e-9);
185 modified_vars_.ClearAndResize(integer_trail_->NumIntegerVariables());
186 });
187
188 // We abort this propagator as soon as a Boolean is propagated, so that we
189 // always finish the Boolean propagation first. This can happen when we push a
190 // bound that has associated Booleans or push enforcement to false. The idea
191 // is to resume from our current state when we are called again. Note however
192 // that we have to clear the propagated_by_ info has other propagator might
193 // have pushed the same variable further.
194 //
195 // TODO(user): More than the propagation speed, I think it is important to
196 // have proper explanation, so if A pushes B, but later on the queue we have C
197 // that push A that push B again, that might be bad? We can try to avoid this
198 // even further, by organizing the queue in passes:
199 // - Scan all relevant constraints, remember who pushes but DO NOT push yet!
200 // - If no cycle, do not pushes constraint whose slack will changes due to
201 // other pushes.
202 // - consider the new constraint that need to be scanned and repeat.
203 // I think it is okay to scan twice the constraints that push something in
204 // order to get better explanation. We tend to diverge from the class shortest
205 // path algo in this regard.
206 //
207 // TODO(user): If we push the idea further, can we first compute the fix point
208 // without pushing anything, then compute a good order of constraints for the
209 // explanations? what is tricky is that we might need to "scan" more than once
210 // a constraint I think. ex: Y, Z, T >=0
211 // - 2 * Y + Z + T <= 11 ==> Y <= 5, Z <= 11, T <= 11 (1)
212 // - Z + Y >= 6 ==> Z >= 1
213 // - (1) again to push T <= 10 and reach the propagation fixed point.
214 Bitset64<int>::View in_queue = in_queue_.view();
215 const bool push_affine_ub = push_affine_ub_for_binary_relations_ ||
216 trail_->CurrentDecisionLevel() == 0;
217 while (true) {
218 // We always process the whole queue in FIFO order.
219 // Note that the order really only matter for infeasible constraint so it
220 // shouldn't have a big impact.
221 const int saved_index = trail_->Index();
222 while (!propagation_queue_.empty()) {
223 const int id = propagation_queue_.front();
224 propagation_queue_.pop_front();
225 in_queue.Clear(id);
226 const auto [slack, num_to_push] = AnalyzeConstraint(id);
227 if (slack < 0) {
228 // This is either a conflict or an enforcement propagation.
229 // We do it right away.
230 if (!PropagateInfeasibleConstraint(id, slack)) return false;
231
232 // We abort after the first pushed boolean. We could abort later too,
233 // it is unclear what works best.
234 //
235 // TODO(user): Maybe we should "update" explanation if we have a shorter
236 // one to be less reliant on the propagation order.
237 if (trail_->Index() > saved_index) {
238 ++num_bool_aborts_;
239 return true;
240 }
241 } else if (num_to_push > 0) {
242 // Note that the last constraint added in to_propagate_ should never
243 // be "skipped" and have any variable marked as var_will_change_.
244 const auto vars = GetVariables(infos_[id]);
245 const auto coeffs = GetCoeffs(infos_[id]);
246 for (int i = 0; i < num_to_push; ++i) {
247 const IntegerVariable var = vars[i];
248 const IntegerValue coeff = coeffs[i];
249 const IntegerValue div = slack / coeff;
250 const IntegerValue new_ub = integer_trail_->LowerBound(var) + div;
251 order_.Register(id, NegationOf(var), -new_ub);
252 }
253 }
254
255 // Look at linear3 and update our "linear2 affine upper bound". If we are
256 // here it means the constraint was in the queue, and its slack changed,
257 // so it might lead to stronger affine ub.
258 //
259 // TODO(user): This can be costly for no reason if we keep updating the
260 // bound for variable appearing in a single linear3. On another hand it is
261 // O(1) compared to what this class already do. Profile will tell if it is
262 // worth it. Maybe we can only share LinearExpression2 that we might look
263 // up.
264 //
265 // TODO(user): This only look at non-enforced linear3. We could look at
266 // constraint whose enforcement or other variables are fixed at level
267 // zero, but it is trickier. It could be done if we add a "batch clean up"
268 // to this class that runs at level zero, and reduce constraints
269 // accordingly.
270 const ConstraintInfo& info = infos_[id];
271 if (push_affine_ub && info.initial_size == 3 && info.enf_id == -1) {
272 // A constraint A + B + C <= rhs can lead to up to 3 relations...
273 const auto vars = GetVariables(info);
274 const auto coeffs = GetCoeffs(info);
275
276 // We don't "push" relation A + B <= ub if A or B is fixed, because
277 // the variable bound of the non-fixed A or B should just be as-strong
278 // as what can be inferred from the binary relation.
279 if (info.rev_size == 2) {
281 expr.vars[0] = vars[0];
282 expr.vars[1] = vars[1];
283 expr.coeffs[0] = coeffs[0];
284 expr.coeffs[1] = coeffs[1];
285
286 // The fixed variable is always at index 2.
287 // The rev_rhs was updated to: initial_rhs - lb(vars[2]) * coeffs[2].
288 const IntegerValue initial_rhs =
289 info.rev_rhs + coeffs[2] * integer_trail_->LowerBound(vars[2]);
290 linear3_bounds_->AddAffineUpperBound(
291 expr, AffineExpression(vars[2], -coeffs[2], initial_rhs));
292 } else if (info.rev_size == 3) {
293 for (int i = 0; i < 3; ++i) {
295 const int a = (i + 1) % 3;
296 const int b = (i + 2) % 3;
297 expr.vars[0] = vars[a];
298 expr.vars[1] = vars[b];
299 expr.coeffs[0] = coeffs[a];
300 expr.coeffs[1] = coeffs[b];
301 linear3_bounds_->AddAffineUpperBound(
302 expr, AffineExpression(vars[i], -coeffs[i], info.rev_rhs));
303 }
304 }
305 }
306 }
307
308 const int next_id = order_.NextId();
309 if (next_id == -1) break;
310
311 // We can probably save the AnalyzeConstraint() cost, but then we only do
312 // that when the constraint propagate, and the context might have change
313 // since we computed it above.
314 if (!PropagateOneConstraint(next_id)) return false;
315
316 // TODO(user): This do not seems always good, especially since we pushed
317 // Boolean with a really small explanation, maybe we want to push more of
318 // these rather than go back to pure-binary propagation.
319 if (trail_->Index() > saved_index) {
320 ++num_bool_aborts_;
321 return true;
322 }
323 }
324 return true;
325}
326
327// Adds a new constraint to the propagator.
329 absl::Span<const Literal> enforcement_literals,
330 absl::Span<const IntegerVariable> vars,
331 absl::Span<const IntegerValue> coeffs, IntegerValue upper_bound) {
332 if (vars.empty()) return true;
333 if (trail_->CurrentDecisionLevel() == 0) {
334 for (const Literal l : enforcement_literals) {
335 if (trail_->Assignment().LiteralIsFalse(l)) return true;
336 }
337 }
338
339 // Make sure max_variations_ is of correct size.
340 // Note that we also have a hard limit of 1 << 29 on the size.
341 CHECK_LT(vars.size(), 1 << 29);
342 if (vars.size() > max_variations_.size()) {
343 max_variations_.resize(vars.size(), 0);
344 buffer_of_ones_.resize(vars.size(), IntegerValue(1));
345 }
346
347 // Initialize constraint data.
348 CHECK_EQ(vars.size(), coeffs.size());
349 const int id = infos_.size();
350 {
351 ConstraintInfo info;
352 info.all_coeffs_are_one = false;
353 info.start = variables_buffer_.size();
354 info.initial_size = vars.size();
355 info.rev_rhs = upper_bound;
356 info.rev_size = vars.size();
357 infos_.push_back(std::move(info));
358 initial_rhs_.push_back(upper_bound);
359 }
360
361 id_to_propagation_count_.push_back(0);
362 variables_buffer_.insert(variables_buffer_.end(), vars.begin(), vars.end());
363 coeffs_buffer_.insert(coeffs_buffer_.end(), coeffs.begin(), coeffs.end());
364 CanonicalizeConstraint(id);
365
366 bool all_at_one = true;
367 for (const IntegerValue coeff : GetCoeffs(infos_.back())) {
368 if (coeff != 1) {
369 all_at_one = false;
370 break;
371 }
372 }
373 if (all_at_one) {
374 // TODO(user): we still waste the space in coeffs_buffer_ so that the
375 // start are aligned with the variables_buffer_.
376 infos_.back().all_coeffs_are_one = true;
377 }
378
379 // Initialize watchers.
380 // Initially we want everything to be propagated at least once.
381 in_queue_.resize(in_queue_.size() + 1);
382
383 if (!enforcement_literals.empty()) {
384 infos_.back().enf_status =
385 static_cast<int>(EnforcementStatus::CANNOT_PROPAGATE);
386 infos_.back().enf_id = enforcement_propagator_->Register(
387 enforcement_literals,
388 [this, id](EnforcementId enf_id, EnforcementStatus status) {
389 infos_[id].enf_status = static_cast<int>(status);
390 // TODO(user): With some care, when we cannot propagate or the
391 // constraint is not enforced, we could leave in_queue_[] at true but
392 // not put the constraint in the queue.
395 AddToQueueIfNeeded(id);
396 watcher_->CallOnNextPropagate(watcher_id_);
397 }
398
399 // When a conditional precedence becomes enforced, add it.
400 // Note that we only look at relation that were a "precedence" from
401 // the start, note the one currently of size 2 if we ignore fixed
402 // variables.
403 if (status == EnforcementStatus::IS_ENFORCED) {
404 const auto info = infos_[id];
405 if (info.initial_size == 2) {
406 const auto vars = GetVariables(info);
407 const auto coeffs = GetCoeffs(info);
408
409 // WARNING, subtle: this callback is called from the enforcement
410 // propagator, which can run before running the propagation of the
411 // IntegerTrail. Since it is in IntegerTrail::Propagate() that we
412 // call SetLevel() on the reversible classes,
413 // EnforcedLinear2Bounds might be at the wrong level.
414 // TODO(user): find a cleaner solution
415 precedences_->SetLevelToTrail();
416
417 precedences_->PushConditionalRelation(
418 enforcement_propagator_->GetEnforcementLiterals(enf_id),
419 LinearExpression2(vars[0], vars[1], coeffs[0], coeffs[1]),
420 initial_rhs_[id]);
421 }
422 }
423 });
424 } else {
425 // TODO(user): Shall we register root level precedence from here rather than
426 // separately?
427 AddToQueueIfNeeded(id);
428 infos_.back().enf_id = -1;
429 infos_.back().enf_status = static_cast<int>(EnforcementStatus::IS_ENFORCED);
430 }
431
432 order_.Resize(var_to_constraint_ids_.size(), in_queue_.size());
433 for (const IntegerVariable var : GetVariables(infos_[id])) {
434 // Transposed graph to know which constraint to wake up.
435 if (var >= var_to_constraint_ids_.size()) {
436 // We need both the var entry and its negation to be allocated.
437 const int size = std::max(var, NegationOf(var)).value() + 1;
438 var_to_constraint_ids_.resize(size);
439 propagated_by_.resize(size, -1);
440 propagated_by_was_set_.Resize(IntegerVariable(size));
441 is_watched_.resize(size, false);
442
443 order_.Resize(size, in_queue_.size());
444 }
445
446 // TODO(user): Shall we decide on some ordering here? maybe big coeff first
447 // so that we get the largest change in slack? the idea being to propagate
448 // large change first in case of cycles.
449 var_to_constraint_ids_[var].push_back(id);
450
451 // We need to be registered to the watcher so Propagate() is called at
452 // the proper priority. But then we rely on modified_vars_.
453 if (!is_watched_[var]) {
454 is_watched_[var] = true;
455 watcher_->WatchLowerBound(var, watcher_id_);
456 }
457 }
458
459 // Propagate this new constraint.
460 // TODO(user): Do we want to do that?
461 //
462 // Tricky: we cannot just call PropagateOneConstraint(id) if some variable
463 // bounds where modified since the last time we propagated, otherwise we
464 // might wrongly detect cycles for instance.
465 return Propagate();
466}
467
468absl::Span<IntegerValue> LinearPropagator::GetCoeffs(
469 const ConstraintInfo& info) {
470 if (info.all_coeffs_are_one) {
471 return absl::MakeSpan(&buffer_of_ones_[0], info.initial_size);
472 }
473 return absl::MakeSpan(&coeffs_buffer_[info.start], info.initial_size);
474}
475
476absl::Span<IntegerVariable> LinearPropagator::GetVariables(
477 const ConstraintInfo& info) {
478 return absl::MakeSpan(&variables_buffer_[info.start], info.initial_size);
479}
480
481void LinearPropagator::CanonicalizeConstraint(int id) {
482 const ConstraintInfo& info = infos_[id];
483 const auto coeffs = GetCoeffs(info);
484 const auto vars = GetVariables(info);
485 for (int i = 0; i < vars.size(); ++i) {
486 if (coeffs[i] < 0) {
487 coeffs[i] = -coeffs[i];
488 vars[i] = NegationOf(vars[i]);
489 }
490 }
491
492 // Note that we DO NOT support having both var and NegationOf(var) in a
493 // constraint, that would break the algo.
494 if (DEBUG_MODE) {
495 absl::flat_hash_set<IntegerVariable> no_dup;
496 for (const IntegerVariable var : GetVariables(info)) {
497 auto [_, inserted] = no_dup.insert(PositiveVariable(var));
498 CHECK(inserted);
499 }
500 }
501}
502
503// TODO(user): template everything for the case info.all_coeffs_are_one ?
504std::pair<IntegerValue, int> LinearPropagator::AnalyzeConstraint(int id) {
505 ++num_scanned_;
506
507 // Skip constraint not enforced or that cannot propagate if false.
508 ConstraintInfo& info = infos_[id];
509 const EnforcementStatus enf_status = EnforcementStatus(info.enf_status);
510 if (DEBUG_MODE && enforcement_propagator_->PropagationIsDone(*trail_)) {
511 const EnforcementStatus debug_status =
512 enforcement_propagator_->DebugStatus(info.enf_id);
513 if (enf_status != debug_status) {
514 if (enf_status == EnforcementStatus::CANNOT_PROPAGATE &&
515 debug_status == EnforcementStatus::IS_FALSE) {
516 // This case might happen because in our two watched literals scheme,
517 // we might watch two unassigned literal without knowing another one is
518 // already false.
519 } else {
520 LOG(FATAL) << "Enforcement status not up to date: " << enf_status
521 << " vs debug: " << debug_status;
522 }
523 }
524 }
525
526 if (enf_status == EnforcementStatus::IS_FALSE ||
528 DCHECK(!in_queue_[id]);
529 if (enf_status == EnforcementStatus::IS_FALSE) {
530 // We mark this constraint as in the queue but will never inspect it
531 // again until we backtrack over this time.
532 in_queue_.Set(id);
533 unenforced_constraints_.push_back(id);
534 }
535 ++num_ignored_;
536 return {0, 0};
537 }
538
539 // Compute the slack and max_variations_ of each variables.
540 // We also filter out fixed variables in a reversible way.
541 IntegerValue implied_lb(0);
542 const auto vars = GetVariables(info);
543 IntegerValue max_variation(0);
544 bool first_change = true;
545 num_terms_for_dtime_update_ += info.rev_size;
546 IntegerValue* max_variations = max_variations_.data();
547 const IntegerValue* lower_bounds = integer_trail_->LowerBoundsData();
548 if (info.all_coeffs_are_one) {
549 // TODO(user): Avoid duplication?
550 for (int i = 0; i < info.rev_size;) {
551 const IntegerVariable var = vars[i];
552 const IntegerValue lb = lower_bounds[var.value()];
553 const IntegerValue diff = -lower_bounds[NegationOf(var).value()] - lb;
554 if (diff == 0) {
555 if (first_change) {
556 // Note that we can save at most one state per fixed var. Also at
557 // level zero we don't save anything.
558 rev_int_repository_->SaveState(&info.rev_size);
559 rev_integer_value_repository_->SaveState(&info.rev_rhs);
560 first_change = false;
561 }
562 info.rev_size--;
563 std::swap(vars[i], vars[info.rev_size]);
564 info.rev_rhs -= lb;
565 } else {
566 implied_lb += lb;
567 max_variations[i] = diff;
568 max_variation = std::max(max_variation, diff);
569 ++i;
570 }
571 }
572 } else {
573 const auto coeffs = GetCoeffs(info);
574 for (int i = 0; i < info.rev_size;) {
575 const IntegerVariable var = vars[i];
576 const IntegerValue coeff = coeffs[i];
577 const IntegerValue lb = lower_bounds[var.value()];
578 const IntegerValue diff = -lower_bounds[NegationOf(var).value()] - lb;
579 if (diff == 0) {
580 if (first_change) {
581 // Note that we can save at most one state per fixed var. Also at
582 // level zero we don't save anything.
583 rev_int_repository_->SaveState(&info.rev_size);
584 rev_integer_value_repository_->SaveState(&info.rev_rhs);
585 first_change = false;
586 }
587 info.rev_size--;
588 std::swap(vars[i], vars[info.rev_size]);
589 std::swap(coeffs[i], coeffs[info.rev_size]);
590 info.rev_rhs -= coeff * lb;
591 } else {
592 implied_lb += coeff * lb;
593 max_variations[i] = diff * coeff;
594 max_variation = std::max(max_variation, max_variations[i]);
595 ++i;
596 }
597 }
598 }
599
600 // What we call slack here is the "room" between the implied_lb and the rhs.
601 // Note that we use slack in other context in this file too.
602 const IntegerValue slack = info.rev_rhs - implied_lb;
603
604 // Negative slack means the constraint is false.
605 // Note that if max_variation > slack, we are sure to propagate something
606 // except if the constraint is enforced and the slack is non-negative.
607 if (slack < 0 || max_variation <= slack) return {slack, 0};
608 if (enf_status == EnforcementStatus::IS_ENFORCED) {
609 // Swap the variable(s) that will be pushed at the beginning.
610 int num_to_push = 0;
611 const auto coeffs = GetCoeffs(info);
612 for (int i = 0; i < info.rev_size; ++i) {
613 if (max_variations[i] <= slack) continue;
614 std::swap(vars[i], vars[num_to_push]);
615 std::swap(coeffs[i], coeffs[num_to_push]);
616 ++num_to_push;
617 }
618 return {slack, num_to_push};
619 }
620 return {slack, 0};
621}
622
623bool LinearPropagator::PropagateInfeasibleConstraint(int id,
624 IntegerValue slack) {
625 DCHECK_LT(slack, 0);
626 const ConstraintInfo& info = infos_[id];
627 const auto vars = GetVariables(info);
628 const auto coeffs = GetCoeffs(info);
629
630 // Fill integer reason.
631 integer_reason_.clear();
632 reason_coeffs_.clear();
633 for (int i = 0; i < info.initial_size; ++i) {
634 const IntegerVariable var = vars[i];
635 if (!integer_trail_->VariableLowerBoundIsFromLevelZero(var)) {
636 integer_reason_.push_back(integer_trail_->LowerBoundAsLiteral(var));
637 reason_coeffs_.push_back(coeffs[i]);
638 }
639 }
640
641 // Relax it.
642 integer_trail_->RelaxLinearReason(-slack - 1, reason_coeffs_,
643 &integer_reason_);
644 ++num_enforcement_pushes_;
645 return enforcement_helper_->PropagateWhenFalse(info.enf_id, {},
646 integer_reason_);
647}
648
649void LinearPropagator::Explain(int id, IntegerLiteral /*to_explain*/,
650 IntegerReason* reason) {
651 const ConstraintInfo& info = infos_[id];
653 enforcement_propagator_->GetEnforcementLiterals(info.enf_id);
654 reason->vars = GetVariables(info);
655 reason->coeffs = GetCoeffs(info);
656}
657
658bool LinearPropagator::PropagateOneConstraint(int id) {
659 const auto [slack, num_to_push] = AnalyzeConstraint(id);
660 if (slack < 0) return PropagateInfeasibleConstraint(id, slack);
661 if (num_to_push == 0) return true;
662
663 DCHECK_GT(num_to_push, 0);
664 DCHECK_GE(slack, 0);
665 const ConstraintInfo& info = infos_[id];
666 const auto vars = GetVariables(info);
667 const auto coeffs = GetCoeffs(info);
668
669 // We can only propagate more if all the enforcement literals are true.
670 // But this should have been checked by SkipConstraint().
671 CHECK_EQ(info.enf_status, static_cast<int>(EnforcementStatus::IS_ENFORCED));
672
673 // We can look for disasemble before the actual push.
674 // This should lead to slighly better reason.
675 // Explore the subtree and detect cycles greedily.
676 // Also postpone some propagation.
677 if (!DisassembleSubtree(id, num_to_push)) {
678 return false;
679 }
680
681 // The lower bound of all the variables except one can be used to update the
682 // upper bound of the last one.
683 int num_pushed = 0;
684 for (int i = 0; i < num_to_push; ++i) {
685 if (!order_.VarShouldBePushedById(NegationOf(vars[i]), id)) {
686 ++num_delayed_;
687 continue;
688 }
689
690 // TODO(user): If the new ub fall into an hole of the variable, we can
691 // actually relax the reason more by computing a better slack.
692 ++num_pushes_;
693 const IntegerVariable var = vars[i];
694 const IntegerValue coeff = coeffs[i];
695 const IntegerValue div = slack / coeff;
696 const IntegerValue new_ub = integer_trail_->LowerBound(var) + div;
697 const IntegerValue propagation_slack = (div + 1) * coeff - slack - 1;
698 if (!integer_trail_->EnqueueWithLazyReason(
699 IntegerLiteral::LowerOrEqual(var, new_ub), id, propagation_slack,
700 this)) {
701 return false;
702 }
703
704 // Add to the queue all touched constraint.
705 const IntegerValue actual_ub = integer_trail_->UpperBound(var);
706 const IntegerVariable next_var = NegationOf(var);
707 if (actual_ub < new_ub) {
708 // Was pushed further due to hole. We clear it.
709 OnVariableChange(next_var, -actual_ub, -1);
710 } else if (actual_ub == new_ub) {
711 OnVariableChange(next_var, -actual_ub, id);
712
713 // We reorder them first.
714 std::swap(vars[i], vars[num_pushed]);
715 std::swap(coeffs[i], coeffs[num_pushed]);
716 ++num_pushed;
717 } else {
718 // The bound was not pushed because we think we are in a propagation loop.
719 ++num_loop_aborts_;
720 }
721 }
722
723 return true;
724}
725
726std::string LinearPropagator::ConstraintDebugString(int id) {
727 std::string result;
728 const ConstraintInfo& info = infos_[id];
729 const auto coeffs = GetCoeffs(info);
730 const auto vars = GetVariables(info);
731 IntegerValue implied_lb(0);
732 IntegerValue rhs_correction(0);
733 for (int i = 0; i < info.initial_size; ++i) {
734 const IntegerValue term = coeffs[i] * integer_trail_->LowerBound(vars[i]);
735 if (i >= info.rev_size) {
736 rhs_correction += term;
737 }
738 implied_lb += term;
739 absl::StrAppend(&result, " +", coeffs[i].value(), "*X", vars[i].value());
740 }
741 const IntegerValue original_rhs = info.rev_rhs + rhs_correction;
742 absl::StrAppend(&result, " <= ", original_rhs.value(),
743 " slack=", original_rhs.value() - implied_lb.value());
744 absl::StrAppend(&result, " enf=", info.enf_status);
745 return result;
746}
747
748bool LinearPropagator::ReportConflictingCycle() {
749 // Often, all coefficients of the variable involved in the cycle are the same
750 // and if we sum all constraint, we get an infeasible one. If this is the
751 // case, we simplify the reason.
752 //
753 // TODO(user): We could relax if the coefficient of the sum do not overflow.
754 // TODO(user): Sum constraints with eventual factor in more cases.
755 {
756 literal_reason_.clear();
757 integer_reason_.clear();
758 absl::int128 rhs_sum = 0;
759 absl::flat_hash_map<IntegerVariable, absl::int128> map_sum;
760 for (const auto [id, next_var, increase] : disassemble_branch_) {
761 const ConstraintInfo& info = infos_[id];
762 enforcement_propagator_->AddEnforcementReason(info.enf_id,
763 &literal_reason_);
764 const auto coeffs = GetCoeffs(info);
765 const auto vars = GetVariables(info);
766 IntegerValue rhs_correction(0);
767 for (int i = 0; i < info.initial_size; ++i) {
768 if (i >= info.rev_size) {
769 rhs_correction += coeffs[i] * integer_trail_->LowerBound(vars[i]);
770 }
771 if (VariableIsPositive(vars[i])) {
772 map_sum[vars[i]] += coeffs[i].value();
773 } else {
774 map_sum[PositiveVariable(vars[i])] -= coeffs[i].value();
775 }
776 }
777 rhs_sum += (info.rev_rhs + rhs_correction).value();
778 }
779
780 // We shouldn't have overflow since each component do not overflow an
781 // int64_t and we sum a small amount of them.
782 absl::int128 implied_lb = 0;
783 for (const auto [var, coeff] : map_sum) {
784 if (coeff > 0) {
785 if (!integer_trail_->VariableLowerBoundIsFromLevelZero(var)) {
786 integer_reason_.push_back(integer_trail_->LowerBoundAsLiteral(var));
787 }
788 implied_lb +=
789 coeff * absl::int128{integer_trail_->LowerBound(var).value()};
790 } else if (coeff < 0) {
791 if (!integer_trail_->VariableLowerBoundIsFromLevelZero(
792 NegationOf(var))) {
793 integer_reason_.push_back(integer_trail_->UpperBoundAsLiteral(var));
794 }
795 implied_lb +=
796 coeff * absl::int128{integer_trail_->UpperBound(var).value()};
797 }
798 }
799 if (implied_lb > rhs_sum) {
800 // We sort for determinism.
801 std::sort(integer_reason_.begin(), integer_reason_.end(),
802 [](const IntegerLiteral& a, const IntegerLiteral& b) {
803 return a.var < b.var;
804 });
805
806 // Relax the linear reason if everything fit on an int64_t.
807 const absl::int128 limit{std::numeric_limits<int64_t>::max()};
808 const absl::int128 slack = implied_lb - rhs_sum;
809 if (slack > 1) {
810 reason_coeffs_.clear();
811 bool abort = false;
812 for (const IntegerLiteral i_lit : integer_reason_) {
813 absl::int128 c = map_sum.at(PositiveVariable(i_lit.var));
814 if (c < 0) c = -c; // No std::abs() for int128.
815 if (c >= limit) {
816 abort = true;
817 break;
818 }
819 reason_coeffs_.push_back(static_cast<int64_t>(c));
820 }
821 if (!abort) {
822 const IntegerValue slack64(
823 static_cast<int64_t>(std::min(limit, slack)));
824 integer_trail_->RelaxLinearReason(slack64 - 1, reason_coeffs_,
825 &integer_reason_);
826 }
827 }
828
829 ++num_short_reasons_;
830 VLOG(2) << "Simplified " << integer_reason_.size() << " slack "
831 << implied_lb - rhs_sum;
832 return integer_trail_->ReportConflict(literal_reason_, integer_reason_);
833 }
834 }
835
836 // For the complex reason, we just use the bound of every variable.
837 // We do some basic simplification for the variable involved in the cycle.
838 //
839 // TODO(user): Can we simplify more?
840 VLOG(2) << "Cycle";
841 literal_reason_.clear();
842 integer_reason_.clear();
843 IntegerVariable previous_var = kNoIntegerVariable;
844 for (const auto [id, next_var, increase] : disassemble_branch_) {
845 const ConstraintInfo& info = infos_[id];
846 enforcement_propagator_->AddEnforcementReason(info.enf_id,
847 &literal_reason_);
848 for (const IntegerVariable var : GetVariables(infos_[id])) {
849 // The lower bound of this variable is implied by the previous constraint,
850 // so we do not need to include it.
851 if (var == previous_var) continue;
852
853 // We do not need the lower bound of var to propagate its upper bound.
854 if (var == NegationOf(next_var)) continue;
855
856 if (!integer_trail_->VariableLowerBoundIsFromLevelZero(var)) {
857 integer_reason_.push_back(integer_trail_->LowerBoundAsLiteral(var));
858 }
859 }
860 previous_var = next_var;
861
862 VLOG(2) << next_var << " [" << integer_trail_->LowerBound(next_var) << ","
863 << integer_trail_->UpperBound(next_var)
864 << "] : " << ConstraintDebugString(id);
865 }
866 ++num_long_reasons_;
867 return integer_trail_->ReportConflict(literal_reason_, integer_reason_);
868}
869
870std::pair<IntegerValue, IntegerValue> LinearPropagator::GetCycleCoefficients(
871 int id, IntegerVariable var, IntegerVariable next_var) {
872 const ConstraintInfo& info = infos_[id];
873 const auto coeffs = GetCoeffs(info);
874 const auto vars = GetVariables(info);
875 IntegerValue var_coeff(0);
876 IntegerValue next_coeff(0);
877 for (int i = 0; i < info.initial_size; ++i) {
878 if (vars[i] == var) var_coeff = coeffs[i];
879 if (vars[i] == NegationOf(next_var)) next_coeff = coeffs[i];
880 }
881 DCHECK_NE(var_coeff, 0);
882 DCHECK_NE(next_coeff, 0);
883 return {var_coeff, next_coeff};
884}
885
886// Note that if there is a loop in the propagated_by_ graph, it must be
887// from root_id -> root_var, because each time we add an edge, we do
888// disassemble.
889//
890// TODO(user): If one of the var coeff is > previous slack we push an id again,
891// we can stop early with a conflict by propagating the ids in sequence.
892//
893// TODO(user): Revisit the algo, no point exploring twice the same var, also
894// the queue reordering heuristic might not be the best.
895bool LinearPropagator::DisassembleSubtree(int root_id, int num_tight) {
896 // The variable was just pushed, we explore the set of variable that will
897 // be pushed further due to this push. Basically, if a constraint propagated
898 // before and its slack will reduce due to the push, then any previously
899 // propagated variable with a coefficient NOT GREATER than the one of the
900 // variable reducing the slack will be pushed further.
901 disassemble_queue_.clear();
902 disassemble_branch_.clear();
903 {
904 const ConstraintInfo& info = infos_[root_id];
905 const auto vars = GetVariables(info);
906 for (int i = 0; i < num_tight; ++i) {
907 disassemble_queue_.push_back({root_id, NegationOf(vars[i]), 1});
908 }
909 }
910
911 // Note that all var should be unique since there is only one propagated_by_
912 // for each one. And each time we explore an id, we disassemble the tree.
913 absl::Span<int> id_to_count = absl::MakeSpan(id_to_propagation_count_);
914 while (!disassemble_queue_.empty()) {
915 const auto [prev_id, var, increase] = disassemble_queue_.back();
916 if (!disassemble_branch_.empty() &&
917 disassemble_branch_.back().id == prev_id &&
918 disassemble_branch_.back().var == var) {
919 disassemble_branch_.pop_back();
920 disassemble_queue_.pop_back();
921 continue;
922 }
923
924 disassemble_branch_.push_back({prev_id, var, increase});
925
926 time_limit_->AdvanceDeterministicTime(
927 static_cast<double>(var_to_constraint_ids_[var].size()) * 1e-9);
928 for (const int id : var_to_constraint_ids_[var]) {
929 if (id == root_id) {
930 // TODO(user): Check previous slack vs var coeff?
931 // TODO(user): Make sure there are none or detect cycle not going back
932 // to the root.
933 CHECK(!disassemble_branch_.empty());
934
935 // This is a corner case in which there is actually no cycle.
936 const auto [test_id, root_var, var_increase] = disassemble_branch_[0];
937 CHECK_EQ(test_id, root_id);
938 CHECK_NE(var, root_var);
939 if (var == NegationOf(root_var)) continue;
940
941 // Simple case, we have a cycle var -> root_var -> ... -> var where
942 // all coefficient are non-increasing.
943 const auto [var_coeff, root_coeff] =
944 GetCycleCoefficients(id, var, root_var);
945 if (CapProdI(var_increase, var_coeff) >= root_coeff) {
946 ++num_cycles_;
947 return ReportConflictingCycle();
948 }
949
950 // We don't want to continue the search from root_id.
951 // TODO(user): We could still try the simple reason, it might be a
952 // conflict.
953 ++num_failed_cycles_;
954 continue;
955 }
956
957 if (id_to_count[id] == 0) continue; // Didn't push or was desassembled.
958
959 // The constraint pushed some variable. Identify which ones will be pushed
960 // further. Disassemble the whole info since we are about to propagate
961 // this constraint again. Any pushed variable must be before the rev_size.
962 const ConstraintInfo& info = infos_[id];
963 const auto coeffs = GetCoeffs(info);
964 const auto vars = GetVariables(info);
965 IntegerValue var_coeff(0);
966 disassemble_candidates_.clear();
967 ++num_explored_in_disassemble_;
968 time_limit_->AdvanceDeterministicTime(static_cast<double>(info.rev_size) *
969 1e-9);
970 for (int i = 0; i < info.rev_size; ++i) {
971 if (vars[i] == var) {
972 var_coeff = coeffs[i];
973 continue;
974 }
975 const IntegerVariable next_var = NegationOf(vars[i]);
976 if (propagated_by_[next_var] == id) {
977 disassemble_candidates_.push_back({next_var, coeffs[i]});
978
979 // We will propagate var again later, so clear all this for now.
980 propagated_by_[next_var] = -1;
981 id_to_count[id]--;
982 }
983 }
984
985 for (const auto [next_var, next_var_coeff] : disassemble_candidates_) {
986 // If var was pushed by increase, next_var is pushed by
987 // (var_coeff * increase) / next_var_coeff.
988 //
989 // Note that it is okay to underevalute the increase in case of
990 // overflow.
991 const IntegerValue next_increase =
992 FloorRatio(CapProdI(var_coeff, increase), next_var_coeff);
993 if (next_increase > 0) {
994 disassemble_queue_.push_back({id, next_var, next_increase});
995
996 // We know this will push later, so we register it with a sentinel
997 // value so that it do not block any earlier propagation. Hopefully,
998 // adding this "dependency" should help find a better propagation
999 // order.
1000 order_.Register(id, next_var, kMinIntegerValue);
1001 }
1002 }
1003 }
1004 }
1005
1006 return true;
1007}
1008
1009void LinearPropagator::AddToQueueIfNeeded(int id) {
1010 DCHECK_LT(id, in_queue_.size());
1011 DCHECK_LT(id, infos_.size());
1012
1013 if (in_queue_[id]) return;
1014 in_queue_.Set(id);
1015 propagation_queue_.push_back(id);
1016}
1017
1018void LinearPropagator::ClearPropagatedBy() {
1019 // To be sparse, we use the fact that each node with a parent must be in
1020 // modified_vars_.
1021 for (const IntegerVariable var :
1022 propagated_by_was_set_.PositionsSetAtLeastOnce()) {
1023 int& id = propagated_by_[var];
1024 if (id != -1) --id_to_propagation_count_[id];
1025 propagated_by_[var] = -1;
1026 }
1027 propagated_by_was_set_.ClearAndResize(propagated_by_was_set_.size());
1028 DCHECK(std::all_of(propagated_by_.begin(), propagated_by_.end(),
1029 [](int id) { return id == -1; }));
1030 DCHECK(std::all_of(id_to_propagation_count_.begin(),
1031 id_to_propagation_count_.end(),
1032 [](int count) { return count == 0; }));
1033}
1034
1035} // namespace sat
1036} // namespace operations_research
void Set(IntegerType index)
Definition bitset.h:878
void AdvanceDeterministicTime(double deterministic_duration)
Definition time_limit.h:186
void UpdateBound(IntegerVariable var, IntegerValue lb)
bool VarShouldBePushedById(IntegerVariable var, int id)
IntegerValue LowerBound(IntegerVariable i) const
Definition integer.h:1537
ABSL_MUST_USE_RESULT bool EnqueueWithLazyReason(IntegerLiteral i_lit, int id, IntegerValue propagation_slack, LazyReasonInterface *explainer)
Definition integer.h:795
bool AddConstraint(absl::Span< const Literal > enforcement_literals, absl::Span< const IntegerVariable > vars, absl::Span< const IntegerValue > coeffs, IntegerValue upper_bound)
void Explain(int id, IntegerLiteral to_explain, IntegerReason *reason) final
IntegerValue FloorRatio(IntegerValue dividend, IntegerValue positive_divisor)
std::vector< IntegerVariable > NegationOf(absl::Span< const IntegerVariable > vars)
Definition integer.cc:52
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
const IntegerVariable kNoIntegerVariable(-1)
IntegerVariable PositiveVariable(IntegerVariable i)
bool VariableIsPositive(IntegerVariable i)
IntegerValue CapProdI(IntegerValue a, IntegerValue b)
OR-Tools root namespace.
const bool DEBUG_MODE
Definition radix_sort.h:266
static IntegerLiteral LowerOrEqual(IntegerVariable i, IntegerValue bound)
absl::Span< const IntegerValue > coeffs
Definition integer.h:457
absl::Span< const Literal > boolean_literals_at_true
Definition integer.h:440
absl::Span< const IntegerVariable > vars
Definition integer.h:456