Google OR-Tools v9.9
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
var_domination.cc
Go to the documentation of this file.
1// Copyright 2010-2024 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 <stddef.h>
17
18#include <algorithm>
19#include <cstdint>
20#include <cstdlib>
21#include <limits>
22#include <memory>
23#include <string>
24#include <utility>
25#include <vector>
26
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/meta/type_traits.h"
31#include "absl/strings/str_cat.h"
32#include "absl/types/span.h"
34#include "ortools/base/hash.h"
38#include "ortools/sat/cp_model.pb.h"
40#include "ortools/sat/integer.h"
43#include "ortools/sat/util.h"
48
49namespace operations_research {
50namespace sat {
51
52void VarDomination::Reset(int num_variables) {
53 phase_ = 0;
54 num_vars_with_negation_ = 2 * num_variables;
55 partition_ =
56 std::make_unique<SimpleDynamicPartition>(num_vars_with_negation_);
57
58 can_freely_decrease_.assign(num_vars_with_negation_, true);
59
60 shared_buffer_.clear();
61 has_initial_candidates_.assign(num_vars_with_negation_, false);
62 initial_candidates_.assign(num_vars_with_negation_, IntegerVariableSpan());
63
64 buffer_.clear();
65 dominating_vars_.assign(num_vars_with_negation_, IntegerVariableSpan());
66
67 ct_index_for_signature_ = 0;
68 block_down_signatures_.assign(num_vars_with_negation_, 0);
69}
70
71void VarDomination::RefinePartition(std::vector<int>* vars) {
72 if (vars->empty()) return;
73 partition_->Refine(*vars);
74 for (int& var : *vars) {
75 const IntegerVariable wrapped(var);
76 can_freely_decrease_[wrapped] = false;
77 can_freely_decrease_[NegationOf(wrapped)] = false;
78 var = NegationOf(wrapped).value();
79 }
80 partition_->Refine(*vars);
81}
82
83void VarDomination::CanOnlyDominateEachOther(absl::Span<const int> refs) {
84 if (phase_ != 0) return;
85 tmp_vars_.clear();
86 for (const int ref : refs) {
87 tmp_vars_.push_back(RefToIntegerVariable(ref).value());
88 }
89 RefinePartition(&tmp_vars_);
90 tmp_vars_.clear();
91}
92
93void VarDomination::ActivityShouldNotChange(absl::Span<const int> refs,
94 absl::Span<const int64_t> coeffs) {
95 if (phase_ != 0) return;
96 FillTempRanks(/*reverse_references=*/false, /*enforcements=*/{}, refs,
97 coeffs);
98 tmp_vars_.clear();
99 for (int i = 0; i < tmp_ranks_.size(); ++i) {
100 if (i > 0 && tmp_ranks_[i].rank != tmp_ranks_[i - 1].rank) {
101 RefinePartition(&tmp_vars_);
102 tmp_vars_.clear();
103 }
104 tmp_vars_.push_back(tmp_ranks_[i].var.value());
105 }
106 RefinePartition(&tmp_vars_);
107 tmp_vars_.clear();
108}
109
110// This correspond to a lower bounded constraint.
111void VarDomination::ProcessTempRanks() {
112 if (phase_ == 0) {
113 // We actually "split" tmp_ranks_ according to the current partition and
114 // process each resulting list independently for a faster algo.
115 ++ct_index_for_signature_;
116 for (IntegerVariableWithRank& entry : tmp_ranks_) {
117 can_freely_decrease_[entry.var] = false;
118 block_down_signatures_[entry.var] |= uint64_t{1}
119 << (ct_index_for_signature_ % 64);
120 entry.part = partition_->PartOf(entry.var.value());
121 }
122 std::stable_sort(
123 tmp_ranks_.begin(), tmp_ranks_.end(),
124 [](const IntegerVariableWithRank& a, const IntegerVariableWithRank& b) {
125 return a.part < b.part;
126 });
127 int start = 0;
128 for (int i = 1; i < tmp_ranks_.size(); ++i) {
129 if (tmp_ranks_[i].part != tmp_ranks_[start].part) {
130 Initialize({&tmp_ranks_[start], static_cast<size_t>(i - start)});
131 start = i;
132 }
133 }
134 if (start < tmp_ranks_.size()) {
135 Initialize({&tmp_ranks_[start], tmp_ranks_.size() - start});
136 }
137 } else if (phase_ == 1) {
138 FilterUsingTempRanks();
139 } else {
140 // This is only used for debugging, and we shouldn't reach here in prod.
141 CheckUsingTempRanks();
142 }
143}
144
146 absl::Span<const int> enforcements, absl::Span<const int> refs,
147 absl::Span<const int64_t> coeffs) {
148 FillTempRanks(/*reverse_references=*/false, enforcements, refs, coeffs);
149 ProcessTempRanks();
150}
151
153 absl::Span<const int> enforcements, absl::Span<const int> refs,
154 absl::Span<const int64_t> coeffs) {
155 FillTempRanks(/*reverse_references=*/true, enforcements, refs, coeffs);
156 ProcessTempRanks();
157}
158
159void VarDomination::MakeRankEqualToStartOfPart(
160 absl::Span<IntegerVariableWithRank> span) {
161 const int size = span.size();
162 int start = 0;
163 int previous_value = 0;
164 for (int i = 0; i < size; ++i) {
165 const int64_t value = span[i].rank;
166 if (value != previous_value) {
167 previous_value = value;
168 start = i;
169 }
170 span[i].rank = start;
171 }
172}
173
174void VarDomination::Initialize(absl::Span<IntegerVariableWithRank> span) {
175 // The rank can be wrong and need to be recomputed because of how we split
176 // tmp_ranks_ into spans.
177 MakeRankEqualToStartOfPart(span);
178
179 // We made sure all the variable belongs to the same part.
180 if (DEBUG_MODE) {
181 if (span.empty()) return;
182 const int part = partition_->PartOf(span[0].var.value());
183 for (int i = 1; i < span.size(); ++i) {
184 CHECK_EQ(part, partition_->PartOf(span[i].var.value()));
185 }
186 }
187
188 const int future_start = shared_buffer_.size();
189 int first_start = -1;
190 const int size = span.size();
191 for (int i = 0; i < size; ++i) {
192 const IntegerVariableWithRank entry = span[i];
193 const int num_candidates = size - entry.rank;
194
195 // Ignore if we already have a shorter list.
196 if (has_initial_candidates_[entry.var]) {
197 if (num_candidates >= initial_candidates_[entry.var].size) continue;
198 }
199
200 if (first_start == -1) first_start = entry.rank;
201 has_initial_candidates_[entry.var] = true;
202 initial_candidates_[entry.var] = {
203 future_start - first_start + static_cast<int>(entry.rank),
204 num_candidates};
205 }
206
207 // Only store what is necessary.
208 // Note that since we share, we will never store more than the problem size.
209 if (first_start == -1) return;
210 for (int i = first_start; i < size; ++i) {
211 shared_buffer_.push_back(span[i].var);
212 }
213}
214
215// TODO(user): Use more heuristics to not miss as much dominance relation when
216// we crop initial lists.
218 CHECK_EQ(phase_, 0);
219 phase_ = 1;
220
221 // Some initial lists ar too long and will be cropped to this size.
222 // We will handle them slightly differently.
223 //
224 // TODO(user): Tune the initial size, 50 might be a bit large, since our
225 // complexity is borned by this number times the number of entries in the
226 // constraints. Still we should in most situation be a lot lower than that.
227 const int kMaxInitialSize = 50;
228 std::vector<IntegerVariable> cropped_vars;
229 absl::StrongVector<IntegerVariable, bool> is_cropped(num_vars_with_negation_,
230 false);
231
232 // Fill the initial domination candidates.
233 int non_cropped_size = 0;
234 std::vector<IntegerVariable> partition_data;
235 const std::vector<absl::Span<const IntegerVariable>> elements_by_part =
236 partition_->GetParts(&partition_data);
237 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
238 if (can_freely_decrease_[var]) continue;
239
240 const int part = partition_->PartOf(var.value());
241 const int start = buffer_.size();
242 const uint64_t var_sig = block_down_signatures_[var];
243 const uint64_t not_var_sig = block_down_signatures_[NegationOf(var)];
244 absl::Span<const IntegerVariable> to_scan =
245 has_initial_candidates_[var] ? InitialDominatingCandidates(var)
246 : elements_by_part[part];
247
248 // Two modes, either we scan the full list, or a small subset of it.
249 // Not that low variable indices should appear first, so it is better not
250 // to randomize.
251 int new_size = 0;
252 if (to_scan.size() <= 1'000) {
253 for (const IntegerVariable x : to_scan) {
254 if (var_sig & ~block_down_signatures_[x]) continue; // !included.
255 if (block_down_signatures_[NegationOf(x)] & ~not_var_sig) continue;
256 if (PositiveVariable(x) == PositiveVariable(var)) continue;
257 if (can_freely_decrease_[NegationOf(x)]) continue;
258 ++new_size;
259 buffer_.push_back(x);
260 if (new_size >= kMaxInitialSize) {
261 is_cropped[var] = true;
262 cropped_vars.push_back(var);
263 }
264 }
265 } else {
266 is_cropped[var] = true;
267 cropped_vars.push_back(var);
268 for (int i = 0; i < 200; ++i) {
269 const IntegerVariable x = to_scan[i];
270 if (var_sig & ~block_down_signatures_[x]) continue; // !included.
271 if (block_down_signatures_[NegationOf(x)] & ~not_var_sig) continue;
272 if (PositiveVariable(x) == PositiveVariable(var)) continue;
273 if (can_freely_decrease_[NegationOf(x)]) continue;
274 ++new_size;
275 buffer_.push_back(x);
276 if (new_size >= kMaxInitialSize) break;
277 }
278 }
279
280 if (!is_cropped[var]) non_cropped_size += new_size;
281 dominating_vars_[var] = {start, new_size};
282 }
283
284 // Heuristic: To try not to remove domination relations corresponding to short
285 // lists during transposition (see EndSecondPhase()), we fill the cropped list
286 // with the transpose of the short list relations. This helps finding more
287 // relation in the presence of cropped lists.
288 //
289 // Compute how many extra space we need for transposed values.
290 // Note that it cannot be more than twice.
291 int total_extra_space = 0;
292 absl::StrongVector<IntegerVariable, int> extra_space(num_vars_with_negation_,
293 0);
294 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
295 for (const IntegerVariable dom : DominatingVariables(var)) {
296 if (!is_cropped[NegationOf(dom)]) continue;
297 ++total_extra_space;
298 extra_space[NegationOf(dom)]++;
299 }
300 }
301
302 // Copy into a new buffer.
303 int copy_index = 0;
304 other_buffer_.resize(buffer_.size() + total_extra_space);
305 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
306 IntegerVariableSpan& s = dominating_vars_[var];
307 for (int i = 0; i < s.size; ++i) {
308 other_buffer_[copy_index + i] = buffer_[s.start + i];
309 }
310 s.start = copy_index;
311 copy_index += s.size + extra_space[var];
312 extra_space[var] = s.size; // Used below.
313 }
314 DCHECK_EQ(copy_index, other_buffer_.size());
315 std::swap(buffer_, other_buffer_);
316 gtl::STLClearObject(&other_buffer_);
317
318 // Fill the free spaces with transposed values.
319 // But do not use new values !
320 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
321 const int start = dominating_vars_[var].start;
322 const int size = extra_space[var];
323 for (int i = 0; i < size; ++i) {
324 const IntegerVariable dom = buffer_[start + i];
325 if (!is_cropped[NegationOf(dom)]) continue;
326 IntegerVariableSpan& s = dominating_vars_[NegationOf(dom)];
327 buffer_[s.start + s.size++] = NegationOf(var);
328 }
329 }
330
331 // Remove any duplicates.
332 //
333 // TODO(user): Maybe we should do that with all lists in case the
334 // input function are called with duplicates too.
335 for (const IntegerVariable var : cropped_vars) {
336 DCHECK(is_cropped[var]);
337 IntegerVariableSpan& s = dominating_vars_[var];
338 if (s.size == 0) continue;
339 IntegerVariable* pt = &buffer_[s.start];
340 std::sort(pt, pt + s.size);
341 const auto end = std::unique(pt, pt + s.size);
342 s.size = end - pt;
343 }
344
345 // We no longer need the first phase memory.
346 VLOG(1) << "Num initial list that where cropped: "
347 << FormatCounter(cropped_vars.size());
348 VLOG(1) << "Shared buffer size: " << FormatCounter(shared_buffer_.size());
349 VLOG(1) << "Non-cropped buffer size: " << FormatCounter(non_cropped_size);
350 VLOG(1) << "Buffer size: " << FormatCounter(buffer_.size());
351 gtl::STLClearObject(&initial_candidates_);
352 gtl::STLClearObject(&shared_buffer_);
353
354 // A second phase is only needed if there are some potential dominance
355 // relations.
356 return !buffer_.empty();
357}
358
360 CHECK_EQ(phase_, 1);
361 phase_ = 2;
362
363 // Perform intersection with transpose.
364 shared_buffer_.clear();
365 initial_candidates_.assign(num_vars_with_negation_, IntegerVariableSpan());
366
367 // Pass 1: count.
368 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
369 for (const IntegerVariable dom : DominatingVariables(var)) {
370 ++initial_candidates_[NegationOf(dom)].size;
371 }
372 }
373
374 // Pass 2: compute starts.
375 int start = 0;
376 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
377 initial_candidates_[var].start = start;
378 start += initial_candidates_[var].size;
379 initial_candidates_[var].size = 0;
380 }
381 shared_buffer_.resize(start);
382
383 // Pass 3: transpose.
384 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
385 for (const IntegerVariable dom : DominatingVariables(var)) {
386 IntegerVariableSpan& span = initial_candidates_[NegationOf(dom)];
387 shared_buffer_[span.start + span.size++] = NegationOf(var);
388 }
389 }
390
391 // Pass 4: intersect.
392 int num_removed = 0;
393 tmp_var_to_rank_.resize(num_vars_with_negation_, -1);
394 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
395 for (const IntegerVariable dom : InitialDominatingCandidates(var)) {
396 tmp_var_to_rank_[dom] = 1;
397 }
398
399 int new_size = 0;
400 IntegerVariableSpan& span = dominating_vars_[var];
401 for (const IntegerVariable dom : DominatingVariables(var)) {
402 if (tmp_var_to_rank_[dom] != 1) {
403 ++num_removed;
404 continue;
405 }
406 buffer_[span.start + new_size++] = dom;
407 }
408 span.size = new_size;
409
410 for (const IntegerVariable dom : InitialDominatingCandidates(var)) {
411 tmp_var_to_rank_[dom] = -1;
412 }
413 }
414
415 VLOG(1) << "Transpose removed " << FormatCounter(num_removed);
416 gtl::STLClearObject(&initial_candidates_);
417 gtl::STLClearObject(&shared_buffer_);
418}
419
420void VarDomination::FillTempRanks(bool reverse_references,
421 absl::Span<const int> enforcements,
422 absl::Span<const int> refs,
423 absl::Span<const int64_t> coeffs) {
424 tmp_ranks_.clear();
425 if (coeffs.empty()) {
426 // Simple case: all coefficients are assumed to be the same.
427 for (const int ref : refs) {
428 const IntegerVariable var =
429 RefToIntegerVariable(reverse_references ? NegatedRef(ref) : ref);
430 tmp_ranks_.push_back({var, 0, 0});
431 }
432 } else {
433 // Complex case: different coefficients.
434 for (int i = 0; i < refs.size(); ++i) {
435 if (coeffs[i] == 0) continue;
436 const IntegerVariable var = RefToIntegerVariable(
437 reverse_references ? NegatedRef(refs[i]) : refs[i]);
438 if (coeffs[i] > 0) {
439 tmp_ranks_.push_back({var, 0, coeffs[i]});
440 } else {
441 tmp_ranks_.push_back({NegationOf(var), 0, -coeffs[i]});
442 }
443 }
444 std::sort(tmp_ranks_.begin(), tmp_ranks_.end());
445 MakeRankEqualToStartOfPart({&tmp_ranks_[0], tmp_ranks_.size()});
446 }
447
448 // Add the enforcement last with a new rank. We add their negation since
449 // we want the activity to not decrease, and we want to allow any
450 // enforcement-- to dominate a variable in the constraint.
451 const int enforcement_rank = tmp_ranks_.size();
452 for (const int ref : enforcements) {
453 tmp_ranks_.push_back(
454 {RefToIntegerVariable(NegatedRef(ref)), 0, enforcement_rank});
455 }
456}
457
458// We take the intersection of the current dominating candidate with the
459// restriction imposed by the current content of tmp_ranks_.
460void VarDomination::FilterUsingTempRanks() {
461 // Expand ranks in temp vector.
462 tmp_var_to_rank_.resize(num_vars_with_negation_, -1);
463 for (const IntegerVariableWithRank entry : tmp_ranks_) {
464 tmp_var_to_rank_[entry.var] = entry.rank;
465 }
466
467 // The activity of the variable in tmp_rank must not decrease.
468 for (const IntegerVariableWithRank entry : tmp_ranks_) {
469 // The only variables that can be paired with a var-- in the constraints are
470 // the var++ in the constraints with the same rank or higher.
471 //
472 // Note that we only filter the var-- domination lists here, we do not
473 // remove the var-- appearing in all the lists corresponding to wrong var++.
474 // This is left to the transpose operation in EndSecondPhase().
475 {
476 IntegerVariableSpan& span = dominating_vars_[entry.var];
477 if (span.size == 0) continue;
478 int new_size = 0;
479 for (const IntegerVariable candidate : DominatingVariables(entry.var)) {
480 if (tmp_var_to_rank_[candidate] < entry.rank) continue;
481 buffer_[span.start + new_size++] = candidate;
482 }
483 span.size = new_size;
484 }
485 }
486
487 // Reset temporary vector to all -1.
488 for (const IntegerVariableWithRank entry : tmp_ranks_) {
489 tmp_var_to_rank_[entry.var] = -1;
490 }
491}
492
493// Slow: This is for debugging only.
494void VarDomination::CheckUsingTempRanks() {
495 tmp_var_to_rank_.resize(num_vars_with_negation_, -1);
496 for (const IntegerVariableWithRank entry : tmp_ranks_) {
497 tmp_var_to_rank_[entry.var] = entry.rank;
498 }
499
500 // The activity of the variable in tmp_rank must not decrease.
501 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
502 const int var_rank = tmp_var_to_rank_[var];
503 const int negated_var_rank = tmp_var_to_rank_[NegationOf(var)];
504 for (const IntegerVariable dom : DominatingVariables(var)) {
505 CHECK(!can_freely_decrease_[NegationOf(dom)]);
506
507 // Doing X--, Y++ is compatible if the rank[X] <= rank[Y]. But we also
508 // need to check if doing Not(Y)-- is compatible with Not(X)++.
509 CHECK_LE(var_rank, tmp_var_to_rank_[dom]);
510 CHECK_LE(tmp_var_to_rank_[NegationOf(dom)], negated_var_rank);
511 }
512 }
513
514 for (const IntegerVariableWithRank entry : tmp_ranks_) {
515 tmp_var_to_rank_[entry.var] = -1;
516 }
517}
518
522
523bool VarDomination::CanFreelyDecrease(IntegerVariable var) const {
524 return can_freely_decrease_[var];
525}
526
527absl::Span<const IntegerVariable> VarDomination::InitialDominatingCandidates(
528 IntegerVariable var) const {
529 const IntegerVariableSpan span = initial_candidates_[var];
530 if (span.size == 0) return absl::Span<const IntegerVariable>();
531 return absl::Span<const IntegerVariable>(&shared_buffer_[span.start],
532 span.size);
533}
534
535absl::Span<const IntegerVariable> VarDomination::DominatingVariables(
536 int ref) const {
538}
539
540absl::Span<const IntegerVariable> VarDomination::DominatingVariables(
541 IntegerVariable var) const {
542 const IntegerVariableSpan span = dominating_vars_[var];
543 if (span.size == 0) return absl::Span<const IntegerVariable>();
544 return absl::Span<const IntegerVariable>(&buffer_[span.start], span.size);
545}
546
547std::string VarDomination::DominationDebugString(IntegerVariable var) const {
548 const int ref = IntegerVariableToRef(var);
549 std::string result =
550 absl::StrCat(PositiveRef(ref), RefIsPositive(ref) ? "--" : "++", " : ");
551 for (const IntegerVariable dom : DominatingVariables(var)) {
552 const int dom_ref = IntegerVariableToRef(dom);
553 absl::StrAppend(&result, PositiveRef(dom_ref),
554 RefIsPositive(dom_ref) ? "++" : "--", " ");
555 }
556 return result;
557}
558
559// TODO(user): No need to set locking_ct_index_[var] if num_locks_[var] > 1
560void DualBoundStrengthening::CannotDecrease(absl::Span<const int> refs,
561 int ct_index) {
562 // Optim: We cache pointers to avoid refetching them in the loop.
563 IntegerValue* bounds = can_freely_decrease_until_.data();
564 int* locks = num_locks_.data();
565 int* locking_index = locking_ct_index_.data();
566 for (const int ref : refs) {
567 const int var = RefToIntegerVariable(ref).value();
569 locks[var]++;
570 locking_index[var] = ct_index;
571 }
572}
573
574void DualBoundStrengthening::CannotIncrease(absl::Span<const int> refs,
575 int ct_index) {
576 // Optim: We cache pointers to avoid refetching them in the loop.
577 IntegerValue* bounds = can_freely_decrease_until_.data();
578 int* locks = num_locks_.data();
579 int* locking_index = locking_ct_index_.data();
580 for (const int ref : refs) {
581 const int negated_var = NegationOf(RefToIntegerVariable(ref)).value();
582 bounds[negated_var] = kMaxIntegerValue;
583 locks[negated_var]++;
584 locking_index[negated_var] = ct_index;
585 }
586}
587
588void DualBoundStrengthening::CannotMove(absl::Span<const int> refs,
589 int ct_index) {
590 // Optim: We cache pointers to avoid refetching them in the loop.
591 IntegerValue* bounds = can_freely_decrease_until_.data();
592 int* locks = num_locks_.data();
593 int* locking_index = locking_ct_index_.data();
594 for (const int ref : refs) {
595 const IntegerVariable var = RefToIntegerVariable(ref);
596 const IntegerVariable minus_var = NegationOf(var);
597 bounds[var.value()] = kMaxIntegerValue;
598 bounds[minus_var.value()] = kMaxIntegerValue;
599 locks[var.value()]++;
600 locks[minus_var.value()]++;
601 locking_index[var.value()] = ct_index;
602 locking_index[minus_var.value()] = ct_index;
603 }
604}
605
606template <typename LinearProto>
608 bool is_objective, const PresolveContext& context,
609 const LinearProto& linear, int64_t min_activity, int64_t max_activity,
610 int ct_index) {
611 const int64_t lb_limit = linear.domain(linear.domain_size() - 2);
612 const int64_t ub_limit = linear.domain(1);
613 const int num_terms = linear.vars_size();
614 for (int i = 0; i < num_terms; ++i) {
615 int ref = linear.vars(i);
616 int64_t coeff = linear.coeffs(i);
617 if (coeff < 0) {
618 ref = NegatedRef(ref);
619 coeff = -coeff;
620 }
621
622 const int64_t min_term = coeff * context.MinOf(ref);
623 const int64_t max_term = coeff * context.MaxOf(ref);
624 const int64_t term_diff = max_term - min_term;
625 const IntegerVariable var = RefToIntegerVariable(ref);
626
627 // lb side.
628 if (min_activity < lb_limit) {
629 num_locks_[var]++;
630 locking_ct_index_[var] = ct_index;
631 if (min_activity + term_diff < lb_limit) {
632 can_freely_decrease_until_[var] = kMaxIntegerValue;
633 } else {
634 const IntegerValue slack(lb_limit - min_activity);
635 const IntegerValue var_diff =
636 CeilRatio(IntegerValue(slack), IntegerValue(coeff));
637 can_freely_decrease_until_[var] =
638 std::max(can_freely_decrease_until_[var],
639 IntegerValue(context.MinOf(ref)) + var_diff);
640 }
641 }
642
643 if (is_objective) {
644 // We never want to increase the objective value. Note that if the
645 // objective is lower bounded, we checked that on the lb side above.
646 num_locks_[NegationOf(var)]++;
647 can_freely_decrease_until_[NegationOf(var)] = kMaxIntegerValue;
648 continue;
649 }
650
651 // ub side.
652 if (max_activity > ub_limit) {
653 num_locks_[NegationOf(var)]++;
654 locking_ct_index_[NegationOf(var)] = ct_index;
655 if (max_activity - term_diff > ub_limit) {
656 can_freely_decrease_until_[NegationOf(var)] = kMaxIntegerValue;
657 } else {
658 const IntegerValue slack(max_activity - ub_limit);
659 const IntegerValue var_diff =
660 CeilRatio(IntegerValue(slack), IntegerValue(coeff));
661 can_freely_decrease_until_[NegationOf(var)] =
662 std::max(can_freely_decrease_until_[NegationOf(var)],
663 -IntegerValue(context.MaxOf(ref)) + var_diff);
664 }
665 }
666 }
667}
668
669namespace {
670
671// This is used to detect if two linear constraint are equivalent if the literal
672// ref is mapped to another value. We fill a vector that will only be equal
673// to another such vector if the two constraint differ only there.
674void TransformLinearWithSpecialBoolean(const ConstraintProto& ct, int ref,
675 std::vector<int64_t>* output) {
676 DCHECK_EQ(ct.constraint_case(), ConstraintProto::kLinear);
677 output->clear();
678
679 // Deal with enforcement.
680 // We only detect NegatedRef() here.
681 if (!ct.enforcement_literal().empty()) {
682 output->push_back(ct.enforcement_literal().size());
683 for (const int literal : ct.enforcement_literal()) {
684 if (literal == NegatedRef(ref)) {
685 output->push_back(std::numeric_limits<int32_t>::max()); // Sentinel
686 } else {
687 output->push_back(literal);
688 }
689 }
690 }
691
692 // Deal with linear part.
693 // We look for both literal and not(literal) here.
694 int64_t offset = 0;
695 output->push_back(ct.linear().vars().size());
696 for (int i = 0; i < ct.linear().vars().size(); ++i) {
697 const int v = ct.linear().vars(i);
698 const int64_t c = ct.linear().coeffs(i);
699 if (v == ref) {
700 output->push_back(std::numeric_limits<int32_t>::max()); // Sentinel
701 output->push_back(c);
702 } else if (v == NegatedRef(ref)) {
703 // c * v = -c * (1 - v) + c
704 output->push_back(std::numeric_limits<int32_t>::max()); // Sentinel
705 output->push_back(-c);
706 offset += c;
707 } else {
708 output->push_back(v);
709 output->push_back(c);
710 }
711 }
712
713 // Domain.
714 for (const int64_t value : ct.linear().domain()) {
715 output->push_back(CapSub(value, offset));
716 }
717}
718
719} // namespace
720
722 num_deleted_constraints_ = 0;
723 const CpModelProto& cp_model = *context->working_model;
724 const int num_vars = cp_model.variables_size();
725 int64_t num_fixed_vars = 0;
726 for (int var = 0; var < num_vars; ++var) {
727 if (context->IsFixed(var)) continue;
728
729 // Fix to lb?
730 const int64_t lb = context->MinOf(var);
731 const int64_t ub_limit = std::max(lb, CanFreelyDecreaseUntil(var));
732 if (ub_limit == lb) {
733 ++num_fixed_vars;
734 CHECK(context->IntersectDomainWith(var, Domain(lb)));
735 continue;
736 }
737
738 // Fix to ub?
739 const int64_t ub = context->MaxOf(var);
740 const int64_t lb_limit =
741 std::min(ub, -CanFreelyDecreaseUntil(NegatedRef(var)));
742 if (lb_limit == ub) {
743 ++num_fixed_vars;
744 CHECK(context->IntersectDomainWith(var, Domain(ub)));
745 continue;
746 }
747
748 // Here we can fix to any value in [ub_limit, lb_limit] that is compatible
749 // with the current domain. We prefer zero or the lowest possible magnitude.
750 if (lb_limit > ub_limit) {
751 const Domain domain =
752 context->DomainOf(var).IntersectionWith(Domain(ub_limit, lb_limit));
753 if (!domain.IsEmpty()) {
754 int64_t value = domain.Contains(0) ? 0 : domain.Min();
755 if (value != 0) {
756 for (const int64_t bound : domain.FlattenedIntervals()) {
757 if (std::abs(bound) < std::abs(value)) value = bound;
758 }
759 }
760 ++num_fixed_vars;
761 context->UpdateRuleStats("dual: fix variable with multiple choices");
762 CHECK(context->IntersectDomainWith(var, Domain(value)));
763 continue;
764 }
765 }
766
767 // Here we can reduce the domain, but we must be careful when the domain
768 // has holes.
769 if (lb_limit > lb || ub_limit < ub) {
770 const int64_t new_ub =
771 ub_limit < ub
772 ? context->DomainOf(var)
773 .IntersectionWith(
774 Domain(ub_limit, std::numeric_limits<int64_t>::max()))
775 .Min()
776 : ub;
777 const int64_t new_lb =
778 lb_limit > lb
779 ? context->DomainOf(var)
780 .IntersectionWith(
781 Domain(std::numeric_limits<int64_t>::min(), lb_limit))
782 .Max()
783 : lb;
784 context->UpdateRuleStats("dual: reduced domain");
785 CHECK(context->IntersectDomainWith(var, Domain(new_lb, new_ub)));
786 }
787 }
788 if (num_fixed_vars > 0) {
789 context->UpdateRuleStats("dual: fix variable", num_fixed_vars);
790 }
791
792 // For detecting near-duplicate constraint that can be made equivalent.
793 // hash -> (ct_index, modified ref).
794 absl::flat_hash_set<int> equiv_ct_index_set;
795 absl::flat_hash_map<uint64_t, std::pair<int, int>> equiv_modified_constraints;
796 std::vector<int64_t> temp_data;
797 std::vector<int64_t> other_temp_data;
798 std::string s;
799
800 // If there is only one blocking constraint, we can simplify the problem in
801 // a few situation.
802 //
803 // TODO(user): Cover all the cases.
804 std::vector<bool> processed(num_vars, false);
805 int64_t num_bool_in_near_duplicate_ct = 0;
806 for (IntegerVariable var(0); var < num_locks_.size(); ++var) {
808 const int positive_ref = PositiveRef(ref);
809 if (processed[positive_ref]) continue;
810 if (context->IsFixed(positive_ref)) continue;
811 if (context->VariableIsNotUsedAnymore(positive_ref)) continue;
812 if (context->VariableWasRemoved(positive_ref)) continue;
813
814 if (num_locks_[var] != 1) continue;
815 if (locking_ct_index_[var] == -1) {
816 context->UpdateRuleStats(
817 "TODO dual: only one unspecified blocking constraint?");
818 continue;
819 }
820
821 const int ct_index = locking_ct_index_[var];
822 const ConstraintProto& ct = context->working_model->constraints(ct_index);
823 if (ct.constraint_case() == ConstraintProto::CONSTRAINT_NOT_SET) {
824 // TODO(user): Fix variable right away rather than waiting for next call.
825 continue;
826 }
827 if (ct.constraint_case() == ConstraintProto::kAtMostOne) {
828 context->UpdateRuleStats("TODO dual: tighten at most one");
829 continue;
830 }
831
832 if (ct.constraint_case() != ConstraintProto::kBoolAnd) {
833 // If we have an enforcement literal then we can always add the
834 // implication "not enforced" => var at its lower bound.
835 // If we also had enforced => fixed var, then var is in affine relation
836 // with the enforced literal and we can remove one variable.
837 //
838 // TODO(user): We can also deal with more than one enforcement.
839 if (ct.enforcement_literal().size() == 1 &&
840 PositiveRef(ct.enforcement_literal(0)) != positive_ref) {
841 const int enf = ct.enforcement_literal(0);
842 const int64_t bound = RefIsPositive(ref) ? context->MinOf(positive_ref)
843 : context->MaxOf(positive_ref);
844 const Domain implied =
845 context->DomainOf(positive_ref)
847 context->deductions.ImpliedDomain(enf, positive_ref));
848 if (implied.IsEmpty()) {
849 context->UpdateRuleStats("dual: fix variable");
850 if (!context->SetLiteralToFalse(enf)) return false;
851 if (!context->IntersectDomainWith(positive_ref, Domain(bound))) {
852 return false;
853 }
854 continue;
855 }
856 if (implied.IsFixed()) {
857 // Corner case.
858 if (implied.FixedValue() == bound) {
859 context->UpdateRuleStats("dual: fix variable");
860 if (!context->IntersectDomainWith(positive_ref, implied)) {
861 return false;
862 }
863 continue;
864 }
865
866 // Note(user): If we have enforced => var fixed, we could actually
867 // just have removed var from the constraint it was implied by
868 // another constraint. If not, because of the new affine relation we
869 // could remove it right away.
870 processed[PositiveRef(enf)] = true;
871 processed[positive_ref] = true;
872 context->UpdateRuleStats("dual: affine relation");
873 if (RefIsPositive(enf)) {
874 // positive_ref = enf * implied + (1 - enf) * bound.
875 if (!context->StoreAffineRelation(
876 positive_ref, enf, implied.FixedValue() - bound, bound)) {
877 return false;
878 }
879 } else {
880 // positive_ref = (1 - enf) * implied + enf * bound.
881 if (!context->StoreAffineRelation(positive_ref, PositiveRef(enf),
882 bound - implied.FixedValue(),
883 implied.FixedValue())) {
884 return false;
885 }
886 }
887 continue;
888 }
889
890 if (context->CanBeUsedAsLiteral(positive_ref)) {
891 // If we have a literal, we always add the implication.
892 // This seems like a good thing to do.
893 processed[PositiveRef(enf)] = true;
894 processed[positive_ref] = true;
895 context->UpdateRuleStats("dual: add implication");
896 context->AddImplication(NegatedRef(enf), NegatedRef(ref));
897 context->UpdateNewConstraintsVariableUsage();
898 continue;
899 }
900
901 // We can add an implication not_enforced => var to its bound ?
902 context->UpdateRuleStats("TODO dual: add implied bound");
903 }
904
905 // We can make enf equivalent to the constraint instead of just =>. This
906 // seems useful since internally we always use fully reified encoding.
907 if (ct.constraint_case() == ConstraintProto::kLinear &&
908 ct.linear().vars().size() == 1 &&
909 ct.enforcement_literal().size() == 1 &&
910 ct.enforcement_literal(0) == NegatedRef(ref)) {
911 const int var = ct.linear().vars(0);
912 const Domain var_domain = context->DomainOf(var);
913 const Domain rhs = ReadDomainFromProto(ct.linear())
914 .InverseMultiplicationBy(ct.linear().coeffs(0))
915 .IntersectionWith(var_domain);
916 if (rhs.IsEmpty()) {
917 context->UpdateRuleStats("linear1: infeasible");
918 if (!context->SetLiteralToFalse(ct.enforcement_literal(0))) {
919 return false;
920 }
921 processed[PositiveRef(ref)] = true;
922 processed[PositiveRef(var)] = true;
923 context->working_model->mutable_constraints(ct_index)->Clear();
924 context->UpdateConstraintVariableUsage(ct_index);
925 continue;
926 }
927 if (rhs == var_domain) {
928 context->UpdateRuleStats("linear1: always true");
929 processed[PositiveRef(ref)] = true;
930 processed[PositiveRef(var)] = true;
931 context->working_model->mutable_constraints(ct_index)->Clear();
932 context->UpdateConstraintVariableUsage(ct_index);
933 continue;
934 }
935
936 const Domain complement = rhs.Complement().IntersectionWith(var_domain);
937 if (rhs.IsFixed() || complement.IsFixed()) {
938 context->UpdateRuleStats("dual: make encoding equiv");
939 const int64_t value =
940 rhs.IsFixed() ? rhs.FixedValue() : complement.FixedValue();
941 int encoding_lit;
942 if (context->HasVarValueEncoding(var, value, &encoding_lit)) {
943 // If it is different, we have an equivalence now, and we can
944 // remove the constraint.
945 if (rhs.IsFixed()) {
946 if (encoding_lit == NegatedRef(ref)) continue;
947 context->StoreBooleanEqualityRelation(encoding_lit,
948 NegatedRef(ref));
949 } else {
950 if (encoding_lit == ref) continue;
951 context->StoreBooleanEqualityRelation(encoding_lit, ref);
952 }
953 context->working_model->mutable_constraints(ct_index)->Clear();
954 context->UpdateConstraintVariableUsage(ct_index);
955 processed[PositiveRef(ref)] = true;
956 processed[PositiveRef(var)] = true;
957 processed[PositiveRef(encoding_lit)] = true;
958 continue;
959 }
960
961 processed[PositiveRef(ref)] = true;
962 processed[PositiveRef(var)] = true;
963 ConstraintProto* new_ct = context->working_model->add_constraints();
964 new_ct->add_enforcement_literal(ref);
965 new_ct->mutable_linear()->add_vars(var);
966 new_ct->mutable_linear()->add_coeffs(1);
967 FillDomainInProto(complement, new_ct->mutable_linear());
968 context->UpdateNewConstraintsVariableUsage();
969
970 if (rhs.IsFixed()) {
971 context->StoreLiteralImpliesVarEqValue(NegatedRef(ref), var, value);
972 context->StoreLiteralImpliesVarNEqValue(ref, var, value);
973 } else if (complement.IsFixed()) {
974 context->StoreLiteralImpliesVarNEqValue(NegatedRef(ref), var,
975 value);
976 context->StoreLiteralImpliesVarEqValue(ref, var, value);
977 }
978 continue;
979 }
980 }
981
982 // If we have two Booleans, each with a single blocking constraint that is
983 // the same if we "swap" the Booleans reference, then we can make the
984 // Booleans equivalent. This is because they will be forced to their bad
985 // value only if it is needed by the other part of their respective
986 // blocking constraints. This is related to the
987 // FindAlmostIdenticalLinearConstraints() except that here we can deal
988 // with non-equality or enforced constraints.
989 //
990 // TODO(user): Generalize to non-Boolean. Also for Boolean, we might
991 // miss some possible reduction if replacing X by 1 - X make a constraint
992 // near-duplicate of another.
993 //
994 // TODO(user): We can generalize to non-linear constraint.
995 //
996 // Optimization: Skip if this constraint was already used as a single
997 // blocking constraint of another variable. If this is the case, it cannot
998 // be equivalent to another constraint with "var" substituted, since
999 // otherwise var would have two blocking constraint. We can safely skip
1000 // it. this make sure this is in O(num_entries) and not more.
1001 if (equiv_ct_index_set.insert(ct_index).second &&
1002 ct.constraint_case() == ConstraintProto::kLinear &&
1003 context->CanBeUsedAsLiteral(ref)) {
1004 TransformLinearWithSpecialBoolean(ct, ref, &temp_data);
1005 const uint64_t hash =
1006 fasthash64(temp_data.data(), temp_data.size() * sizeof(int64_t),
1007 uint64_t{0xa5b85c5e198ed849});
1008 const auto [it, inserted] =
1009 equiv_modified_constraints.insert({hash, {ct_index, ref}});
1010 if (!inserted) {
1011 // Already present!
1012 const auto [other_c_with_same_hash, other_ref] = it->second;
1013 CHECK_NE(other_c_with_same_hash, ct_index);
1014 const auto& other_ct =
1015 context->working_model->constraints(other_c_with_same_hash);
1016 TransformLinearWithSpecialBoolean(other_ct, other_ref,
1017 &other_temp_data);
1018 if (temp_data == other_temp_data) {
1019 // We have a true equality. The two ref can be made equivalent.
1020 if (!processed[PositiveRef(other_ref)]) {
1021 ++num_bool_in_near_duplicate_ct;
1022 processed[PositiveRef(ref)] = true;
1023 processed[PositiveRef(other_ref)] = true;
1024 context->StoreBooleanEqualityRelation(ref, other_ref);
1025
1026 // We can delete one of the constraint since they are duplicate
1027 // now.
1028 ++num_deleted_constraints_;
1029 context->working_model->mutable_constraints(ct_index)->Clear();
1030 context->UpdateConstraintVariableUsage(ct_index);
1031 continue;
1032 }
1033 }
1034 }
1035 }
1036
1037 // Other potential cases?
1038 if (!ct.enforcement_literal().empty()) {
1039 if (ct.constraint_case() == ConstraintProto::kLinear &&
1040 ct.linear().vars().size() == 1 &&
1041 ct.enforcement_literal().size() == 1 &&
1042 ct.enforcement_literal(0) == NegatedRef(ref)) {
1043 context->UpdateRuleStats("TODO dual: make linear1 equiv");
1044 } else {
1045 context->UpdateRuleStats(
1046 "TODO dual: only one blocking enforced constraint?");
1047 }
1048 } else {
1049 context->UpdateRuleStats("TODO dual: only one blocking constraint?");
1050 }
1051 continue;
1052 }
1053 if (ct.enforcement_literal().size() != 1) continue;
1054
1055 // If (a => b) is the only constraint blocking a literal a in the up
1056 // direction, then we can set a == b !
1057 //
1058 // Recover a => b where a is having an unique up_lock (i.e this constraint).
1059 // Note that if many implications are encoded in the same bool_and, we have
1060 // to be careful that a is appearing in just one of them.
1061 //
1062 // TODO(user): Make sure implication graph is transitively reduced to not
1063 // miss such reduction. More generally, this might only use the graph rather
1064 // than the encoding into bool_and / at_most_one ? Basically if a =>
1065 // all_direct_deduction, we can transform it into a <=> all_direct_deduction
1066 // if that is interesting. This could always be done on a max-2sat problem
1067 // in one of the two direction. Also think about max-2sat specific presolve.
1068 int a = ct.enforcement_literal(0);
1069 int b = 1;
1070 if (PositiveRef(a) == positive_ref &&
1071 num_locks_[RefToIntegerVariable(NegatedRef(a))] == 1) {
1072 // Here, we can only add the equivalence if the literal is the only
1073 // on the lhs, otherwise there is actually more lock.
1074 if (ct.bool_and().literals().size() != 1) continue;
1075 b = ct.bool_and().literals(0);
1076 } else {
1077 bool found = false;
1078 b = NegatedRef(ct.enforcement_literal(0));
1079 for (const int lhs : ct.bool_and().literals()) {
1080 if (PositiveRef(lhs) == positive_ref &&
1081 num_locks_[RefToIntegerVariable(lhs)] == 1) {
1082 found = true;
1083 a = NegatedRef(lhs);
1084 break;
1085 }
1086 }
1087 CHECK(found);
1088 }
1089 CHECK_EQ(num_locks_[RefToIntegerVariable(NegatedRef(a))], 1);
1090
1091 processed[PositiveRef(a)] = true;
1092 processed[PositiveRef(b)] = true;
1093 context->StoreBooleanEqualityRelation(a, b);
1094 context->UpdateRuleStats("dual: enforced equivalence");
1095 }
1096
1097 if (num_bool_in_near_duplicate_ct) {
1098 context->UpdateRuleStats(
1099 "dual: equivalent Boolean in near-duplicate constraints",
1100 num_bool_in_near_duplicate_ct);
1101 }
1102
1103 VLOG(2) << "Num deleted constraints: " << num_deleted_constraints_;
1104 return true;
1105}
1106
1108 VarDomination* var_domination) {
1109 if (context.ModelIsUnsat()) return;
1110
1111 const CpModelProto& cp_model = *context.working_model;
1112 const int num_vars = cp_model.variables().size();
1113 var_domination->Reset(num_vars);
1114
1115 for (int var = 0; var < num_vars; ++var) {
1116 // Ignore variables that have been substituted already or are unused.
1117 if (context.IsFixed(var) || context.VariableWasRemoved(var) ||
1118 context.VariableIsNotUsedAnymore(var)) {
1119 var_domination->CanOnlyDominateEachOther({var});
1120 continue;
1121 }
1122
1123 // Deal with the affine relations that are not part of the proto.
1124 // Those only need to be processed in the first pass.
1125 const AffineRelation::Relation r = context.GetAffineRelation(var);
1126 if (r.representative != var) {
1127 if (r.coeff == 1) {
1128 var_domination->CanOnlyDominateEachOther(
1130 } else if (r.coeff == -1) {
1131 var_domination->CanOnlyDominateEachOther({var, r.representative});
1132 } else {
1133 var_domination->CanOnlyDominateEachOther({var});
1134 var_domination->CanOnlyDominateEachOther({r.representative});
1135 }
1136 }
1137 }
1138
1139 // First scan: update the partition.
1140 const int num_constraints = cp_model.constraints_size();
1141 std::vector<bool> c_is_free_to_increase(num_constraints);
1142 std::vector<bool> c_is_free_to_decrease(num_constraints);
1143 for (int c = 0; c < num_constraints; ++c) {
1144 const ConstraintProto& ct = cp_model.constraints(c);
1145 switch (ct.constraint_case()) {
1146 case ConstraintProto::kBoolOr:
1147 break;
1148 case ConstraintProto::kBoolAnd:
1149 break;
1150 case ConstraintProto::kAtMostOne:
1151 break;
1152 case ConstraintProto::kExactlyOne:
1153 var_domination->ActivityShouldNotChange(ct.exactly_one().literals(),
1154 /*coeffs=*/{});
1155 break;
1156 case ConstraintProto::kLinear: {
1157 // TODO(user): Maybe we should avoid recomputing that here.
1158 const auto [min_activity, max_activity] =
1159 context.ComputeMinMaxActivity(ct.linear());
1160 const bool domain_is_simple = ct.linear().domain().size() == 2;
1161 const bool free_to_increase =
1162 domain_is_simple && ct.linear().domain(1) >= max_activity;
1163 const bool free_to_decrease =
1164 domain_is_simple && ct.linear().domain(0) <= min_activity;
1165 c_is_free_to_increase[c] = free_to_increase;
1166 c_is_free_to_decrease[c] = free_to_decrease;
1167 if (free_to_decrease && free_to_increase) break;
1168 if (!free_to_increase && !free_to_decrease) {
1169 var_domination->ActivityShouldNotChange(ct.linear().vars(),
1170 ct.linear().coeffs());
1171 }
1172 break;
1173 }
1174 default:
1175 // We cannot infer anything if we don't know the constraint.
1176 // TODO(user): Handle enforcement better here.
1177 for (const int var : context.ConstraintToVars(c)) {
1178 var_domination->CanOnlyDominateEachOther({var});
1179 }
1180 break;
1181 }
1182 }
1183
1184 // The objective is handled like a <= constraints, or an == constraint if
1185 // there is a non-trivial domain.
1186 if (cp_model.has_objective()) {
1187 // Important: We need to write the objective first to make sure it is up to
1188 // date.
1189 context.WriteObjectiveToProto();
1190 const auto [min_activity, max_activity] =
1191 context.ComputeMinMaxActivity(cp_model.objective());
1192 const auto& domain = cp_model.objective().domain();
1193 if (domain.empty() || (domain.size() == 2 && domain[0] <= min_activity)) {
1194 // do nothing for now.
1195 } else {
1196 var_domination->ActivityShouldNotChange(cp_model.objective().vars(),
1197 cp_model.objective().coeffs());
1198 }
1199 }
1200
1201 // Now do two more scan.
1202 // - the phase_ = 0 initialize candidate list, then EndFirstPhase()
1203 // - the phase_ = 1 filter them, then EndSecondPhase();
1204 std::vector<int> tmp;
1205 for (int phase = 0; phase < 2; phase++) {
1206 for (int c = 0; c < num_constraints; ++c) {
1207 const ConstraintProto& ct = cp_model.constraints(c);
1208 switch (ct.constraint_case()) {
1209 case ConstraintProto::kBoolOr:
1210 var_domination->ActivityShouldNotDecrease(ct.enforcement_literal(),
1211 ct.bool_or().literals(),
1212 /*coeffs=*/{});
1213 break;
1214 case ConstraintProto::kBoolAnd:
1215 // We process it like n clauses.
1216 //
1217 // TODO(user): the way we process that is a bit restrictive. By
1218 // working on the implication graph we could detect more dominance
1219 // relations. Since if a => b we say that a++ can only be paired with
1220 // b--, but it could actually be paired with any variables that when
1221 // dereased implies b = 0. This is a bit mitigated by the fact that
1222 // we regroup when we can such implications into big at most ones.
1223 tmp.clear();
1224 for (const int ref : ct.enforcement_literal()) {
1225 tmp.push_back(NegatedRef(ref));
1226 }
1227 for (const int ref : ct.bool_and().literals()) {
1228 tmp.push_back(ref);
1229 var_domination->ActivityShouldNotDecrease(/*enforcements=*/{}, tmp,
1230 /*coeffs=*/{});
1231 tmp.pop_back();
1232 }
1233 break;
1234 case ConstraintProto::kAtMostOne:
1235 var_domination->ActivityShouldNotIncrease(ct.enforcement_literal(),
1236 ct.at_most_one().literals(),
1237 /*coeffs=*/{});
1238 break;
1239 case ConstraintProto::kLinear: {
1240 const bool free_to_increase = c_is_free_to_increase[c];
1241 const bool free_to_decrease = c_is_free_to_decrease[c];
1242 if (free_to_decrease && free_to_increase) break;
1243 if (free_to_increase) {
1244 var_domination->ActivityShouldNotDecrease(ct.enforcement_literal(),
1245 ct.linear().vars(),
1246 ct.linear().coeffs());
1247 } else if (free_to_decrease) {
1248 var_domination->ActivityShouldNotIncrease(ct.enforcement_literal(),
1249 ct.linear().vars(),
1250 ct.linear().coeffs());
1251 } else {
1252 if (!ct.enforcement_literal().empty()) {
1253 var_domination->ActivityShouldNotIncrease(
1254 /*enforcements=*/{}, ct.enforcement_literal(), /*coeffs=*/{});
1255 }
1256 }
1257 break;
1258 }
1259 default:
1260 break;
1261 }
1262 }
1263
1264 // The objective is handled like a <= constraints, or an == constraint if
1265 // there is a non-trivial domain.
1266 if (cp_model.has_objective()) {
1267 const auto [min_activity, max_activity] =
1268 context.ComputeMinMaxActivity(cp_model.objective());
1269 const auto& domain = cp_model.objective().domain();
1270 if (domain.empty() || (domain.size() == 2 && domain[0] <= min_activity)) {
1271 var_domination->ActivityShouldNotIncrease(
1272 /*enforcements=*/{}, cp_model.objective().vars(),
1273 cp_model.objective().coeffs());
1274 }
1275 }
1276
1277 if (phase == 0) {
1278 // Early abort if no possible relations can be found.
1279 //
1280 // TODO(user): We might be able to detect that nothing can be done earlier
1281 // during the constraint scanning.
1282 if (!var_domination->EndFirstPhase()) return;
1283 } else {
1284 CHECK_EQ(phase, 1);
1285 var_domination->EndSecondPhase();
1286 }
1287 }
1288
1289 // Some statistics.
1290 int64_t num_unconstrained_refs = 0;
1291 int64_t num_dominated_refs = 0;
1292 int64_t num_dominance_relations = 0;
1293 for (int var = 0; var < num_vars; ++var) {
1294 if (context.IsFixed(var)) continue;
1295
1296 for (const int ref : {var, NegatedRef(var)}) {
1297 if (var_domination->CanFreelyDecrease(ref)) {
1298 num_unconstrained_refs++;
1299 } else if (!var_domination->DominatingVariables(ref).empty()) {
1300 num_dominated_refs++;
1301 num_dominance_relations +=
1302 var_domination->DominatingVariables(ref).size();
1303 }
1304 }
1305 }
1306 if (num_unconstrained_refs == 0 && num_dominated_refs == 0) return;
1307 VLOG(1) << "Dominance:" << " num_unconstrained_refs="
1308 << num_unconstrained_refs
1309 << " num_dominated_refs=" << num_dominated_refs
1310 << " num_dominance_relations=" << num_dominance_relations;
1311}
1312
1314 const PresolveContext& context,
1315 DualBoundStrengthening* dual_bound_strengthening) {
1316 if (context.ModelIsUnsat()) return;
1317 const CpModelProto& cp_model = *context.working_model;
1318 const int num_vars = cp_model.variables().size();
1319 dual_bound_strengthening->Reset(num_vars);
1320
1321 for (int var = 0; var < num_vars; ++var) {
1322 // Ignore variables that have been substituted already or are unused.
1323 if (context.IsFixed(var) || context.VariableWasRemoved(var) ||
1324 context.VariableIsNotUsedAnymore(var)) {
1325 dual_bound_strengthening->CannotMove({var});
1326 continue;
1327 }
1328
1329 // Deal with the affine relations that are not part of the proto.
1330 // Those only need to be processed in the first pass.
1331 const AffineRelation::Relation r = context.GetAffineRelation(var);
1332 if (r.representative != var) {
1333 dual_bound_strengthening->CannotMove({var, r.representative});
1334 }
1335 }
1336
1337 const int num_constraints = cp_model.constraints_size();
1338 for (int c = 0; c < num_constraints; ++c) {
1339 const ConstraintProto& ct = cp_model.constraints(c);
1340 dual_bound_strengthening->CannotIncrease(ct.enforcement_literal(), c);
1341 switch (ct.constraint_case()) {
1342 case ConstraintProto::kBoolOr:
1343 dual_bound_strengthening->CannotDecrease(ct.bool_or().literals(), c);
1344 break;
1345 case ConstraintProto::kBoolAnd:
1346 dual_bound_strengthening->CannotDecrease(ct.bool_and().literals(), c);
1347 break;
1348 case ConstraintProto::kAtMostOne:
1349 dual_bound_strengthening->CannotIncrease(ct.at_most_one().literals(),
1350 c);
1351 break;
1352 case ConstraintProto::kExactlyOne:
1353 dual_bound_strengthening->CannotMove(ct.exactly_one().literals(), c);
1354 break;
1355 case ConstraintProto::kLinear: {
1356 // TODO(user): Maybe we should avoid recomputing that here.
1357 const auto [min_activity, max_activity] =
1358 context.ComputeMinMaxActivity(ct.linear());
1359 dual_bound_strengthening->ProcessLinearConstraint(
1360 false, context, ct.linear(), min_activity, max_activity, c);
1361 break;
1362 }
1363 default:
1364 // We cannot infer anything if we don't know the constraint.
1365 // TODO(user): Handle enforcement better here.
1366 dual_bound_strengthening->CannotMove(context.ConstraintToVars(c), c);
1367 break;
1368 }
1369 }
1370
1371 // The objective is handled like a <= constraints, or an == constraint if
1372 // there is a non-trivial domain.
1373 if (cp_model.has_objective()) {
1374 // WARNING: The proto objective might not be up to date, so we need to
1375 // write it first.
1376 context.WriteObjectiveToProto();
1377 const auto [min_activity, max_activity] =
1378 context.ComputeMinMaxActivity(cp_model.objective());
1379 dual_bound_strengthening->ProcessLinearConstraint(
1380 true, context, cp_model.objective(), min_activity, max_activity);
1381 }
1382}
1383
1384namespace {
1385
1386bool ProcessAtMostOne(absl::Span<const int> literals,
1387 const std::string& message,
1388 const VarDomination& var_domination,
1390 PresolveContext* context) {
1391 for (const int ref : literals) {
1392 (*in_constraints)[VarDomination::RefToIntegerVariable(ref)] = true;
1393 }
1394 for (const int ref : literals) {
1395 if (context->IsFixed(ref)) continue;
1396
1397 const auto dominating_ivars = var_domination.DominatingVariables(ref);
1398 for (const IntegerVariable ivar : dominating_ivars) {
1399 if (!(*in_constraints)[ivar]) continue;
1400 if (context->IsFixed(VarDomination::IntegerVariableToRef(ivar))) {
1401 continue;
1402 }
1403
1404 // We can set the dominated variable to false.
1405 context->UpdateRuleStats(message);
1406 if (!context->SetLiteralToFalse(ref)) return false;
1407 break;
1408 }
1409 }
1410 for (const int ref : literals) {
1411 (*in_constraints)[VarDomination::RefToIntegerVariable(ref)] = false;
1412 }
1413 return true;
1414}
1415
1416} // namespace
1417
1418bool ExploitDominanceRelations(const VarDomination& var_domination,
1420 const CpModelProto& cp_model = *context->working_model;
1421 const int num_vars = cp_model.variables_size();
1422
1423 // Abort early if there is nothing to do.
1424 bool work_to_do = false;
1425 for (int var = 0; var < num_vars; ++var) {
1426 if (context->IsFixed(var)) continue;
1427 if (!var_domination.DominatingVariables(var).empty() ||
1428 !var_domination.DominatingVariables(NegatedRef(var)).empty()) {
1429 work_to_do = true;
1430 break;
1431 }
1432 }
1433 if (!work_to_do) return true;
1434
1435 absl::StrongVector<IntegerVariable, int64_t> var_lb_to_ub_diff(num_vars * 2,
1436 0);
1437 absl::StrongVector<IntegerVariable, bool> in_constraints(num_vars * 2, false);
1438
1439 absl::flat_hash_set<std::pair<int, int>> implications;
1440 const int num_constraints = cp_model.constraints_size();
1441 for (int c = 0; c < num_constraints; ++c) {
1442 const ConstraintProto& ct = cp_model.constraints(c);
1443
1444 if (ct.constraint_case() == ConstraintProto::kBoolAnd) {
1445 if (ct.enforcement_literal().size() != 1) continue;
1446 const int a = ct.enforcement_literal(0);
1447 if (context->IsFixed(a)) continue;
1448 for (const int b : ct.bool_and().literals()) {
1449 if (context->IsFixed(b)) continue;
1450 implications.insert({a, b});
1451 implications.insert({NegatedRef(b), NegatedRef(a)});
1452
1453 // If (a--, b--) is valid, we can always set a to false.
1454 for (const IntegerVariable ivar :
1455 var_domination.DominatingVariables(a)) {
1456 const int ref = VarDomination::IntegerVariableToRef(ivar);
1457 if (ref == NegatedRef(b)) {
1458 context->UpdateRuleStats("domination: in implication");
1459 if (!context->SetLiteralToFalse(a)) return false;
1460 break;
1461 }
1462 }
1463 if (context->IsFixed(a)) break;
1464
1465 // If (b++, a++) is valid, then we can always set b to true.
1466 for (const IntegerVariable ivar :
1467 var_domination.DominatingVariables(NegatedRef(b))) {
1468 const int ref = VarDomination::IntegerVariableToRef(ivar);
1469 if (ref == a) {
1470 context->UpdateRuleStats("domination: in implication");
1471 if (!context->SetLiteralToTrue(b)) return false;
1472 break;
1473 }
1474 }
1475 }
1476 continue;
1477 }
1478
1479 if (!ct.enforcement_literal().empty()) continue;
1480
1481 // TODO(user): More generally, combine with probing? if a dominated
1482 // variable implies one of its dominant to zero, then it can be set to
1483 // zero. It seems adding the implication below should have the same
1484 // effect? but currently it requires a lot of presolve rounds.
1485 const auto add_implications = [&implications](absl::Span<const int> lits) {
1486 if (lits.size() > 3) return;
1487 for (int i = 0; i < lits.size(); ++i) {
1488 for (int j = 0; j < lits.size(); ++j) {
1489 if (i == j) continue;
1490 implications.insert({lits[i], NegatedRef(lits[j])});
1491 }
1492 }
1493 };
1494 if (ct.constraint_case() == ConstraintProto::kAtMostOne) {
1495 add_implications(ct.at_most_one().literals());
1496 if (!ProcessAtMostOne(ct.at_most_one().literals(),
1497 "domination: in at most one", var_domination,
1498 &in_constraints, context)) {
1499 return false;
1500 }
1501 } else if (ct.constraint_case() == ConstraintProto::kExactlyOne) {
1502 add_implications(ct.exactly_one().literals());
1503 if (!ProcessAtMostOne(ct.exactly_one().literals(),
1504 "domination: in exactly one", var_domination,
1505 &in_constraints, context)) {
1506 return false;
1507 }
1508 }
1509
1510 if (ct.constraint_case() != ConstraintProto::kLinear) continue;
1511
1512 int num_dominated = 0;
1513 for (const int var : context->ConstraintToVars(c)) {
1514 if (!var_domination.DominatingVariables(var).empty()) ++num_dominated;
1515 if (!var_domination.DominatingVariables(NegatedRef(var)).empty()) {
1516 ++num_dominated;
1517 }
1518 }
1519 if (num_dominated == 0) continue;
1520
1521 // Precompute.
1522 int64_t min_activity = 0;
1523 int64_t max_activity = 0;
1524 const int num_terms = ct.linear().vars_size();
1525 for (int i = 0; i < num_terms; ++i) {
1526 int ref = ct.linear().vars(i);
1527 int64_t coeff = ct.linear().coeffs(i);
1528 if (coeff < 0) {
1529 ref = NegatedRef(ref);
1530 coeff = -coeff;
1531 }
1532 const int64_t min_term = coeff * context->MinOf(ref);
1533 const int64_t max_term = coeff * context->MaxOf(ref);
1534 min_activity += min_term;
1535 max_activity += max_term;
1536 const IntegerVariable ivar = VarDomination::RefToIntegerVariable(ref);
1537 var_lb_to_ub_diff[ivar] = max_term - min_term;
1538 var_lb_to_ub_diff[NegationOf(ivar)] = min_term - max_term;
1539 }
1540 const int64_t rhs_lb = ct.linear().domain(0);
1541 const int64_t rhs_ub = ct.linear().domain(ct.linear().domain_size() - 1);
1542 if (max_activity < rhs_lb || min_activity > rhs_ub) {
1543 return context->NotifyThatModelIsUnsat("linear equation unsat.");
1544 }
1545
1546 // Look for dominated var.
1547 for (int i = 0; i < num_terms; ++i) {
1548 const int ref = ct.linear().vars(i);
1549 const int64_t coeff = ct.linear().coeffs(i);
1550 const int64_t coeff_magnitude = std::abs(coeff);
1551 if (context->IsFixed(ref)) continue;
1552
1553 for (const int current_ref : {ref, NegatedRef(ref)}) {
1554 const absl::Span<const IntegerVariable> dominated_by =
1555 var_domination.DominatingVariables(current_ref);
1556 if (dominated_by.empty()) continue;
1557
1558 const bool ub_side = (coeff > 0) == (current_ref == ref);
1559 if (ub_side) {
1560 if (max_activity <= rhs_ub) continue;
1561 } else {
1562 if (min_activity >= rhs_lb) continue;
1563 }
1564 const int64_t slack =
1565 ub_side ? rhs_ub - min_activity : max_activity - rhs_lb;
1566
1567 // Compute the delta in activity if all dominating var moves to their
1568 // other bound.
1569 int64_t delta = 0;
1570 for (const IntegerVariable ivar : dominated_by) {
1571 // Tricky: For now we skip complex domain as we are not sure they
1572 // can be moved correctly.
1573 if (context->DomainOf(VarDomination::IntegerVariableToRef(ivar))
1574 .NumIntervals() != 1) {
1575 continue;
1576 }
1577 if (ub_side) {
1578 delta += std::max(int64_t{0}, var_lb_to_ub_diff[ivar]);
1579 } else {
1580 delta += std::max(int64_t{0}, -var_lb_to_ub_diff[ivar]);
1581 }
1582 }
1583
1584 const int64_t lb = context->MinOf(current_ref);
1585 if (delta + coeff_magnitude > slack) {
1586 context->UpdateRuleStats("domination: fixed to lb.");
1587 if (!context->IntersectDomainWith(current_ref, Domain(lb))) {
1588 return false;
1589 }
1590
1591 // We need to update the precomputed quantities.
1592 const IntegerVariable current_var =
1594 if (ub_side) {
1595 CHECK_GE(var_lb_to_ub_diff[current_var], 0);
1596 max_activity -= var_lb_to_ub_diff[current_var];
1597 } else {
1598 CHECK_LE(var_lb_to_ub_diff[current_var], 0);
1599 min_activity -= var_lb_to_ub_diff[current_var];
1600 }
1601 var_lb_to_ub_diff[current_var] = 0;
1602 var_lb_to_ub_diff[NegationOf(current_var)] = 0;
1603
1604 continue;
1605 }
1606
1607 const IntegerValue diff = FloorRatio(IntegerValue(slack - delta),
1608 IntegerValue(coeff_magnitude));
1609 int64_t new_ub = lb + diff.value();
1610 if (new_ub < context->MaxOf(current_ref)) {
1611 // Tricky: If there are holes, we can't just reduce the domain to
1612 // new_ub if it is not a valid value, so we need to compute the
1613 // Min() of the intersection.
1614 new_ub = context->DomainOf(current_ref)
1615 .IntersectionWith(
1616 Domain(new_ub, std::numeric_limits<int64_t>::max()))
1617 .Min();
1618 }
1619 if (new_ub < context->MaxOf(current_ref)) {
1620 context->UpdateRuleStats("domination: reduced ub.");
1621 if (!context->IntersectDomainWith(current_ref, Domain(lb, new_ub))) {
1622 return false;
1623 }
1624
1625 // We need to update the precomputed quantities.
1626 const IntegerVariable current_var =
1628 if (ub_side) {
1629 CHECK_GE(var_lb_to_ub_diff[current_var], 0);
1630 max_activity -= var_lb_to_ub_diff[current_var];
1631 } else {
1632 CHECK_LE(var_lb_to_ub_diff[current_var], 0);
1633 min_activity -= var_lb_to_ub_diff[current_var];
1634 }
1635 const int64_t new_diff = std::abs(coeff_magnitude * (new_ub - lb));
1636 if (ub_side) {
1637 var_lb_to_ub_diff[current_var] = new_diff;
1638 var_lb_to_ub_diff[NegationOf(current_var)] = -new_diff;
1639 max_activity += new_diff;
1640 } else {
1641 var_lb_to_ub_diff[current_var] = -new_diff;
1642 var_lb_to_ub_diff[NegationOf(current_var)] = +new_diff;
1643 min_activity -= new_diff;
1644 }
1645 }
1646 }
1647 }
1648
1649 // Restore.
1650 for (const int ref : ct.linear().vars()) {
1651 const IntegerVariable ivar = VarDomination::RefToIntegerVariable(ref);
1652 var_lb_to_ub_diff[ivar] = 0;
1653 var_lb_to_ub_diff[NegationOf(ivar)] = 0;
1654 }
1655 }
1656
1657 // For any dominance relation still left (i.e. between non-fixed vars), if
1658 // the variable are Boolean and X is dominated by Y, we can add
1659 // (X = 1) => (Y = 1). But, as soon as we do that, we break some symmetry
1660 // and cannot add any incompatible relations.
1661 //
1662 // EX: It is possible that X dominate Y and Y dominate X if they are both
1663 // appearing in exactly the same constraint with the same coefficient.
1664 //
1665 // TODO(user): if both variable are in a bool_or, this will allow us to
1666 // remove the dominated variable. Maybe we should exploit that to decide
1667 // which implication we add. Or just remove such variable and not add the
1668 // implications?
1669 //
1670 // TODO(user): generalize to non Booleans?
1671 int num_added = 0;
1672 absl::StrongVector<IntegerVariable, bool> increase_is_forbidden(2 * num_vars,
1673 false);
1674 for (int positive_ref = 0; positive_ref < num_vars; ++positive_ref) {
1675 if (context->IsFixed(positive_ref)) continue;
1676 if (context->VariableIsNotUsedAnymore(positive_ref)) continue;
1677 if (context->VariableWasRemoved(positive_ref)) continue;
1678 if (!context->CanBeUsedAsLiteral(positive_ref)) continue;
1679 for (const int ref : {positive_ref, NegatedRef(positive_ref)}) {
1680 const IntegerVariable var = VarDomination::RefToIntegerVariable(ref);
1681 if (increase_is_forbidden[NegationOf(var)]) continue;
1682 for (const IntegerVariable dom :
1683 var_domination.DominatingVariables(ref)) {
1684 if (increase_is_forbidden[dom]) continue;
1685 const int dom_ref = VarDomination::IntegerVariableToRef(dom);
1686 if (context->IsFixed(dom_ref)) continue;
1687 if (context->VariableIsNotUsedAnymore(dom_ref)) continue;
1688 if (context->VariableWasRemoved(dom_ref)) continue;
1689 if (!context->CanBeUsedAsLiteral(dom_ref)) continue;
1690 if (implications.contains({ref, dom_ref})) continue;
1691
1692 ++num_added;
1693 context->AddImplication(ref, dom_ref);
1694 implications.insert({ref, dom_ref});
1695 implications.insert({NegatedRef(dom_ref), NegatedRef(ref)});
1696
1697 // dom-- or var++ are now forbidden.
1698 increase_is_forbidden[var] = true;
1699 increase_is_forbidden[NegationOf(dom)] = true;
1700 }
1701 }
1702 }
1703 if (num_added > 0) {
1704 VLOG(1) << "Added " << num_added << " domination implications.";
1705 context->UpdateNewConstraintsVariableUsage();
1706 context->UpdateRuleStats("domination: added implications", num_added);
1707 }
1708
1709 return true;
1710}
1711
1712} // namespace sat
1713} // namespace operations_research
STL vector ---------------------------------------------------------------—.
void resize(size_type new_size)
void assign(size_type n, const value_type &val)
– Pass-through methods to STL vector ----------------------------------—
size_type size() const
std::vector< int64_t > FlattenedIntervals() const
Domain IntersectionWith(const Domain &domain) const
bool Contains(int64_t value) const
Domain InverseMultiplicationBy(int64_t coeff) const
void CannotIncrease(absl::Span< const int > refs, int ct_index=-1)
void CannotDecrease(absl::Span< const int > refs, int ct_index=-1)
All constraints should be mapped to one of more call to these functions.
void CannotMove(absl::Span< const int > refs, int ct_index=-1)
void ProcessLinearConstraint(bool is_objective, const PresolveContext &context, const LinearProto &linear, int64_t min_activity, int64_t max_activity, int ct_index=-1)
Most of the logic here deals with linear constraints.
static int IntegerVariableToRef(IntegerVariable var)
absl::Span< const IntegerVariable > DominatingVariables(int ref) const
static IntegerVariable RefToIntegerVariable(int ref)
std::string DominationDebugString(IntegerVariable var) const
void CanOnlyDominateEachOther(absl::Span< const int > refs)
void ActivityShouldNotIncrease(absl::Span< const int > enforcements, absl::Span< const int > refs, absl::Span< const int64_t > coeffs)
void ActivityShouldNotChange(absl::Span< const int > refs, absl::Span< const int64_t > coeffs)
void ActivityShouldNotDecrease(absl::Span< const int > enforcements, absl::Span< const int > refs, absl::Span< const int64_t > coeffs)
int64_t a
Definition table.cc:44
std::unique_ptr< SharedBoundsManager > bounds
These can be nullptr depending on the options.
DecisionBuilder *const phase
The decision builder we are going to use in this dive.
const Constraint * ct
int64_t value
IntVar * var
GurobiMPCallbackContext * context
int literal
int ct_index
const bool DEBUG_MODE
Definition macros.h:24
int64_t hash
void STLClearObject(T *obj)
Definition stl_util.h:123
IntegerValue FloorRatio(IntegerValue dividend, IntegerValue positive_divisor)
Definition integer.h:94
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
bool ExploitDominanceRelations(const VarDomination &var_domination, PresolveContext *context)
IntegerValue CeilRatio(IntegerValue dividend, IntegerValue positive_divisor)
Definition integer.h:85
std::vector< IntegerVariable > NegationOf(const std::vector< IntegerVariable > &vars)
Returns the vector of the negated variables.
Definition integer.cc:51
void ScanModelForDualBoundStrengthening(const PresolveContext &context, DualBoundStrengthening *dual_bound_strengthening)
Scan the model so that dual_bound_strengthening.Strenghten() works.
IntegerVariable PositiveVariable(IntegerVariable i)
Definition integer.h:193
std::string FormatCounter(int64_t num)
Prints a positive number with separators for easier reading (ex: 1'348'065).
Definition util.cc:48
void FillDomainInProto(const Domain &domain, ProtoWithDomain *proto)
Serializes a Domain into the domain field of a proto.
Domain ReadDomainFromProto(const ProtoWithDomain &proto)
Reads a Domain from the domain field of a proto.
void ScanModelForDominanceDetection(PresolveContext &context, VarDomination *var_domination)
int NegatedRef(int ref)
Small utility functions to deal with negative variable/literal references.
In SWIG mode, we don't want anything besides these top-level includes.
int64_t CapSub(int64_t x, int64_t y)
uint64_t fasthash64(const void *buf, size_t len, uint64_t seed)
Definition hash.cc:36
const Variable x
Definition qp_tests.cc:127
int64_t delta
Definition resource.cc:1709
Fractional coeff_magnitude
int64_t bound
std::optional< int64_t > end
int64_t start
std::string message
Definition trace.cc:397